From: AsamK Date: Sun, 18 Apr 2021 16:26:12 +0000 (+0200) Subject: Refactor identity key store X-Git-Tag: v0.8.2~44 X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/8a0c6cae157bf92f752fe9b6cb76048dde989348 Refactor identity key store --- diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index c1189df3..a8eaa9ab 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -34,9 +34,10 @@ import org.asamk.signal.manager.storage.contacts.ContactInfo; 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; @@ -234,8 +235,6 @@ public class Manager implements Closeable { clientZkProfileOperations, ServiceConfig.AUTOMATIC_NETWORK_RETRY); - this.account.setResolver(this::resolveSignalServiceAddress); - this.unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey, account.getProfileStore()::getProfileKey, this::getRecipientProfile, @@ -1223,17 +1222,7 @@ public class Manager implements Closeable { 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 getSignalServiceAddresses(Collection numbers) throws InvalidNumberException { @@ -1303,22 +1292,8 @@ public class Manager implements Closeable { 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 { @@ -1388,12 +1363,6 @@ public class Manager implements Closeable { 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()); } } @@ -1406,12 +1375,6 @@ public class Manager implements Closeable { 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()); } } @@ -1424,15 +1387,7 @@ public class Manager implements Closeable { 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); } @@ -2004,8 +1959,8 @@ public class Manager implements Closeable { } 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())); } @@ -2040,8 +1995,8 @@ public class Manager implements Closeable { } 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())); } @@ -2283,7 +2238,8 @@ public class Manager implements Closeable { 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(), @@ -2395,11 +2351,12 @@ public class Manager implements Closeable { } public List getIdentities() { - return account.getSignalProtocolStore().getIdentities(); + return account.getIdentityKeyStore().getIdentities(); } public List 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); } /** @@ -2409,8 +2366,10 @@ public class Manager implements Closeable { * @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); } /** @@ -2420,72 +2379,43 @@ public class Manager implements Closeable { * @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 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 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; } @@ -2499,6 +2429,7 @@ public class Manager implements Closeable { theirIdentityKey); } + @Deprecated public SignalServiceAddress canonicalizeAndResolveSignalServiceAddress(String identifier) throws InvalidNumberException { var canonicalizedNumber = UuidUtil.isUuid(identifier) ? identifier @@ -2506,12 +2437,14 @@ public class Manager implements Closeable { 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(); @@ -2520,6 +2453,27 @@ public class Manager implements Closeable { 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); diff --git a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java index a16ead37..9c1eecce 100644 --- a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java @@ -40,6 +40,7 @@ import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider 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 { @@ -164,10 +165,10 @@ 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 { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index 298dd1ed..b1b888cc 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -14,12 +14,13 @@ import org.asamk.signal.manager.storage.contacts.ContactInfo; 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; @@ -81,10 +82,11 @@ public class SignalAccount implements Closeable { 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; @@ -141,11 +143,14 @@ public class SignalAccount implements Closeable { 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(); @@ -190,11 +195,14 @@ public class SignalAccount implements Closeable { 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(); @@ -235,6 +243,7 @@ public class SignalAccount implements Closeable { private void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) { sessionStore.mergeRecipients(recipientId, toBeMergedRecipientId); + identityKeyStore.mergeRecipients(recipientId, toBeMergedRecipientId); } public static File getFileName(File dataPath, String username) { @@ -261,6 +270,10 @@ public class SignalAccount implements Closeable { 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"); } @@ -299,6 +312,17 @@ public class SignalAccount implements Closeable { } 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(); } @@ -338,13 +362,15 @@ public class SignalAccount implements Closeable { } } - 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) { @@ -352,12 +378,11 @@ public class SignalAccount implements Closeable { } } } - 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) { @@ -365,12 +390,11 @@ public class SignalAccount implements Closeable { } } } - 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)); @@ -379,7 +403,27 @@ public class SignalAccount implements Closeable { } } } - 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"); @@ -431,10 +475,6 @@ public class SignalAccount implements Closeable { .collect(Collectors.toSet()); } } - - for (var identity : signalProtocolStore.getIdentities()) { - identity.setAddress(recipientStore.resolveServiceAddress(identity.getAddress())); - } } messageCache = new MessageCache(getMessageCachePath(dataPath, username)); @@ -475,6 +515,13 @@ public class SignalAccount implements Closeable { .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())) @@ -484,7 +531,6 @@ public class SignalAccount implements Closeable { .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) @@ -517,10 +563,6 @@ public class SignalAccount implements Closeable { return new Pair<>(fileChannel, lock); } - public void setResolver(final SignalServiceAddressResolver resolver) { - signalProtocolStore.setResolver(resolver); - } - public void addPreKeys(List records) { for (var record : records) { if (preKeyIdOffset != record.getId()) { @@ -543,7 +585,7 @@ public class SignalAccount implements Closeable { save(); } - public JsonSignalProtocolStore getSignalProtocolStore() { + public SignalProtocolStore getSignalProtocolStore() { return signalProtocolStore; } @@ -551,6 +593,10 @@ public class SignalAccount implements Closeable { return sessionStore; } + public IdentityKeyStore getIdentityKeyStore() { + return identityKeyStore; + } + public JsonGroupStore getGroupStore() { return groupStore; } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityInfo.java b/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityInfo.java new file mode 100644 index 00000000..31b2a85c --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityInfo.java @@ -0,0 +1,48 @@ +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(); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java new file mode 100644 index 00000000..d1cfceda --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java @@ -0,0 +1,273 @@ +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 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 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; + } + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/JsonIdentityKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/JsonIdentityKeyStore.java deleted file mode 100644 index 561138c6..00000000 --- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/JsonIdentityKeyStore.java +++ /dev/null @@ -1,277 +0,0 @@ -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 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 getIdentities() { - // TODO deep copy - return identities; - } - - public List getIdentities(SignalServiceAddress serviceAddress) { - var identities = new ArrayList(); - for (var identity : this.identities) { - if (identity.address.matches(serviceAddress)) { - identities.add(identity); - } - } - return identities; - } - - public static class JsonIdentityKeyStoreDeserializer extends JsonDeserializer { - - @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 { - - @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(); - } - } -} diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/IdentityInfo.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyIdentityInfo.java similarity index 88% rename from lib/src/main/java/org/asamk/signal/manager/storage/protocol/IdentityInfo.java rename to lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyIdentityInfo.java index 652bf524..eb66b3e5 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/IdentityInfo.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyIdentityInfo.java @@ -6,14 +6,14 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress; 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; diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonIdentityKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonIdentityKeyStore.java new file mode 100644 index 00000000..781b6f96 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonIdentityKeyStore.java @@ -0,0 +1,120 @@ +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 identities; + private final IdentityKeyPair identityKeyPair; + private final int localRegistrationId; + + private LegacyJsonIdentityKeyStore( + final List identities, IdentityKeyPair identityKeyPair, int localRegistrationId + ) { + this.identities = identities; + this.identityKeyPair = identityKeyPair; + this.localRegistrationId = localRegistrationId; + } + + public List 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 { + + @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(); + + 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); + } + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSessionStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSessionStore.java index 9a14ab60..5f301aeb 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSessionStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSessionStore.java @@ -16,13 +16,13 @@ import java.util.List; public class LegacyJsonSessionStore { - private final List sessions; + private final List sessions; - private LegacyJsonSessionStore(final List sessions) { + private LegacyJsonSessionStore(final List sessions) { this.sessions = sessions; } - public List getSessions() { + public List getSessions() { return sessions; } @@ -34,7 +34,7 @@ public class LegacyJsonSessionStore { ) throws IOException { JsonNode node = jsonParser.getCodec().readTree(jsonParser); - var sessions = new ArrayList(); + var sessions = new ArrayList(); if (node.isArray()) { for (var session : node) { @@ -50,7 +50,7 @@ public class LegacyJsonSessionStore { : 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); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSignalProtocolStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSignalProtocolStore.java new file mode 100644 index 00000000..9cc4c6c2 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSignalProtocolStore.java @@ -0,0 +1,42 @@ +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; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SessionInfo.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacySessionInfo.java similarity index 70% rename from lib/src/main/java/org/asamk/signal/manager/storage/protocol/SessionInfo.java rename to lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacySessionInfo.java index 802b896b..a19bbd86 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SessionInfo.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacySessionInfo.java @@ -2,7 +2,7 @@ package org.asamk.signal.manager.storage.protocol; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -public class SessionInfo { +public class LegacySessionInfo { public SignalServiceAddress address; @@ -10,7 +10,7 @@ public class SessionInfo { 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; diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/JsonSignalProtocolStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalProtocolStore.java similarity index 51% rename from lib/src/main/java/org/asamk/signal/manager/storage/protocol/JsonSignalProtocolStore.java rename to lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalProtocolStore.java index 91318084..1872e356 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/JsonSignalProtocolStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalProtocolStore.java @@ -1,15 +1,10 @@ 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; @@ -17,76 +12,26 @@ import org.whispersystems.libsignal.state.SignedPreKeyRecord; 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 @@ -104,28 +49,6 @@ public class JsonSignalProtocolStore implements SignalServiceProtocolStore { 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 getIdentities() { - return identityKeyStore.getIdentities(); - } - - public List getIdentities(SignalServiceAddress serviceAddress) { - return identityKeyStore.getIdentities(serviceAddress); - } - @Override public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) { return identityKeyStore.isTrustedIdentity(address, identityKey, direction); @@ -136,10 +59,6 @@ public class JsonSignalProtocolStore implements SignalServiceProtocolStore { 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); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalServiceAddressResolver.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalServiceAddressResolver.java deleted file mode 100644 index 86eea05e..00000000 --- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalServiceAddressResolver.java +++ /dev/null @@ -1,13 +0,0 @@ -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); -} diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java index 533808d8..7235b662 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java @@ -229,14 +229,15 @@ public class RecipientStore { } 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()); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java index 0773af9a..30df6900 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java @@ -199,7 +199,7 @@ public class SessionStore implements SignalServiceSessionStore { .collect(Collectors.toList()); } - private File getSessionPath(Key key) { + private File getSessionFile(Key key) { try { IOUtils.createPrivateDirectories(sessionsPath); } catch (IOException e) { @@ -216,7 +216,7 @@ public class SessionStore implements SignalServiceSessionStore { } } - final var file = getSessionPath(key); + final var file = getSessionFile(key); if (!file.exists()) { return null; } @@ -233,7 +233,7 @@ public class SessionStore implements SignalServiceSessionStore { 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()); @@ -263,7 +263,7 @@ public class SessionStore implements SignalServiceSessionStore { private void deleteSessionLocked(final Key key) { cachedSessions.remove(key); - final var file = getSessionPath(key); + final var file = getSessionFile(key); if (!file.exists()) { return; } diff --git a/lib/src/main/java/org/asamk/signal/manager/util/KeyUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/KeyUtils.java index 5167331a..03bf8d79 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/KeyUtils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/KeyUtils.java @@ -6,6 +6,7 @@ import org.whispersystems.libsignal.IdentityKey; 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; @@ -23,6 +24,17 @@ public class KeyUtils { 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()); diff --git a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java index dc2d92fb..78bd6576 100644 --- a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java @@ -8,11 +8,12 @@ import org.asamk.signal.PlainTextWriterImpl; 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; @@ -22,9 +23,10 @@ public class ListIdentitiesCommand implements LocalCommand { 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()), diff --git a/src/main/java/org/asamk/signal/commands/TrustCommand.java b/src/main/java/org/asamk/signal/commands/TrustCommand.java index 08fe6a41..00371b59 100644 --- a/src/main/java/org/asamk/signal/commands/TrustCommand.java +++ b/src/main/java/org/asamk/signal/commands/TrustCommand.java @@ -29,7 +29,12 @@ public class TrustCommand implements LocalCommand { 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."); } diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 6b22029b..f50363cb 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -8,6 +8,7 @@ import org.asamk.signal.manager.groups.GroupId; 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; @@ -144,7 +145,11 @@ public class DbusSignalImpl implements Signal { @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(1); recipients.add(recipient); @@ -153,7 +158,11 @@ public class DbusSignalImpl implements Signal { @Override public long sendMessageReaction( - final String emoji, final boolean remove, final String targetAuthor, final long targetSentTimestamp, final List recipients + final String emoji, + final boolean remove, + final String targetAuthor, + final long targetSentTimestamp, + final List recipients ) { try { final var results = m.sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipients); @@ -210,10 +219,18 @@ public class DbusSignalImpl implements Signal { @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) { @@ -366,8 +383,11 @@ public class DbusSignalImpl implements Signal { // all numbers the system knows @Override public List 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()); @@ -385,7 +405,8 @@ public class DbusSignalImpl implements Signal { } // 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);