import org.asamk.signal.manager.storage.groups.GroupInfo;
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
+import org.asamk.signal.manager.storage.identities.IdentityInfo;
import org.asamk.signal.manager.storage.messageCache.CachedMessage;
import org.asamk.signal.manager.storage.profiles.SignalProfile;
-import org.asamk.signal.manager.storage.protocol.IdentityInfo;
+import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.stickers.Sticker;
import org.asamk.signal.manager.util.AttachmentUtils;
import org.asamk.signal.manager.util.IOUtils;
clientZkProfileOperations,
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
- this.account.setResolver(this::resolveSignalServiceAddress);
-
this.unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey,
account.getProfileStore()::getProfileKey,
this::getRecipientProfile,
private void sendSyncMessage(SignalServiceSyncMessage message) throws IOException, UntrustedIdentityException {
var messageSender = createMessageSender();
- try {
- messageSender.sendMessage(message, unidentifiedAccessHelper.getAccessForSync());
- } catch (UntrustedIdentityException e) {
- if (e.getIdentityKey() != null) {
- account.getSignalProtocolStore()
- .saveIdentity(resolveSignalServiceAddress(e.getIdentifier()),
- e.getIdentityKey(),
- TrustLevel.UNTRUSTED);
- }
- throw e;
- }
+ messageSender.sendMessage(message, unidentifiedAccessHelper.getAccessForSync());
}
private Collection<SignalServiceAddress> getSignalServiceAddresses(Collection<String> numbers) throws InvalidNumberException {
unidentifiedAccessHelper.getAccessFor(recipients),
isRecipientUpdate,
message);
- for (var r : result) {
- if (r.getIdentityFailure() != null) {
- account.getSignalProtocolStore()
- .saveIdentity(r.getAddress(),
- r.getIdentityFailure().getIdentityKey(),
- TrustLevel.UNTRUSTED);
- }
- }
return new Pair<>(timestamp, result);
} catch (UntrustedIdentityException e) {
- if (e.getIdentityKey() != null) {
- account.getSignalProtocolStore()
- .saveIdentity(resolveSignalServiceAddress(e.getIdentifier()),
- e.getIdentityKey(),
- TrustLevel.UNTRUSTED);
- }
return new Pair<>(timestamp, List.of());
}
} else {
false,
System.currentTimeMillis() - startTime);
} catch (UntrustedIdentityException e) {
- if (e.getIdentityKey() != null) {
- account.getSignalProtocolStore()
- .saveIdentity(resolveSignalServiceAddress(e.getIdentifier()),
- e.getIdentityKey(),
- TrustLevel.UNTRUSTED);
- }
return SendMessageResult.identityFailure(recipient, e.getIdentityKey());
}
}
try {
return messageSender.sendMessage(address, unidentifiedAccessHelper.getAccessFor(address), message);
} catch (UntrustedIdentityException e) {
- if (e.getIdentityKey() != null) {
- account.getSignalProtocolStore()
- .saveIdentity(resolveSignalServiceAddress(e.getIdentifier()),
- e.getIdentityKey(),
- TrustLevel.UNTRUSTED);
- }
return SendMessageResult.identityFailure(address, e.getIdentityKey());
}
}
return cipher.decrypt(envelope);
} catch (ProtocolUntrustedIdentityException e) {
if (e.getCause() instanceof org.whispersystems.libsignal.UntrustedIdentityException) {
- var identityException = (org.whispersystems.libsignal.UntrustedIdentityException) e.getCause();
- final var untrustedIdentity = identityException.getUntrustedIdentity();
- if (untrustedIdentity != null) {
- account.getSignalProtocolStore()
- .saveIdentity(resolveSignalServiceAddress(identityException.getName()),
- untrustedIdentity,
- TrustLevel.UNTRUSTED);
- }
- throw identityException;
+ throw (org.whispersystems.libsignal.UntrustedIdentityException) e.getCause();
}
throw new AssertionError(e);
}
}
if (c.getVerified().isPresent()) {
final var verifiedMessage = c.getVerified().get();
- account.getSignalProtocolStore()
- .setIdentityTrustLevel(verifiedMessage.getDestination(),
+ account.getIdentityKeyStore()
+ .setIdentityTrustLevel(resolveRecipientTrusted(verifiedMessage.getDestination()),
verifiedMessage.getIdentityKey(),
TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
}
}
if (syncMessage.getVerified().isPresent()) {
final var verifiedMessage = syncMessage.getVerified().get();
- account.getSignalProtocolStore()
- .setIdentityTrustLevel(resolveSignalServiceAddress(verifiedMessage.getDestination()),
+ account.getIdentityKeyStore()
+ .setIdentityTrustLevel(resolveRecipientTrusted(verifiedMessage.getDestination()),
verifiedMessage.getIdentityKey(),
TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
}
var out = new DeviceContactsOutputStream(fos);
for (var record : account.getContactStore().getContacts()) {
VerifiedMessage verifiedMessage = null;
- var currentIdentity = account.getSignalProtocolStore().getIdentity(record.getAddress());
+ var currentIdentity = account.getIdentityKeyStore()
+ .getIdentity(resolveRecipientTrusted(record.getAddress()));
if (currentIdentity != null) {
verifiedMessage = new VerifiedMessage(record.getAddress(),
currentIdentity.getIdentityKey(),
}
public List<IdentityInfo> getIdentities() {
- return account.getSignalProtocolStore().getIdentities();
+ return account.getIdentityKeyStore().getIdentities();
}
public List<IdentityInfo> getIdentities(String number) throws InvalidNumberException {
- return account.getSignalProtocolStore().getIdentities(canonicalizeAndResolveSignalServiceAddress(number));
+ final var identity = account.getIdentityKeyStore().getIdentity(canonicalizeAndResolveRecipient(number));
+ return identity == null ? List.of() : List.of(identity);
}
/**
* @param fingerprint Fingerprint
*/
public boolean trustIdentityVerified(String name, byte[] fingerprint) throws InvalidNumberException {
- var address = canonicalizeAndResolveSignalServiceAddress(name);
- return trustIdentity(address, (identityKey) -> Arrays.equals(identityKey.serialize(), fingerprint));
+ var recipientId = canonicalizeAndResolveRecipient(name);
+ return trustIdentity(recipientId,
+ identityKey -> Arrays.equals(identityKey.serialize(), fingerprint),
+ TrustLevel.TRUSTED_VERIFIED);
}
/**
* @param safetyNumber Safety number
*/
public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) throws InvalidNumberException {
- var address = canonicalizeAndResolveSignalServiceAddress(name);
- return trustIdentity(address, (identityKey) -> safetyNumber.equals(computeSafetyNumber(address, identityKey)));
+ var recipientId = canonicalizeAndResolveRecipient(name);
+ var address = account.getRecipientStore().resolveServiceAddress(recipientId);
+ return trustIdentity(recipientId,
+ identityKey -> safetyNumber.equals(computeSafetyNumber(address, identityKey)),
+ TrustLevel.TRUSTED_VERIFIED);
}
- private boolean trustIdentity(SignalServiceAddress address, Function<IdentityKey, Boolean> verifier) {
- var ids = account.getSignalProtocolStore().getIdentities(address);
- if (ids == null) {
- return false;
- }
-
- IdentityInfo foundIdentity = null;
+ /**
+ * Trust all keys of this identity without verification
+ *
+ * @param name username of the identity
+ */
+ public boolean trustIdentityAllKeys(String name) throws InvalidNumberException {
+ var recipientId = canonicalizeAndResolveRecipient(name);
+ return trustIdentity(recipientId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED);
+ }
- for (var id : ids) {
- if (verifier.apply(id.getIdentityKey())) {
- foundIdentity = id;
- break;
- }
+ private boolean trustIdentity(
+ RecipientId recipientId, Function<IdentityKey, Boolean> verifier, TrustLevel trustLevel
+ ) {
+ var identity = account.getIdentityKeyStore().getIdentity(recipientId);
+ if (identity == null) {
+ return false;
}
- if (foundIdentity == null) {
+ if (!verifier.apply(identity.getIdentityKey())) {
return false;
}
- account.getSignalProtocolStore()
- .setIdentityTrustLevel(address, foundIdentity.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
+ account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identity.getIdentityKey(), trustLevel);
try {
- sendVerifiedMessage(address, foundIdentity.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
+ var address = account.getRecipientStore().resolveServiceAddress(recipientId);
+ sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel);
} catch (IOException | UntrustedIdentityException e) {
logger.warn("Failed to send verification sync message: {}", e.getMessage());
}
- // Successfully trusted the new identity, now remove all other identities for that number
- for (var id : ids) {
- if (id == foundIdentity) {
- continue;
- }
- account.getSignalProtocolStore().removeIdentity(address, id.getIdentityKey());
- }
-
- account.save();
- return true;
- }
-
- /**
- * Trust all keys of this identity without verification
- *
- * @param name username of the identity
- */
- public boolean trustIdentityAllKeys(String name) {
- var address = resolveSignalServiceAddress(name);
- var ids = account.getSignalProtocolStore().getIdentities(address);
- if (ids == null) {
- return false;
- }
- for (var id : ids) {
- if (id.getTrustLevel() == TrustLevel.UNTRUSTED) {
- account.getSignalProtocolStore()
- .setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED);
- try {
- sendVerifiedMessage(address, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED);
- } catch (IOException | UntrustedIdentityException e) {
- logger.warn("Failed to send verification sync message: {}", e.getMessage());
- }
- }
- }
- account.save();
return true;
}
theirIdentityKey);
}
+ @Deprecated
public SignalServiceAddress canonicalizeAndResolveSignalServiceAddress(String identifier) throws InvalidNumberException {
var canonicalizedNumber = UuidUtil.isUuid(identifier)
? identifier
return resolveSignalServiceAddress(canonicalizedNumber);
}
+ @Deprecated
public SignalServiceAddress resolveSignalServiceAddress(String identifier) {
var address = Utils.getSignalServiceAddressFromIdentifier(identifier);
return resolveSignalServiceAddress(address);
}
+ @Deprecated
public SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address) {
if (address.matches(account.getSelfAddress())) {
return account.getSelfAddress();
return account.getRecipientStore().resolveServiceAddress(address);
}
+ public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) {
+ return account.getRecipientStore().resolveServiceAddress(recipientId);
+ }
+
+ public RecipientId canonicalizeAndResolveRecipient(String identifier) throws InvalidNumberException {
+ var canonicalizedNumber = UuidUtil.isUuid(identifier)
+ ? identifier
+ : PhoneNumberFormatter.formatNumber(identifier, account.getUsername());
+ var address = Utils.getSignalServiceAddressFromIdentifier(canonicalizedNumber);
+
+ return resolveRecipient(address);
+ }
+
+ public RecipientId resolveRecipient(SignalServiceAddress address) {
+ return account.getRecipientStore().resolveRecipientUntrusted(address);
+ }
+
+ private RecipientId resolveRecipientTrusted(SignalServiceAddress address) {
+ return account.getRecipientStore().resolveRecipient(address);
+ }
+
@Override
public void close() throws IOException {
close(true);
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
+import java.util.Date;
import java.util.Locale;
public class RegistrationManager implements Closeable {
account.setUuid(UuidUtil.parseOrNull(response.getUuid()));
account.setRegistrationLockPin(pin);
account.getSessionStore().archiveAllSessions();
- account.getSignalProtocolStore()
- .saveIdentity(account.getSelfAddress(),
- account.getIdentityKeyPair().getPublicKey(),
- TrustLevel.TRUSTED_VERIFIED);
+ final var recipientId = account.getRecipientStore().resolveRecipient(account.getSelfAddress());
+ final var publicKey = account.getIdentityKeyPair().getPublicKey();
+ account.getIdentityKeyStore().saveIdentity(recipientId, publicKey, new Date());
+ account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, publicKey, TrustLevel.TRUSTED_VERIFIED);
Manager m = null;
try {
import org.asamk.signal.manager.storage.contacts.JsonContactsStore;
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
import org.asamk.signal.manager.storage.groups.JsonGroupStore;
+import org.asamk.signal.manager.storage.identities.IdentityKeyStore;
import org.asamk.signal.manager.storage.messageCache.MessageCache;
import org.asamk.signal.manager.storage.prekeys.PreKeyStore;
import org.asamk.signal.manager.storage.prekeys.SignedPreKeyStore;
import org.asamk.signal.manager.storage.profiles.ProfileStore;
-import org.asamk.signal.manager.storage.protocol.JsonSignalProtocolStore;
-import org.asamk.signal.manager.storage.protocol.SignalServiceAddressResolver;
+import org.asamk.signal.manager.storage.protocol.LegacyJsonSignalProtocolStore;
+import org.asamk.signal.manager.storage.protocol.SignalProtocolStore;
import org.asamk.signal.manager.storage.recipients.LegacyRecipientStore;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.recipients.RecipientStore;
private boolean registered = false;
- private JsonSignalProtocolStore signalProtocolStore;
+ private SignalProtocolStore signalProtocolStore;
private PreKeyStore preKeyStore;
private SignedPreKeyStore signedPreKeyStore;
private SessionStore sessionStore;
+ private IdentityKeyStore identityKeyStore;
private JsonGroupStore groupStore;
private JsonContactsStore contactStore;
private RecipientStore recipientStore;
account.signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username));
account.sessionStore = new SessionStore(getSessionsPath(dataPath, username),
account.recipientStore::resolveRecipient);
- account.signalProtocolStore = new JsonSignalProtocolStore(identityKey,
- registrationId,
- account.preKeyStore,
+ account.identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, username),
+ account.recipientStore::resolveRecipient,
+ identityKey,
+ registrationId);
+ account.signalProtocolStore = new SignalProtocolStore(account.preKeyStore,
account.signedPreKeyStore,
- account.sessionStore);
+ account.sessionStore,
+ account.identityKeyStore);
account.profileStore = new ProfileStore();
account.stickerStore = new StickerStore();
account.signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username));
account.sessionStore = new SessionStore(getSessionsPath(dataPath, username),
account.recipientStore::resolveRecipient);
- account.signalProtocolStore = new JsonSignalProtocolStore(identityKey,
- registrationId,
- account.preKeyStore,
+ account.identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, username),
+ account.recipientStore::resolveRecipient,
+ identityKey,
+ registrationId);
+ account.signalProtocolStore = new SignalProtocolStore(account.preKeyStore,
account.signedPreKeyStore,
- account.sessionStore);
+ account.sessionStore,
+ account.identityKeyStore);
account.profileStore = new ProfileStore();
account.stickerStore = new StickerStore();
private void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
sessionStore.mergeRecipients(recipientId, toBeMergedRecipientId);
+ identityKeyStore.mergeRecipients(recipientId, toBeMergedRecipientId);
}
public static File getFileName(File dataPath, String username) {
return new File(getUserPath(dataPath, username), "signed-pre-keys");
}
+ private static File getIdentitiesPath(File dataPath, String username) {
+ return new File(getUserPath(dataPath, username), "identities");
+ }
+
private static File getSessionsPath(File dataPath, String username) {
return new File(getUserPath(dataPath, username), "sessions");
}
}
username = Utils.getNotNullNode(rootNode, "username").asText();
password = Utils.getNotNullNode(rootNode, "password").asText();
+ int registrationId = 0;
+ if (rootNode.hasNonNull("registrationId")) {
+ registrationId = rootNode.get("registrationId").asInt();
+ }
+ IdentityKeyPair identityKeyPair = null;
+ if (rootNode.hasNonNull("identityPrivateKey") && rootNode.hasNonNull("identityKey")) {
+ final var publicKeyBytes = Base64.getDecoder().decode(rootNode.get("identityKey").asText());
+ final var privateKeyBytes = Base64.getDecoder().decode(rootNode.get("identityPrivateKey").asText());
+ identityKeyPair = KeyUtils.getIdentityKeyPair(publicKeyBytes, privateKeyBytes);
+ }
+
if (rootNode.hasNonNull("registrationLockPin")) {
registrationLockPin = rootNode.get("registrationLockPin").asText();
}
}
}
- signalProtocolStore = jsonProcessor.convertValue(Utils.getNotNullNode(rootNode, "axolotlStore"),
- JsonSignalProtocolStore.class);
+ var legacySignalProtocolStore = rootNode.hasNonNull("axolotlStore")
+ ? jsonProcessor.convertValue(Utils.getNotNullNode(rootNode, "axolotlStore"),
+ LegacyJsonSignalProtocolStore.class)
+ : null;
preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username));
- if (signalProtocolStore.getLegacyPreKeyStore() != null) {
+ if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyPreKeyStore() != null) {
logger.debug("Migrating legacy pre key store.");
- for (var entry : signalProtocolStore.getLegacyPreKeyStore().getPreKeys().entrySet()) {
+ for (var entry : legacySignalProtocolStore.getLegacyPreKeyStore().getPreKeys().entrySet()) {
try {
preKeyStore.storePreKey(entry.getKey(), new PreKeyRecord(entry.getValue()));
} catch (IOException e) {
}
}
}
- signalProtocolStore.setPreKeyStore(preKeyStore);
signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username));
- if (signalProtocolStore.getLegacySignedPreKeyStore() != null) {
+ if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacySignedPreKeyStore() != null) {
logger.debug("Migrating legacy signed pre key store.");
- for (var entry : signalProtocolStore.getLegacySignedPreKeyStore().getSignedPreKeys().entrySet()) {
+ for (var entry : legacySignalProtocolStore.getLegacySignedPreKeyStore().getSignedPreKeys().entrySet()) {
try {
signedPreKeyStore.storeSignedPreKey(entry.getKey(), new SignedPreKeyRecord(entry.getValue()));
} catch (IOException e) {
}
}
}
- signalProtocolStore.setSignedPreKeyStore(signedPreKeyStore);
sessionStore = new SessionStore(getSessionsPath(dataPath, username), recipientStore::resolveRecipient);
- if (signalProtocolStore.getLegacySessionStore() != null) {
+ if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacySessionStore() != null) {
logger.debug("Migrating legacy session store.");
- for (var session : signalProtocolStore.getLegacySessionStore().getSessions()) {
+ for (var session : legacySignalProtocolStore.getLegacySessionStore().getSessions()) {
try {
sessionStore.storeSession(new SignalProtocolAddress(session.address.getIdentifier(),
session.deviceId), new SessionRecord(session.sessionRecord));
}
}
}
- signalProtocolStore.setSessionStore(sessionStore);
+
+ if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyIdentityKeyStore() != null) {
+ identityKeyPair = legacySignalProtocolStore.getLegacyIdentityKeyStore().getIdentityKeyPair();
+ registrationId = legacySignalProtocolStore.getLegacyIdentityKeyStore().getLocalRegistrationId();
+ }
+ identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, username),
+ recipientStore::resolveRecipient,
+ identityKeyPair,
+ registrationId);
+ if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyIdentityKeyStore() != null) {
+ logger.debug("Migrating identity session store.");
+ for (var identity : legacySignalProtocolStore.getLegacyIdentityKeyStore().getIdentities()) {
+ RecipientId recipientId = recipientStore.resolveRecipient(identity.getAddress());
+ identityKeyStore.saveIdentity(recipientId, identity.getIdentityKey(), identity.getDateAdded());
+ identityKeyStore.setIdentityTrustLevel(recipientId,
+ identity.getIdentityKey(),
+ identity.getTrustLevel());
+ }
+ }
+
+ signalProtocolStore = new SignalProtocolStore(preKeyStore, signedPreKeyStore, sessionStore, identityKeyStore);
registered = Utils.getNotNullNode(rootNode, "registered").asBoolean();
var groupStoreNode = rootNode.get("groupStore");
.collect(Collectors.toSet());
}
}
-
- for (var identity : signalProtocolStore.getIdentities()) {
- identity.setAddress(recipientStore.resolveServiceAddress(identity.getAddress()));
- }
}
messageCache = new MessageCache(getMessageCachePath(dataPath, username));
.put("deviceId", deviceId)
.put("isMultiDevice", isMultiDevice)
.put("password", password)
+ .put("registrationId", identityKeyStore.getLocalRegistrationId())
+ .put("identityPrivateKey",
+ Base64.getEncoder()
+ .encodeToString(identityKeyStore.getIdentityKeyPair().getPrivateKey().serialize()))
+ .put("identityKey",
+ Base64.getEncoder()
+ .encodeToString(identityKeyStore.getIdentityKeyPair().getPublicKey().serialize()))
.put("registrationLockPin", registrationLockPin)
.put("pinMasterKey",
pinMasterKey == null ? null : Base64.getEncoder().encodeToString(pinMasterKey.serialize()))
.put("nextSignedPreKeyId", nextSignedPreKeyId)
.put("profileKey", Base64.getEncoder().encodeToString(profileKey.serialize()))
.put("registered", registered)
- .putPOJO("axolotlStore", signalProtocolStore)
.putPOJO("groupStore", groupStore)
.putPOJO("contactStore", contactStore)
.putPOJO("profileStore", profileStore)
return new Pair<>(fileChannel, lock);
}
- public void setResolver(final SignalServiceAddressResolver resolver) {
- signalProtocolStore.setResolver(resolver);
- }
-
public void addPreKeys(List<PreKeyRecord> records) {
for (var record : records) {
if (preKeyIdOffset != record.getId()) {
save();
}
- public JsonSignalProtocolStore getSignalProtocolStore() {
+ public SignalProtocolStore getSignalProtocolStore() {
return signalProtocolStore;
}
return sessionStore;
}
+ public IdentityKeyStore getIdentityKeyStore() {
+ return identityKeyStore;
+ }
+
public JsonGroupStore getGroupStore() {
return groupStore;
}
--- /dev/null
+package org.asamk.signal.manager.storage.identities;
+
+import org.asamk.signal.manager.TrustLevel;
+import org.asamk.signal.manager.storage.recipients.RecipientId;
+import org.whispersystems.libsignal.IdentityKey;
+
+import java.util.Date;
+
+public class IdentityInfo {
+
+ private final RecipientId recipientId;
+ private final IdentityKey identityKey;
+ private final TrustLevel trustLevel;
+ private final Date added;
+
+ IdentityInfo(
+ final RecipientId recipientId, IdentityKey identityKey, TrustLevel trustLevel, Date added
+ ) {
+ this.recipientId = recipientId;
+ this.identityKey = identityKey;
+ this.trustLevel = trustLevel;
+ this.added = added;
+ }
+
+ public RecipientId getRecipientId() {
+ return recipientId;
+ }
+
+ public IdentityKey getIdentityKey() {
+ return this.identityKey;
+ }
+
+ public TrustLevel getTrustLevel() {
+ return this.trustLevel;
+ }
+
+ boolean isTrusted() {
+ return trustLevel == TrustLevel.TRUSTED_UNVERIFIED || trustLevel == TrustLevel.TRUSTED_VERIFIED;
+ }
+
+ public Date getDateAdded() {
+ return this.added;
+ }
+
+ public byte[] getFingerprint() {
+ return identityKey.getPublicKey().serialize();
+ }
+}
--- /dev/null
+package org.asamk.signal.manager.storage.identities;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.asamk.signal.manager.TrustLevel;
+import org.asamk.signal.manager.storage.recipients.RecipientId;
+import org.asamk.signal.manager.storage.recipients.RecipientResolver;
+import org.asamk.signal.manager.util.IOUtils;
+import org.asamk.signal.manager.util.Utils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.whispersystems.libsignal.IdentityKey;
+import org.whispersystems.libsignal.IdentityKeyPair;
+import org.whispersystems.libsignal.InvalidKeyException;
+import org.whispersystems.libsignal.SignalProtocolAddress;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+public class IdentityKeyStore implements org.whispersystems.libsignal.state.IdentityKeyStore {
+
+ private final static Logger logger = LoggerFactory.getLogger(IdentityKeyStore.class);
+ private final ObjectMapper objectMapper = org.asamk.signal.manager.storage.Utils.createStorageObjectMapper();
+
+ private final Map<RecipientId, IdentityInfo> cachedIdentities = new HashMap<>();
+
+ private final File identitiesPath;
+
+ private final RecipientResolver resolver;
+ private final IdentityKeyPair identityKeyPair;
+ private final int localRegistrationId;
+
+ public IdentityKeyStore(
+ final File identitiesPath,
+ final RecipientResolver resolver,
+ final IdentityKeyPair identityKeyPair,
+ final int localRegistrationId
+ ) {
+ this.identitiesPath = identitiesPath;
+ this.resolver = resolver;
+ this.identityKeyPair = identityKeyPair;
+ this.localRegistrationId = localRegistrationId;
+ }
+
+ @Override
+ public IdentityKeyPair getIdentityKeyPair() {
+ return identityKeyPair;
+ }
+
+ @Override
+ public int getLocalRegistrationId() {
+ return localRegistrationId;
+ }
+
+ @Override
+ public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
+ final var recipientId = resolveRecipient(address.getName());
+
+ return saveIdentity(recipientId, identityKey, new Date());
+ }
+
+ public boolean saveIdentity(final RecipientId recipientId, final IdentityKey identityKey, Date added) {
+ synchronized (cachedIdentities) {
+ final var identityInfo = loadIdentityLocked(recipientId);
+ if (identityInfo != null && identityInfo.getIdentityKey().equals(identityKey)) {
+ // Identity already exists, not updating the trust level
+ return false;
+ }
+
+ final var trustLevel = identityInfo == null ? TrustLevel.TRUSTED_UNVERIFIED : TrustLevel.UNTRUSTED;
+ final var newIdentityInfo = new IdentityInfo(recipientId, identityKey, trustLevel, added);
+ storeIdentityLocked(recipientId, newIdentityInfo);
+ return true;
+ }
+ }
+
+ public boolean setIdentityTrustLevel(
+ RecipientId recipientId, IdentityKey identityKey, TrustLevel trustLevel
+ ) {
+ synchronized (cachedIdentities) {
+ final var identityInfo = loadIdentityLocked(recipientId);
+ if (identityInfo == null || !identityInfo.getIdentityKey().equals(identityKey)) {
+ // Identity not found, not updating the trust level
+ return false;
+ }
+
+ final var newIdentityInfo = new IdentityInfo(recipientId,
+ identityKey,
+ trustLevel,
+ identityInfo.getDateAdded());
+ storeIdentityLocked(recipientId, newIdentityInfo);
+ return true;
+ }
+ }
+
+ @Override
+ public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
+ var recipientId = resolveRecipient(address.getName());
+
+ synchronized (cachedIdentities) {
+ final var identityInfo = loadIdentityLocked(recipientId);
+ if (identityInfo == null) {
+ // Identity not found
+ return true;
+ }
+
+ // TODO implement possibility for different handling of incoming/outgoing trust decisions
+ if (!identityInfo.getIdentityKey().equals(identityKey)) {
+ // Identity found, but different
+ return false;
+ }
+
+ return identityInfo.isTrusted();
+ }
+ }
+
+ @Override
+ public IdentityKey getIdentity(SignalProtocolAddress address) {
+ var recipientId = resolveRecipient(address.getName());
+
+ synchronized (cachedIdentities) {
+ var identity = loadIdentityLocked(recipientId);
+ return identity == null ? null : identity.getIdentityKey();
+ }
+ }
+
+ public IdentityInfo getIdentity(RecipientId recipientId) {
+ synchronized (cachedIdentities) {
+ return loadIdentityLocked(recipientId);
+ }
+ }
+
+ final Pattern identityFileNamePattern = Pattern.compile("([0-9]+)");
+
+ public List<IdentityInfo> getIdentities() {
+ final var files = identitiesPath.listFiles();
+ if (files == null) {
+ return List.of();
+ }
+ return Arrays.stream(files)
+ .filter(f -> identityFileNamePattern.matcher(f.getName()).matches())
+ .map(f -> RecipientId.of(Integer.parseInt(f.getName())))
+ .map(this::loadIdentityLocked)
+ .collect(Collectors.toList());
+ }
+
+ public void mergeRecipients(final RecipientId recipientId, final RecipientId toBeMergedRecipientId) {
+ synchronized (cachedIdentities) {
+ deleteIdentityLocked(toBeMergedRecipientId);
+ }
+ }
+
+ /**
+ * @param identifier can be either a serialized uuid or a e164 phone number
+ */
+ private RecipientId resolveRecipient(String identifier) {
+ return resolver.resolveRecipient(Utils.getSignalServiceAddressFromIdentifier(identifier));
+ }
+
+ private File getIdentityFile(final RecipientId recipientId) {
+ try {
+ IOUtils.createPrivateDirectories(identitiesPath);
+ } catch (IOException e) {
+ throw new AssertionError("Failed to create identities path", e);
+ }
+ return new File(identitiesPath, String.valueOf(recipientId.getId()));
+ }
+
+ private IdentityInfo loadIdentityLocked(final RecipientId recipientId) {
+ {
+ final var session = cachedIdentities.get(recipientId);
+ if (session != null) {
+ return session;
+ }
+ }
+
+ final var file = getIdentityFile(recipientId);
+ if (!file.exists()) {
+ return null;
+ }
+ try (var inputStream = new FileInputStream(file)) {
+ var storage = objectMapper.readValue(inputStream, IdentityStorage.class);
+
+ var id = new IdentityKey(Base64.getDecoder().decode(storage.getIdentityKey()));
+ var trustLevel = TrustLevel.fromInt(storage.getTrustLevel());
+ var added = new Date(storage.getAddedTimestamp());
+
+ final var identityInfo = new IdentityInfo(recipientId, id, trustLevel, added);
+ cachedIdentities.put(recipientId, identityInfo);
+ return identityInfo;
+ } catch (IOException | InvalidKeyException e) {
+ logger.warn("Failed to load identity key: {}", e.getMessage());
+ return null;
+ }
+ }
+
+ private void storeIdentityLocked(final RecipientId recipientId, final IdentityInfo identityInfo) {
+ cachedIdentities.put(recipientId, identityInfo);
+
+ var storage = new IdentityStorage(Base64.getEncoder().encodeToString(identityInfo.getIdentityKey().serialize()),
+ identityInfo.getTrustLevel().ordinal(),
+ identityInfo.getDateAdded().getTime());
+
+ final var file = getIdentityFile(recipientId);
+ // Write to memory first to prevent corrupting the file in case of serialization errors
+ try (var inMemoryOutput = new ByteArrayOutputStream()) {
+ objectMapper.writeValue(inMemoryOutput, storage);
+
+ var input = new ByteArrayInputStream(inMemoryOutput.toByteArray());
+ try (var outputStream = new FileOutputStream(file)) {
+ input.transferTo(outputStream);
+ }
+ } catch (Exception e) {
+ logger.error("Error saving identity file: {}", e.getMessage());
+ }
+ }
+
+ private void deleteIdentityLocked(final RecipientId recipientId) {
+ cachedIdentities.remove(recipientId);
+
+ final var file = getIdentityFile(recipientId);
+ if (!file.exists()) {
+ return;
+ }
+ try {
+ Files.delete(file.toPath());
+ } catch (IOException e) {
+ logger.error("Failed to delete identity file {}: {}", file, e.getMessage());
+ }
+ }
+
+ private static final class IdentityStorage {
+
+ private String identityKey;
+ private int trustLevel;
+ private long addedTimestamp;
+
+ // For deserialization
+ private IdentityStorage() {
+ }
+
+ private IdentityStorage(final String identityKey, final int trustLevel, final long addedTimestamp) {
+ this.identityKey = identityKey;
+ this.trustLevel = trustLevel;
+ this.addedTimestamp = addedTimestamp;
+ }
+
+ public String getIdentityKey() {
+ return identityKey;
+ }
+
+ public int getTrustLevel() {
+ return trustLevel;
+ }
+
+ public long getAddedTimestamp() {
+ return addedTimestamp;
+ }
+ }
+}
+++ /dev/null
-package org.asamk.signal.manager.storage.protocol;
-
-import com.fasterxml.jackson.core.JsonGenerator;
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.databind.DeserializationContext;
-import com.fasterxml.jackson.databind.JsonDeserializer;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.JsonSerializer;
-import com.fasterxml.jackson.databind.SerializerProvider;
-
-import org.asamk.signal.manager.TrustLevel;
-import org.asamk.signal.manager.util.Utils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.whispersystems.libsignal.IdentityKey;
-import org.whispersystems.libsignal.IdentityKeyPair;
-import org.whispersystems.libsignal.InvalidKeyException;
-import org.whispersystems.libsignal.SignalProtocolAddress;
-import org.whispersystems.libsignal.state.IdentityKeyStore;
-import org.whispersystems.signalservice.api.push.SignalServiceAddress;
-import org.whispersystems.signalservice.api.util.UuidUtil;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Base64;
-import java.util.Date;
-import java.util.List;
-
-public class JsonIdentityKeyStore implements IdentityKeyStore {
-
- private final static Logger logger = LoggerFactory.getLogger(JsonIdentityKeyStore.class);
-
- private final List<IdentityInfo> identities = new ArrayList<>();
-
- private final IdentityKeyPair identityKeyPair;
- private final int localRegistrationId;
-
- private SignalServiceAddressResolver resolver;
-
- public JsonIdentityKeyStore(IdentityKeyPair identityKeyPair, int localRegistrationId) {
- this.identityKeyPair = identityKeyPair;
- this.localRegistrationId = localRegistrationId;
- }
-
- public void setResolver(final SignalServiceAddressResolver resolver) {
- this.resolver = resolver;
- }
-
- private SignalServiceAddress resolveSignalServiceAddress(String identifier) {
- if (resolver != null) {
- return resolver.resolveSignalServiceAddress(identifier);
- } else {
- return Utils.getSignalServiceAddressFromIdentifier(identifier);
- }
- }
-
- @Override
- public IdentityKeyPair getIdentityKeyPair() {
- return identityKeyPair;
- }
-
- @Override
- public int getLocalRegistrationId() {
- return localRegistrationId;
- }
-
- @Override
- public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
- return saveIdentity(resolveSignalServiceAddress(address.getName()),
- identityKey,
- TrustLevel.TRUSTED_UNVERIFIED,
- null);
- }
-
- /**
- * Adds the given identityKey for the user name and sets the trustLevel and added timestamp.
- * If the identityKey already exists, the trustLevel and added timestamp are NOT updated.
- *
- * @param serviceAddress User address, i.e. phone number and/or uuid
- * @param identityKey The user's public key
- * @param trustLevel Level of trust: untrusted, trusted, trusted and verified
- * @param added Added timestamp, if null and the key is newly added, the current time is used.
- */
- public boolean saveIdentity(
- SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel, Date added
- ) {
- for (var id : identities) {
- if (!id.address.matches(serviceAddress) || !id.identityKey.equals(identityKey)) {
- continue;
- }
-
- if (!id.address.getUuid().isPresent() || !id.address.getNumber().isPresent()) {
- id.address = serviceAddress;
- }
- // Identity already exists, not updating the trust level
- return true;
- }
-
- identities.add(new IdentityInfo(serviceAddress, identityKey, trustLevel, added != null ? added : new Date()));
- return false;
- }
-
- /**
- * Update trustLevel for the given identityKey for the user name.
- *
- * @param serviceAddress User address, i.e. phone number and/or uuid
- * @param identityKey The user's public key
- * @param trustLevel Level of trust: untrusted, trusted, trusted and verified
- */
- public void setIdentityTrustLevel(
- SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel
- ) {
- for (var id : identities) {
- if (!id.address.matches(serviceAddress) || !id.identityKey.equals(identityKey)) {
- continue;
- }
-
- if (!id.address.getUuid().isPresent() || !id.address.getNumber().isPresent()) {
- id.address = serviceAddress;
- }
- id.trustLevel = trustLevel;
- return;
- }
-
- identities.add(new IdentityInfo(serviceAddress, identityKey, trustLevel, new Date()));
- }
-
- public void removeIdentity(SignalServiceAddress serviceAddress, IdentityKey identityKey) {
- identities.removeIf(id -> id.address.matches(serviceAddress) && id.identityKey.equals(identityKey));
- }
-
- @Override
- public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
- // TODO implement possibility for different handling of incoming/outgoing trust decisions
- var serviceAddress = resolveSignalServiceAddress(address.getName());
- var trustOnFirstUse = true;
-
- for (var id : identities) {
- if (!id.address.matches(serviceAddress)) {
- continue;
- }
-
- if (id.identityKey.equals(identityKey)) {
- return id.isTrusted();
- } else {
- trustOnFirstUse = false;
- }
- }
-
- if (!trustOnFirstUse) {
- saveIdentity(resolveSignalServiceAddress(address.getName()), identityKey, TrustLevel.UNTRUSTED, null);
- }
-
- return trustOnFirstUse;
- }
-
- @Override
- public IdentityKey getIdentity(SignalProtocolAddress address) {
- var serviceAddress = resolveSignalServiceAddress(address.getName());
- var identity = getIdentity(serviceAddress);
- return identity == null ? null : identity.getIdentityKey();
- }
-
- public IdentityInfo getIdentity(SignalServiceAddress serviceAddress) {
- long maxDate = 0;
- IdentityInfo maxIdentity = null;
- for (var id : this.identities) {
- if (!id.address.matches(serviceAddress)) {
- continue;
- }
-
- final var time = id.getDateAdded().getTime();
- if (maxIdentity == null || maxDate <= time) {
- maxDate = time;
- maxIdentity = id;
- }
- }
- return maxIdentity;
- }
-
- public List<IdentityInfo> getIdentities() {
- // TODO deep copy
- return identities;
- }
-
- public List<IdentityInfo> getIdentities(SignalServiceAddress serviceAddress) {
- var identities = new ArrayList<IdentityInfo>();
- for (var identity : this.identities) {
- if (identity.address.matches(serviceAddress)) {
- identities.add(identity);
- }
- }
- return identities;
- }
-
- public static class JsonIdentityKeyStoreDeserializer extends JsonDeserializer<JsonIdentityKeyStore> {
-
- @Override
- public JsonIdentityKeyStore deserialize(
- JsonParser jsonParser, DeserializationContext deserializationContext
- ) throws IOException {
- JsonNode node = jsonParser.getCodec().readTree(jsonParser);
-
- var localRegistrationId = node.get("registrationId").asInt();
- var identityKeyPair = new IdentityKeyPair(Base64.getDecoder().decode(node.get("identityKey").asText()));
-
- var keyStore = new JsonIdentityKeyStore(identityKeyPair, localRegistrationId);
-
- var trustedKeysNode = node.get("trustedKeys");
- if (trustedKeysNode.isArray()) {
- for (var trustedKey : trustedKeysNode) {
- var trustedKeyName = trustedKey.hasNonNull("name") ? trustedKey.get("name").asText() : null;
-
- if (UuidUtil.isUuid(trustedKeyName)) {
- // Ignore identities that were incorrectly created with UUIDs as name
- continue;
- }
-
- var uuid = trustedKey.hasNonNull("uuid")
- ? UuidUtil.parseOrNull(trustedKey.get("uuid").asText())
- : null;
- final var serviceAddress = uuid == null
- ? Utils.getSignalServiceAddressFromIdentifier(trustedKeyName)
- : new SignalServiceAddress(uuid, trustedKeyName);
- try {
- var id = new IdentityKey(Base64.getDecoder().decode(trustedKey.get("identityKey").asText()), 0);
- var trustLevel = trustedKey.hasNonNull("trustLevel") ? TrustLevel.fromInt(trustedKey.get(
- "trustLevel").asInt()) : TrustLevel.TRUSTED_UNVERIFIED;
- var added = trustedKey.hasNonNull("addedTimestamp") ? new Date(trustedKey.get("addedTimestamp")
- .asLong()) : new Date();
- keyStore.saveIdentity(serviceAddress, id, trustLevel, added);
- } catch (InvalidKeyException e) {
- logger.warn("Error while decoding key for {}: {}", trustedKeyName, e.getMessage());
- }
- }
- }
-
- return keyStore;
- }
- }
-
- public static class JsonIdentityKeyStoreSerializer extends JsonSerializer<JsonIdentityKeyStore> {
-
- @Override
- public void serialize(
- JsonIdentityKeyStore jsonIdentityKeyStore, JsonGenerator json, SerializerProvider serializerProvider
- ) throws IOException {
- json.writeStartObject();
- json.writeNumberField("registrationId", jsonIdentityKeyStore.getLocalRegistrationId());
- json.writeStringField("identityKey",
- Base64.getEncoder().encodeToString(jsonIdentityKeyStore.getIdentityKeyPair().serialize()));
- json.writeStringField("identityPrivateKey",
- Base64.getEncoder()
- .encodeToString(jsonIdentityKeyStore.getIdentityKeyPair().getPrivateKey().serialize()));
- json.writeStringField("identityPublicKey",
- Base64.getEncoder()
- .encodeToString(jsonIdentityKeyStore.getIdentityKeyPair().getPublicKey().serialize()));
- json.writeArrayFieldStart("trustedKeys");
- for (var trustedKey : jsonIdentityKeyStore.identities) {
- json.writeStartObject();
- if (trustedKey.getAddress().getNumber().isPresent()) {
- json.writeStringField("name", trustedKey.getAddress().getNumber().get());
- }
- if (trustedKey.getAddress().getUuid().isPresent()) {
- json.writeStringField("uuid", trustedKey.getAddress().getUuid().get().toString());
- }
- json.writeStringField("identityKey",
- Base64.getEncoder().encodeToString(trustedKey.identityKey.serialize()));
- json.writeNumberField("trustLevel", trustedKey.trustLevel.ordinal());
- json.writeNumberField("addedTimestamp", trustedKey.added.getTime());
- json.writeEndObject();
- }
- json.writeEndArray();
- json.writeEndObject();
- }
- }
-}
import java.util.Date;
-public class IdentityInfo {
+public class LegacyIdentityInfo {
SignalServiceAddress address;
IdentityKey identityKey;
TrustLevel trustLevel;
Date added;
- IdentityInfo(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel, Date added) {
+ LegacyIdentityInfo(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel, Date added) {
this.address = address;
this.identityKey = identityKey;
this.trustLevel = trustLevel;
--- /dev/null
+package org.asamk.signal.manager.storage.protocol;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonNode;
+
+import org.asamk.signal.manager.TrustLevel;
+import org.asamk.signal.manager.util.Utils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.whispersystems.libsignal.IdentityKey;
+import org.whispersystems.libsignal.IdentityKeyPair;
+import org.whispersystems.libsignal.InvalidKeyException;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.whispersystems.signalservice.api.util.UuidUtil;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class LegacyJsonIdentityKeyStore {
+
+ private final static Logger logger = LoggerFactory.getLogger(LegacyJsonIdentityKeyStore.class);
+
+ private final List<LegacyIdentityInfo> identities;
+ private final IdentityKeyPair identityKeyPair;
+ private final int localRegistrationId;
+
+ private LegacyJsonIdentityKeyStore(
+ final List<LegacyIdentityInfo> identities, IdentityKeyPair identityKeyPair, int localRegistrationId
+ ) {
+ this.identities = identities;
+ this.identityKeyPair = identityKeyPair;
+ this.localRegistrationId = localRegistrationId;
+ }
+
+ public List<LegacyIdentityInfo> getIdentities() {
+ return identities.stream()
+ .map(LegacyIdentityInfo::getAddress)
+ .collect(Collectors.toSet())
+ .stream()
+ .map(this::getIdentity)
+ .collect(Collectors.toList());
+ }
+
+ public IdentityKeyPair getIdentityKeyPair() {
+ return identityKeyPair;
+ }
+
+ public int getLocalRegistrationId() {
+ return localRegistrationId;
+ }
+
+ private LegacyIdentityInfo getIdentity(SignalServiceAddress serviceAddress) {
+ long maxDate = 0;
+ LegacyIdentityInfo maxIdentity = null;
+ for (var id : this.identities) {
+ if (!id.address.matches(serviceAddress)) {
+ continue;
+ }
+
+ final var time = id.getDateAdded().getTime();
+ if (maxIdentity == null || maxDate <= time) {
+ maxDate = time;
+ maxIdentity = id;
+ }
+ }
+ return maxIdentity;
+ }
+
+ public static class JsonIdentityKeyStoreDeserializer extends JsonDeserializer<LegacyJsonIdentityKeyStore> {
+
+ @Override
+ public LegacyJsonIdentityKeyStore deserialize(
+ JsonParser jsonParser, DeserializationContext deserializationContext
+ ) throws IOException {
+ JsonNode node = jsonParser.getCodec().readTree(jsonParser);
+
+ var localRegistrationId = node.get("registrationId").asInt();
+ var identityKeyPair = new IdentityKeyPair(Base64.getDecoder().decode(node.get("identityKey").asText()));
+
+ var identities = new ArrayList<LegacyIdentityInfo>();
+
+ var trustedKeysNode = node.get("trustedKeys");
+ if (trustedKeysNode.isArray()) {
+ for (var trustedKey : trustedKeysNode) {
+ var trustedKeyName = trustedKey.hasNonNull("name") ? trustedKey.get("name").asText() : null;
+
+ if (UuidUtil.isUuid(trustedKeyName)) {
+ // Ignore identities that were incorrectly created with UUIDs as name
+ continue;
+ }
+
+ var uuid = trustedKey.hasNonNull("uuid")
+ ? UuidUtil.parseOrNull(trustedKey.get("uuid").asText())
+ : null;
+ final var serviceAddress = uuid == null
+ ? Utils.getSignalServiceAddressFromIdentifier(trustedKeyName)
+ : new SignalServiceAddress(uuid, trustedKeyName);
+ try {
+ var id = new IdentityKey(Base64.getDecoder().decode(trustedKey.get("identityKey").asText()), 0);
+ var trustLevel = trustedKey.hasNonNull("trustLevel") ? TrustLevel.fromInt(trustedKey.get(
+ "trustLevel").asInt()) : TrustLevel.TRUSTED_UNVERIFIED;
+ var added = trustedKey.hasNonNull("addedTimestamp") ? new Date(trustedKey.get("addedTimestamp")
+ .asLong()) : new Date();
+ identities.add(new LegacyIdentityInfo(serviceAddress, id, trustLevel, added));
+ } catch (InvalidKeyException e) {
+ logger.warn("Error while decoding key for {}: {}", trustedKeyName, e.getMessage());
+ }
+ }
+ }
+
+ return new LegacyJsonIdentityKeyStore(identities, identityKeyPair, localRegistrationId);
+ }
+ }
+}
public class LegacyJsonSessionStore {
- private final List<SessionInfo> sessions;
+ private final List<LegacySessionInfo> sessions;
- private LegacyJsonSessionStore(final List<SessionInfo> sessions) {
+ private LegacyJsonSessionStore(final List<LegacySessionInfo> sessions) {
this.sessions = sessions;
}
- public List<SessionInfo> getSessions() {
+ public List<LegacySessionInfo> getSessions() {
return sessions;
}
) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
- var sessions = new ArrayList<SessionInfo>();
+ var sessions = new ArrayList<LegacySessionInfo>();
if (node.isArray()) {
for (var session : node) {
: new SignalServiceAddress(uuid, sessionName);
final var deviceId = session.get("deviceId").asInt();
final var record = Base64.getDecoder().decode(session.get("record").asText());
- var sessionInfo = new SessionInfo(serviceAddress, deviceId, record);
+ var sessionInfo = new LegacySessionInfo(serviceAddress, deviceId, record);
sessions.add(sessionInfo);
}
}
--- /dev/null
+package org.asamk.signal.manager.storage.protocol;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+
+public class LegacyJsonSignalProtocolStore {
+
+ @JsonProperty("preKeys")
+ @JsonDeserialize(using = LegacyJsonPreKeyStore.JsonPreKeyStoreDeserializer.class)
+ private LegacyJsonPreKeyStore legacyPreKeyStore;
+
+ @JsonProperty("sessionStore")
+ @JsonDeserialize(using = LegacyJsonSessionStore.JsonSessionStoreDeserializer.class)
+ private LegacyJsonSessionStore legacySessionStore;
+
+ @JsonProperty("signedPreKeyStore")
+ @JsonDeserialize(using = LegacyJsonSignedPreKeyStore.JsonSignedPreKeyStoreDeserializer.class)
+ private LegacyJsonSignedPreKeyStore legacySignedPreKeyStore;
+
+ @JsonProperty("identityKeyStore")
+ @JsonDeserialize(using = LegacyJsonIdentityKeyStore.JsonIdentityKeyStoreDeserializer.class)
+ private LegacyJsonIdentityKeyStore legacyIdentityKeyStore;
+
+ private LegacyJsonSignalProtocolStore() {
+ }
+
+ public LegacyJsonPreKeyStore getLegacyPreKeyStore() {
+ return legacyPreKeyStore;
+ }
+
+ public LegacyJsonSignedPreKeyStore getLegacySignedPreKeyStore() {
+ return legacySignedPreKeyStore;
+ }
+
+ public LegacyJsonSessionStore getLegacySessionStore() {
+ return legacySessionStore;
+ }
+
+ public LegacyJsonIdentityKeyStore getLegacyIdentityKeyStore() {
+ return legacyIdentityKeyStore;
+ }
+}
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
-public class SessionInfo {
+public class LegacySessionInfo {
public SignalServiceAddress address;
public byte[] sessionRecord;
- public SessionInfo(final SignalServiceAddress address, final int deviceId, final byte[] sessionRecord) {
+ LegacySessionInfo(final SignalServiceAddress address, final int deviceId, final byte[] sessionRecord) {
this.address = address;
this.deviceId = deviceId;
this.sessionRecord = sessionRecord;
package org.asamk.signal.manager.storage.protocol;
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
-import com.fasterxml.jackson.databind.annotation.JsonSerialize;
-
-import org.asamk.signal.manager.TrustLevel;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyIdException;
import org.whispersystems.libsignal.SignalProtocolAddress;
+import org.whispersystems.libsignal.state.IdentityKeyStore;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.PreKeyStore;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SignedPreKeyStore;
import org.whispersystems.signalservice.api.SignalServiceProtocolStore;
import org.whispersystems.signalservice.api.SignalServiceSessionStore;
-import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.List;
-@JsonIgnoreProperties(value = {"sessionStore", "preKeys", "signedPreKeyStore"}, allowSetters = true)
-public class JsonSignalProtocolStore implements SignalServiceProtocolStore {
-
- @JsonProperty("preKeys")
- @JsonDeserialize(using = LegacyJsonPreKeyStore.JsonPreKeyStoreDeserializer.class)
- private LegacyJsonPreKeyStore legacyPreKeyStore;
-
- @JsonProperty("sessionStore")
- @JsonDeserialize(using = LegacyJsonSessionStore.JsonSessionStoreDeserializer.class)
- private LegacyJsonSessionStore legacySessionStore;
-
- @JsonProperty("signedPreKeyStore")
- @JsonDeserialize(using = LegacyJsonSignedPreKeyStore.JsonSignedPreKeyStoreDeserializer.class)
- private LegacyJsonSignedPreKeyStore legacySignedPreKeyStore;
-
- @JsonProperty("identityKeyStore")
- @JsonDeserialize(using = JsonIdentityKeyStore.JsonIdentityKeyStoreDeserializer.class)
- @JsonSerialize(using = JsonIdentityKeyStore.JsonIdentityKeyStoreSerializer.class)
- private JsonIdentityKeyStore identityKeyStore;
+public class SignalProtocolStore implements SignalServiceProtocolStore {
- private PreKeyStore preKeyStore;
- private SignedPreKeyStore signedPreKeyStore;
- private SignalServiceSessionStore sessionStore;
+ private final PreKeyStore preKeyStore;
+ private final SignedPreKeyStore signedPreKeyStore;
+ private final SignalServiceSessionStore sessionStore;
+ private final IdentityKeyStore identityKeyStore;
- public JsonSignalProtocolStore() {
- }
-
- public JsonSignalProtocolStore(
- IdentityKeyPair identityKeyPair,
- int registrationId,
- PreKeyStore preKeyStore,
- SignedPreKeyStore signedPreKeyStore,
- SignalServiceSessionStore sessionStore
+ public SignalProtocolStore(
+ final PreKeyStore preKeyStore,
+ final SignedPreKeyStore signedPreKeyStore,
+ final SignalServiceSessionStore sessionStore,
+ final IdentityKeyStore identityKeyStore
) {
this.preKeyStore = preKeyStore;
this.signedPreKeyStore = signedPreKeyStore;
this.sessionStore = sessionStore;
- this.identityKeyStore = new JsonIdentityKeyStore(identityKeyPair, registrationId);
- }
-
- public void setResolver(final SignalServiceAddressResolver resolver) {
- identityKeyStore.setResolver(resolver);
- }
-
- public void setPreKeyStore(final PreKeyStore preKeyStore) {
- this.preKeyStore = preKeyStore;
- }
-
- public void setSignedPreKeyStore(final SignedPreKeyStore signedPreKeyStore) {
- this.signedPreKeyStore = signedPreKeyStore;
- }
-
- public void setSessionStore(final SignalServiceSessionStore sessionStore) {
- this.sessionStore = sessionStore;
- }
-
- public LegacyJsonPreKeyStore getLegacyPreKeyStore() {
- return legacyPreKeyStore;
- }
-
- public LegacyJsonSignedPreKeyStore getLegacySignedPreKeyStore() {
- return legacySignedPreKeyStore;
- }
-
- public LegacyJsonSessionStore getLegacySessionStore() {
- return legacySessionStore;
+ this.identityKeyStore = identityKeyStore;
}
@Override
return identityKeyStore.saveIdentity(address, identityKey);
}
- public void saveIdentity(SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel) {
- identityKeyStore.saveIdentity(serviceAddress, identityKey, trustLevel, null);
- }
-
- public void setIdentityTrustLevel(
- SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel
- ) {
- identityKeyStore.setIdentityTrustLevel(serviceAddress, identityKey, trustLevel);
- }
-
- public void removeIdentity(SignalServiceAddress serviceAddress, IdentityKey identityKey) {
- identityKeyStore.removeIdentity(serviceAddress, identityKey);
- }
-
- public List<IdentityInfo> getIdentities() {
- return identityKeyStore.getIdentities();
- }
-
- public List<IdentityInfo> getIdentities(SignalServiceAddress serviceAddress) {
- return identityKeyStore.getIdentities(serviceAddress);
- }
-
@Override
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
return identityKeyStore.isTrustedIdentity(address, identityKey, direction);
return identityKeyStore.getIdentity(address);
}
- public IdentityInfo getIdentity(SignalServiceAddress serviceAddress) {
- return identityKeyStore.getIdentity(serviceAddress);
- }
-
@Override
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
return preKeyStore.loadPreKey(preKeyId);
+++ /dev/null
-package org.asamk.signal.manager.storage.protocol;
-
-import org.whispersystems.signalservice.api.push.SignalServiceAddress;
-
-public interface SignalServiceAddressResolver {
-
- /**
- * Get a SignalServiceAddress with number and/or uuid from an identifier name.
- *
- * @param identifier can be either a serialized uuid or a e164 phone number
- */
- SignalServiceAddress resolveSignalServiceAddress(String identifier);
-}
}
private void save() {
+ var storage = new Storage(recipients.entrySet()
+ .stream()
+ .map(pair -> new Storage.Recipient(pair.getKey().getId(),
+ pair.getValue().getNumber().orNull(),
+ pair.getValue().getUuid().transform(UUID::toString).orNull()))
+ .collect(Collectors.toList()), lastId);
+
// Write to memory first to prevent corrupting the file in case of serialization errors
try (var inMemoryOutput = new ByteArrayOutputStream()) {
- var storage = new Storage(recipients.entrySet()
- .stream()
- .map(pair -> new Storage.Recipient(pair.getKey().getId(),
- pair.getValue().getNumber().orNull(),
- pair.getValue().getUuid().transform(UUID::toString).orNull()))
- .collect(Collectors.toList()), lastId);
objectMapper.writeValue(inMemoryOutput, storage);
var input = new ByteArrayInputStream(inMemoryOutput.toByteArray());
.collect(Collectors.toList());
}
- private File getSessionPath(Key key) {
+ private File getSessionFile(Key key) {
try {
IOUtils.createPrivateDirectories(sessionsPath);
} catch (IOException e) {
}
}
- final var file = getSessionPath(key);
+ final var file = getSessionFile(key);
if (!file.exists()) {
return null;
}
private void storeSessionLocked(final Key key, final SessionRecord session) {
cachedSessions.put(key, session);
- final var file = getSessionPath(key);
+ final var file = getSessionFile(key);
try {
try (var outputStream = new FileOutputStream(file)) {
outputStream.write(session.serialize());
private void deleteSessionLocked(final Key key) {
cachedSessions.remove(key);
- final var file = getSessionPath(key);
+ final var file = getSessionFile(key);
if (!file.exists()) {
return;
}
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.Curve;
+import org.whispersystems.libsignal.ecc.ECPrivateKey;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.Medium;
private KeyUtils() {
}
+ public static IdentityKeyPair getIdentityKeyPair(byte[] publicKeyBytes, byte[] privateKeyBytes) {
+ try {
+ IdentityKey publicKey = new IdentityKey(publicKeyBytes);
+ ECPrivateKey privateKey = Curve.decodePrivatePoint(privateKeyBytes);
+
+ return new IdentityKeyPair(publicKey, privateKey);
+ } catch (InvalidKeyException e) {
+ throw new AssertionError(e);
+ }
+ }
+
public static IdentityKeyPair generateIdentityKeyPair() {
var djbKeyPair = Curve.generateKeyPair();
var djbIdentityKey = new IdentityKey(djbKeyPair.getPublicKey());
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.Manager;
-import org.asamk.signal.manager.storage.protocol.IdentityInfo;
+import org.asamk.signal.manager.storage.identities.IdentityInfo;
import org.asamk.signal.util.Hex;
import org.asamk.signal.util.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.util.List;
private final static Logger logger = LoggerFactory.getLogger(ListIdentitiesCommand.class);
private static void printIdentityFingerprint(PlainTextWriter writer, Manager m, IdentityInfo theirId) {
- var digits = Util.formatSafetyNumber(m.computeSafetyNumber(theirId.getAddress(), theirId.getIdentityKey()));
+ final SignalServiceAddress address = m.resolveSignalServiceAddress(theirId.getRecipientId());
+ var digits = Util.formatSafetyNumber(m.computeSafetyNumber(address, theirId.getIdentityKey()));
writer.println("{}: {} Added: {} Fingerprint: {} Safety Number: {}",
- theirId.getAddress().getNumber().orNull(),
+ address.getNumber().orNull(),
theirId.getTrustLevel(),
theirId.getDateAdded(),
Hex.toString(theirId.getFingerprint()),
public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
var number = ns.getString("number");
if (ns.getBoolean("trust_all_known_keys")) {
- var res = m.trustIdentityAllKeys(number);
+ boolean res;
+ try {
+ res = m.trustIdentityAllKeys(number);
+ } catch (InvalidNumberException e) {
+ throw new UserErrorException("Failed to parse recipient: " + e.getMessage());
+ }
if (!res) {
throw new UserErrorException("Failed to set the trust for this number, make sure the number is correct.");
}
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
import org.asamk.signal.manager.groups.GroupNotFoundException;
import org.asamk.signal.manager.groups.NotAGroupMemberException;
+import org.asamk.signal.manager.storage.identities.IdentityInfo;
import org.asamk.signal.util.ErrorUtils;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.whispersystems.libsignal.util.guava.Optional;
@Override
public long sendMessageReaction(
- final String emoji, final boolean remove, final String targetAuthor, final long targetSentTimestamp, final String recipient
+ final String emoji,
+ final boolean remove,
+ final String targetAuthor,
+ final long targetSentTimestamp,
+ final String recipient
) {
var recipients = new ArrayList<String>(1);
recipients.add(recipient);
@Override
public long sendMessageReaction(
- final String emoji, final boolean remove, final String targetAuthor, final long targetSentTimestamp, final List<String> recipients
+ final String emoji,
+ final boolean remove,
+ final String targetAuthor,
+ final long targetSentTimestamp,
+ final List<String> recipients
) {
try {
final var results = m.sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipients);
@Override
public long sendGroupMessageReaction(
- final String emoji, final boolean remove, final String targetAuthor, final long targetSentTimestamp, final byte[] groupId
+ final String emoji,
+ final boolean remove,
+ final String targetAuthor,
+ final long targetSentTimestamp,
+ final byte[] groupId
) {
try {
- final var results = m.sendGroupMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, GroupId.unknownVersion(groupId));
+ final var results = m.sendGroupMessageReaction(emoji,
+ remove,
+ targetAuthor,
+ targetSentTimestamp,
+ GroupId.unknownVersion(groupId));
checkSendMessageResults(results.first(), results.second());
return results.first();
} catch (IOException e) {
// all numbers the system knows
@Override
public List<String> listNumbers() {
- return Stream.concat(m.getIdentities().stream().map(i -> i.getAddress().getNumber().orNull()),
- m.getContacts().stream().map(c -> c.number))
+ return Stream.concat(m.getIdentities()
+ .stream()
+ .map(IdentityInfo::getRecipientId)
+ .map(m::resolveSignalServiceAddress)
+ .map(a -> a.getNumber().orNull()), m.getContacts().stream().map(c -> c.number))
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
}
// Try profiles if no contact name was found
for (var identity : m.getIdentities()) {
- final var address = identity.getAddress();
+ final var recipientId = identity.getRecipientId();
+ final var address = m.resolveSignalServiceAddress(recipientId);
var number = address.getNumber().orNull();
if (number != null) {
var profile = m.getRecipientProfile(address);