From: AsamK Date: Sun, 29 Mar 2020 10:51:32 +0000 (+0200) Subject: Store uuids in identity and session store X-Git-Tag: v0.6.6~3 X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/7e5aec6e15ac104a62a375754bf4d2d21f55ee3a Store uuids in identity and session store --- diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 1460aa61..baf456bd 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -158,7 +158,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { System.out.println("Received sync message with verified identities:"); final VerifiedMessage verifiedMessage = syncMessage.getVerified().get(); System.out.println(" - " + verifiedMessage.getDestination() + ": " + verifiedMessage.getVerified()); - String safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(verifiedMessage.getDestination().getNumber().get(), verifiedMessage.getIdentityKey())); + String safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey())); System.out.println(" " + safetyNumber); } if (syncMessage.getConfiguration().isPresent()) { diff --git a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java index a7cf8705..529c7c30 100644 --- a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java @@ -7,17 +7,15 @@ import org.asamk.signal.manager.Manager; import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; import org.asamk.signal.util.Hex; import org.asamk.signal.util.Util; -import org.whispersystems.libsignal.util.Pair; import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.util.List; -import java.util.Map; public class ListIdentitiesCommand implements LocalCommand { - private static void printIdentityFingerprint(Manager m, String theirUsername, JsonIdentityKeyStore.Identity theirId) { - String digits = Util.formatSafetyNumber(m.computeSafetyNumber(theirUsername, theirId.getIdentityKey())); - System.out.println(String.format("%s: %s Added: %s Fingerprint: %s Safety Number: %s", theirUsername, + private static void printIdentityFingerprint(Manager m, JsonIdentityKeyStore.Identity theirId) { + String digits = Util.formatSafetyNumber(m.computeSafetyNumber(theirId.getAddress(), theirId.getIdentityKey())); + System.out.println(String.format("%s: %s Added: %s Fingerprint: %s Safety Number: %s", theirId.getAddress().getNumber().orNull(), theirId.getTrustLevel(), theirId.getDateAdded(), Hex.toString(theirId.getFingerprint()), digits)); } @@ -34,17 +32,15 @@ public class ListIdentitiesCommand implements LocalCommand { return 1; } if (ns.get("number") == null) { - for (Map.Entry> keys : m.getIdentities().entrySet()) { - for (JsonIdentityKeyStore.Identity id : keys.getValue()) { - printIdentityFingerprint(m, keys.getKey(), id); - } + for (JsonIdentityKeyStore.Identity identity : m.getIdentities()) { + printIdentityFingerprint(m, identity); } } else { String number = ns.getString("number"); try { - Pair> key = m.getIdentities(number); - for (JsonIdentityKeyStore.Identity id : key.second()) { - printIdentityFingerprint(m, key.first(), id); + List identities = m.getIdentities(number); + for (JsonIdentityKeyStore.Identity id : identities) { + printIdentityFingerprint(m, id); } } catch (InvalidNumberException e) { System.out.println("Invalid number: " + e.getMessage()); diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index ffb30195..eb1327ac 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -9,7 +9,6 @@ import org.asamk.signal.GroupNotFoundException; import org.asamk.signal.NotAGroupMemberException; import org.asamk.signal.manager.Manager; import org.asamk.signal.util.Util; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.signalservice.api.util.InvalidNumberException; @@ -63,7 +62,7 @@ public class SendReactionCommand implements LocalCommand { String emoji = ns.getString("emoji"); boolean isRemove = ns.getBoolean("remove"); - SignalServiceAddress targetAuthor = new SignalServiceAddress(null, ns.getString("target_author")); + String targetAuthor = ns.getString("target_author"); long targetTimestamp = ns.getLong("target_timestamp"); try { diff --git a/src/main/java/org/asamk/signal/commands/TrustCommand.java b/src/main/java/org/asamk/signal/commands/TrustCommand.java index f2744545..a507e265 100644 --- a/src/main/java/org/asamk/signal/commands/TrustCommand.java +++ b/src/main/java/org/asamk/signal/commands/TrustCommand.java @@ -6,7 +6,9 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.manager.Manager; +import org.asamk.signal.util.ErrorUtils; import org.asamk.signal.util.Hex; +import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.util.Locale; @@ -50,13 +52,25 @@ public class TrustCommand implements LocalCommand { System.err.println("Failed to parse the fingerprint, make sure the fingerprint is a correctly encoded hex string without additional characters."); return 1; } - boolean res = m.trustIdentityVerified(number, fingerprintBytes); + boolean res; + try { + res = m.trustIdentityVerified(number, fingerprintBytes); + } catch (InvalidNumberException e) { + ErrorUtils.handleInvalidNumberException(e); + return 1; + } if (!res) { System.err.println("Failed to set the trust for the fingerprint of this number, make sure the number and the fingerprint are correct."); return 1; } } else if (fingerprint.length() == 60) { - boolean res = m.trustIdentityVerifiedSafetyNumber(number, fingerprint); + boolean res; + try { + res = m.trustIdentityVerifiedSafetyNumber(number, fingerprint); + } catch (InvalidNumberException e) { + ErrorUtils.handleInvalidNumberException(e); + return 1; + } if (!res) { System.err.println("Failed to set the trust for the safety number of this phone number, make sure the phone number and the safety number are correct."); return 1; diff --git a/src/main/java/org/asamk/signal/manager/BaseConfig.java b/src/main/java/org/asamk/signal/manager/BaseConfig.java index edb6c201..1461d99e 100644 --- a/src/main/java/org/asamk/signal/manager/BaseConfig.java +++ b/src/main/java/org/asamk/signal/manager/BaseConfig.java @@ -1,5 +1,6 @@ package org.asamk.signal.manager; +import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl; @@ -49,6 +50,8 @@ public class BaseConfig { zkGroupServerPublicParams ); + static final SignalServiceProfile.Capabilities capabilities = new SignalServiceProfile.Capabilities(false, false); + private BaseConfig() { } } diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index b418d310..dcc01d94 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -106,6 +106,7 @@ import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.util.StreamDetails; import org.whispersystems.signalservice.api.util.UptimeSleepTimer; +import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException; import org.whispersystems.signalservice.internal.util.Hex; @@ -133,7 +134,6 @@ import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.UUID; @@ -144,8 +144,6 @@ import java.util.zip.ZipFile; public class Manager implements Signal { - private static final SignalServiceProfile.Capabilities capabilities = new SignalServiceProfile.Capabilities(false, false); - private final String settingsPath; private final String dataPath; private final String attachmentsPath; @@ -192,6 +190,10 @@ public class Manager implements Signal { } private String getMessageCachePath(String sender) { + if (sender == null || sender.isEmpty()) { + return getMessageCachePath(); + } + return getMessageCachePath() + "/" + sender.replace("/", "_"); } @@ -210,6 +212,7 @@ public class Manager implements Signal { return; } account = SignalAccount.load(dataPath, username); + account.setResolver(this::resolveSignalServiceAddress); migrateLegacyConfigs(); @@ -257,11 +260,11 @@ public class Manager implements Signal { int registrationId = KeyHelper.generateRegistrationId(false); if (username == null) { account = SignalAccount.createTemporaryAccount(identityKey, registrationId); + account.setResolver(this::resolveSignalServiceAddress); } else { - account.getSignalProtocolStore().saveIdentity(username, identityKey.getPublicKey(), TrustLevel.TRUSTED_VERIFIED); - ProfileKey profileKey = KeyUtils.createProfileKey(); account = SignalAccount.create(dataPath, username, identityKey, registrationId, profileKey); + account.setResolver(this::resolveSignalServiceAddress); account.save(); } } @@ -289,7 +292,7 @@ public class Manager implements Signal { } public void updateAccountAttributes() throws IOException { - accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), account.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, capabilities); + accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), account.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, BaseConfig.capabilities); } public void setProfileName(String name) throws IOException { @@ -350,6 +353,7 @@ public class Manager implements Signal { } } account = SignalAccount.createLinkedAccount(dataPath, username, ret.getUuid(), account.getPassword(), ret.getDeviceId(), ret.getIdentity(), account.getSignalProtocolStore().getLocalRegistrationId(), account.getSignalingKey(), profileKey); + account.setResolver(this::resolveSignalServiceAddress); refreshPreKeys(); @@ -427,12 +431,13 @@ public class Manager implements Signal { verificationCode = verificationCode.replace("-", ""); account.setSignalingKey(KeyUtils.createSignalingKey()); // TODO make unrestricted unidentified access configurable - UUID uuid = accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, null, getSelfUnidentifiedAccessKey(), false, capabilities); + UUID uuid = accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, null, getSelfUnidentifiedAccessKey(), false, BaseConfig.capabilities); //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); account.setRegistered(true); account.setUuid(uuid); account.setRegistrationLockPin(pin); + account.getSignalProtocolStore().saveIdentity(account.getSelfAddress(), account.getSignalProtocolStore().getIdentityKeyPair().getPublicKey(), TrustLevel.TRUSTED_VERIFIED); refreshPreKeys(); account.save(); @@ -540,10 +545,10 @@ public class Manager implements Signal { sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress())); } - public void sendGroupMessageReaction(String emoji, boolean remove, SignalServiceAddress targetAuthor, + public void sendGroupMessageReaction(String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, byte[] groupId) - throws IOException, EncapsulatedExceptions, AttachmentInvalidException { - SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, targetAuthor, targetSentTimestamp); + throws IOException, EncapsulatedExceptions, AttachmentInvalidException, InvalidNumberException { + SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, canonicalizeAndResolveSignalServiceAddress(targetAuthor), targetSentTimestamp); final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .withReaction(reaction); if (groupId != null) { @@ -713,10 +718,10 @@ public class Manager implements Signal { sendMessageLegacy(messageBuilder, getSignalServiceAddresses(recipients)); } - public void sendMessageReaction(String emoji, boolean remove, SignalServiceAddress targetAuthor, + public void sendMessageReaction(String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, List recipients) throws IOException, EncapsulatedExceptions, AttachmentInvalidException, InvalidNumberException { - SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, targetAuthor, targetSentTimestamp); + SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, canonicalizeAndResolveSignalServiceAddress(targetAuthor), targetSentTimestamp); final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .withReaction(reaction); sendMessageLegacy(messageBuilder, getSignalServiceAddresses(recipients)); @@ -732,8 +737,7 @@ public class Manager implements Signal { @Override public String getContactName(String number) throws InvalidNumberException { - String canonicalizedNumber = Utils.canonicalizeNumber(number, account.getUsername()); - ContactInfo contact = account.getContactStore().getContact(new SignalServiceAddress(null, canonicalizedNumber)); + ContactInfo contact = account.getContactStore().getContact(canonicalizeAndResolveSignalServiceAddress(number)); if (contact == null) { return ""; } else { @@ -743,14 +747,13 @@ public class Manager implements Signal { @Override public void setContactName(String number, String name) throws InvalidNumberException { - String canonicalizedNumber = Utils.canonicalizeNumber(number, account.getUsername()); - final SignalServiceAddress address = new SignalServiceAddress(null, canonicalizedNumber); + final SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(number); ContactInfo contact = account.getContactStore().getContact(address); if (contact == null) { contact = new ContactInfo(address); - System.err.println("Add contact " + canonicalizedNumber + " named " + name); + System.err.println("Add contact " + contact.number + " named " + name); } else { - System.err.println("Updating contact " + canonicalizedNumber + " name " + contact.name + " -> " + name); + System.err.println("Updating contact " + contact.number + " name " + contact.name + " -> " + name); } contact.name = name; account.getContactStore().updateContact(contact); @@ -759,14 +762,16 @@ public class Manager implements Signal { @Override public void setContactBlocked(String number, boolean blocked) throws InvalidNumberException { - number = Utils.canonicalizeNumber(number, account.getUsername()); - final SignalServiceAddress address = new SignalServiceAddress(null, number); + setContactBlocked(canonicalizeAndResolveSignalServiceAddress(number), blocked); + } + + private void setContactBlocked(SignalServiceAddress address, boolean blocked) { ContactInfo contact = account.getContactStore().getContact(address); if (contact == null) { contact = new ContactInfo(address); - System.err.println("Adding and " + (blocked ? "blocking" : "unblocking") + " contact " + number); + System.err.println("Adding and " + (blocked ? "blocking" : "unblocking") + " contact " + address.getNumber().orNull()); } else { - System.err.println((blocked ? "Blocking" : "Unblocking") + " contact " + number); + System.err.println((blocked ? "Blocking" : "Unblocking") + " contact " + address.getNumber().orNull()); } contact.blocked = blocked; account.getContactStore().updateContact(contact); @@ -996,10 +1001,16 @@ public class Manager implements Signal { } } - private byte[] getSenderCertificate() throws IOException { + private byte[] getSenderCertificate() { // TODO support UUID capable sender certificates // byte[] certificate = accountManager.getSenderCertificate(); - byte[] certificate = accountManager.getSenderCertificateLegacy(); + byte[] certificate; + try { + certificate = accountManager.getSenderCertificateLegacy(); + } catch (IOException e) { + System.err.println("Failed to get sender certificate: " + e); + return null; + } // TODO cache for a day return certificate; } @@ -1023,7 +1034,7 @@ public class Manager implements Signal { } } - private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) throws IOException { + private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) { ContactInfo contact = account.getContactStore().getContact(recipient); if (contact == null || contact.profileKey == null) { return null; @@ -1031,10 +1042,16 @@ public class Manager implements Signal { ProfileKey theirProfileKey; try { theirProfileKey = new ProfileKey(Base64.decode(contact.profileKey)); - } catch (InvalidInputException e) { + } catch (InvalidInputException | IOException e) { throw new AssertionError(e); } - SignalProfile targetProfile = decryptProfile(getRecipientProfile(recipient, Optional.absent()), theirProfileKey); + SignalProfile targetProfile; + try { + targetProfile = decryptProfile(getRecipientProfile(recipient, Optional.absent()), theirProfileKey); + } catch (IOException e) { + System.err.println("Failed to get recipient profile: " + e); + return null; + } if (targetProfile == null || targetProfile.getUnidentifiedAccess() == null) { return null; @@ -1047,7 +1064,7 @@ public class Manager implements Signal { return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey); } - private Optional getAccessForSync() throws IOException { + private Optional getAccessForSync() { byte[] selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(); byte[] selfUnidentifiedAccessCertificate = getSenderCertificate(); @@ -1065,7 +1082,7 @@ public class Manager implements Signal { } } - private List> getAccessFor(Collection recipients) throws IOException { + private List> getAccessFor(Collection recipients) { List> result = new ArrayList<>(recipients.size()); for (SignalServiceAddress recipient : recipients) { result.add(getAccessFor(recipient)); @@ -1073,7 +1090,7 @@ public class Manager implements Signal { return result; } - private Optional getAccessFor(SignalServiceAddress recipient) throws IOException { + private Optional getAccessFor(SignalServiceAddress recipient) { byte[] recipientUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient); byte[] selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(); byte[] selfUnidentifiedAccessCertificate = getSenderCertificate(); @@ -1092,13 +1109,23 @@ public class Manager implements Signal { } } + private Optional getUnidentifiedAccess(SignalServiceAddress recipient) { + Optional unidentifiedAccess = getAccessFor(recipient); + + if (unidentifiedAccess.isPresent()) { + return unidentifiedAccess.get().getTargetUnidentifiedAccess(); + } + + return Optional.absent(); + } + private void sendSyncMessage(SignalServiceSyncMessage message) throws IOException, UntrustedIdentityException { SignalServiceMessageSender messageSender = getMessageSender(); try { messageSender.sendMessage(message, getAccessForSync()); } catch (UntrustedIdentityException e) { - account.getSignalProtocolStore().saveIdentity(e.getIdentifier(), e.getIdentityKey(), TrustLevel.UNTRUSTED); + account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED); throw e; } } @@ -1116,11 +1143,11 @@ public class Manager implements Signal { for (SendMessageResult result : results) { if (result.isUnregisteredFailure()) { - unregisteredUsers.add(new UnregisteredUserException(result.getAddress().getNumber().get(), null)); + unregisteredUsers.add(new UnregisteredUserException(result.getAddress().getLegacyIdentifier(), null)); } else if (result.isNetworkFailure()) { - networkExceptions.add(new NetworkFailureException(result.getAddress().getNumber().get(), null)); + networkExceptions.add(new NetworkFailureException(result.getAddress().getLegacyIdentifier(), null)); } else if (result.getIdentityFailure() != null) { - untrustedIdentities.add(new UntrustedIdentityException("Untrusted", result.getAddress().getNumber().get(), result.getIdentityFailure().getIdentityKey())); + untrustedIdentities.add(new UntrustedIdentityException("Untrusted", result.getAddress().getLegacyIdentifier(), result.getIdentityFailure().getIdentityKey())); } } if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty() || !networkExceptions.isEmpty()) { @@ -1130,19 +1157,9 @@ public class Manager implements Signal { private Collection getSignalServiceAddresses(Collection numbers) throws InvalidNumberException { final Set signalServiceAddresses = new HashSet<>(numbers.size()); - final String username = account.getUsername(); for (String number : numbers) { - String canonicalizedNumber = Utils.canonicalizeNumber(number, username); - if (canonicalizedNumber.equals(username)) { - signalServiceAddresses.add(account.getSelfAddress()); - } else { - SignalServiceAddress address = new SignalServiceAddress(null, canonicalizedNumber); - ContactInfo contact = account.getContactStore().getContact(address); - signalServiceAddresses.add(contact == null - ? address - : contact.getAddress()); - } + signalServiceAddresses.add(canonicalizeAndResolveSignalServiceAddress(number)); } return signalServiceAddresses; } @@ -1166,12 +1183,12 @@ public class Manager implements Signal { List result = messageSender.sendMessage(new ArrayList<>(recipients), getAccessFor(recipients), isRecipientUpdate, message); for (SendMessageResult r : result) { if (r.getIdentityFailure() != null) { - account.getSignalProtocolStore().saveIdentity(r.getAddress().getNumber().get(), r.getIdentityFailure().getIdentityKey(), TrustLevel.UNTRUSTED); + account.getSignalProtocolStore().saveIdentity(r.getAddress(), r.getIdentityFailure().getIdentityKey(), TrustLevel.UNTRUSTED); } } return result; } catch (UntrustedIdentityException e) { - account.getSignalProtocolStore().saveIdentity(e.getIdentifier(), e.getIdentityKey(), TrustLevel.UNTRUSTED); + account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED); return Collections.emptyList(); } } else if (recipients.size() == 1 && recipients.contains(account.getSelfAddress())) { @@ -1189,7 +1206,7 @@ public class Manager implements Signal { try { messageSender.sendMessage(syncMessage, unidentifiedAccess); } catch (UntrustedIdentityException e) { - account.getSignalProtocolStore().saveIdentity(e.getIdentifier(), e.getIdentityKey(), TrustLevel.UNTRUSTED); + account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED); results.add(SendMessageResult.identityFailure(recipient, e.getIdentityKey())); } return results; @@ -1210,7 +1227,7 @@ public class Manager implements Signal { SendMessageResult result = messageSender.sendMessage(address, getAccessFor(address), message); results.add(result); } catch (UntrustedIdentityException e) { - account.getSignalProtocolStore().saveIdentity(e.getIdentifier(), e.getIdentityKey(), TrustLevel.UNTRUSTED); + account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED); results.add(SendMessageResult.identityFailure(address, e.getIdentityKey())); } } @@ -1219,7 +1236,7 @@ public class Manager implements Signal { } finally { if (message != null && message.isEndSession()) { for (SignalServiceAddress recipient : recipients) { - handleEndSession(recipient.getNumber().get()); + handleEndSession(recipient); } } account.save(); @@ -1237,7 +1254,7 @@ public class Manager implements Signal { } } - private void handleEndSession(String source) { + private void handleEndSession(SignalServiceAddress source) { account.getSignalProtocolStore().deleteAllSessions(source); } @@ -1307,7 +1324,7 @@ public class Manager implements Signal { } } if (message.isEndSession()) { - handleEndSession(isSync ? destination.getNumber().get() : source.getNumber().get()); + handleEndSession(isSync ? destination : source); } if (message.isExpirationUpdate() || message.getBody().isPresent()) { if (message.getGroupInfo().isPresent()) { @@ -1384,6 +1401,7 @@ public class Manager implements Signal { } for (final File dir : Objects.requireNonNull(cachePath.listFiles())) { if (!dir.isDirectory()) { + retryFailedReceivedMessage(handler, ignoreAttachments, dir); continue; } @@ -1391,38 +1409,42 @@ public class Manager implements Signal { if (!fileEntry.isFile()) { continue; } - SignalServiceEnvelope envelope; - try { - envelope = Utils.loadEnvelope(fileEntry); - if (envelope == null) { - continue; - } - } catch (IOException e) { - e.printStackTrace(); - continue; - } - SignalServiceContent content = null; - if (!envelope.isReceipt()) { - try { - content = decryptMessage(envelope); - } catch (Exception e) { - continue; - } - handleMessage(envelope, content, ignoreAttachments); - } - account.save(); - handler.handleMessage(envelope, content, null); - try { - Files.delete(fileEntry.toPath()); - } catch (IOException e) { - System.err.println("Failed to delete cached message file “" + fileEntry + "”: " + e.getMessage()); - } + retryFailedReceivedMessage(handler, ignoreAttachments, fileEntry); } // Try to delete directory if empty dir.delete(); } } + private void retryFailedReceivedMessage(final ReceiveMessageHandler handler, final boolean ignoreAttachments, final File fileEntry) { + SignalServiceEnvelope envelope; + try { + envelope = Utils.loadEnvelope(fileEntry); + if (envelope == null) { + return; + } + } catch (IOException e) { + e.printStackTrace(); + return; + } + SignalServiceContent content = null; + if (!envelope.isReceipt()) { + try { + content = decryptMessage(envelope); + } catch (Exception e) { + return; + } + handleMessage(envelope, content, ignoreAttachments); + } + account.save(); + handler.handleMessage(envelope, content, null); + try { + Files.delete(fileEntry.toPath()); + } catch (IOException e) { + System.err.println("Failed to delete cached message file “" + fileEntry + "”: " + e.getMessage()); + } + } + public void receiveMessages(long timeout, TimeUnit unit, boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler) throws IOException { retryFailedReceivedMessages(handler, ignoreAttachments); final SignalServiceMessageReceiver messageReceiver = getMessageReceiver(); @@ -1496,7 +1518,7 @@ public class Manager implements Signal { } else { return false; } - ContactInfo sourceContact = getContact(source.getNumber().get()); + ContactInfo sourceContact = account.getContactStore().getContact(source); if (sourceContact != null && sourceContact.blocked) { return true; } @@ -1617,13 +1639,7 @@ public class Manager implements Signal { if (syncMessage.getBlockedList().isPresent()) { final BlockedListMessage blockedListMessage = syncMessage.getBlockedList().get(); for (SignalServiceAddress address : blockedListMessage.getAddresses()) { - if (address.getNumber().isPresent()) { - try { - setContactBlocked(address.getNumber().get(), true); - } catch (InvalidNumberException e) { - e.printStackTrace(); - } - } + setContactBlocked(address, true); } for (byte[] groupId : blockedListMessage.getGroupIds()) { try { @@ -1663,7 +1679,7 @@ public class Manager implements Signal { } if (c.getVerified().isPresent()) { final VerifiedMessage verifiedMessage = c.getVerified().get(); - account.getSignalProtocolStore().saveIdentity(verifiedMessage.getDestination().getNumber().get(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); + account.getSignalProtocolStore().setIdentityTrustLevel(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); } if (c.getExpirationTimer().isPresent()) { contact.messageExpirationTime = c.getExpirationTimer().get(); @@ -1692,7 +1708,7 @@ public class Manager implements Signal { } if (syncMessage.getVerified().isPresent()) { final VerifiedMessage verifiedMessage = syncMessage.getVerified().get(); - account.getSignalProtocolStore().saveIdentity(verifiedMessage.getDestination().getNumber().get(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); + account.getSignalProtocolStore().setIdentityTrustLevel(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); } if (syncMessage.getConfiguration().isPresent()) { // TODO @@ -1829,16 +1845,9 @@ public class Manager implements Signal { DeviceContactsOutputStream out = new DeviceContactsOutputStream(fos); for (ContactInfo record : account.getContactStore().getContacts()) { VerifiedMessage verifiedMessage = null; - if (getIdentities().containsKey(record.number)) { - JsonIdentityKeyStore.Identity currentIdentity = null; - for (JsonIdentityKeyStore.Identity id : getIdentities().get(record.number)) { - if (currentIdentity == null || id.getDateAdded().after(currentIdentity.getDateAdded())) { - currentIdentity = id; - } - } - if (currentIdentity != null) { - verifiedMessage = new VerifiedMessage(record.getAddress(), currentIdentity.getIdentityKey(), currentIdentity.getTrustLevel().toVerifiedState(), currentIdentity.getDateAdded().getTime()); - } + JsonIdentityKeyStore.Identity currentIdentity = account.getSignalProtocolStore().getIdentity(record.getAddress()); + if (currentIdentity != null) { + verifiedMessage = new VerifiedMessage(record.getAddress(), currentIdentity.getIdentityKey(), currentIdentity.getTrustLevel().toVerifiedState(), currentIdentity.getDateAdded().getTime()); } ProfileKey profileKey = null; @@ -1909,20 +1918,19 @@ public class Manager implements Signal { } public ContactInfo getContact(String number) { - return account.getContactStore().getContact(new SignalServiceAddress(null, number)); + return account.getContactStore().getContact(Util.getSignalServiceAddressFromIdentifier(number)); } public GroupInfo getGroup(byte[] groupId) { return account.getGroupStore().getGroup(groupId); } - public Map> getIdentities() { + public List getIdentities() { return account.getSignalProtocolStore().getIdentities(); } - public Pair> getIdentities(String number) throws InvalidNumberException { - String canonicalizedNumber = Utils.canonicalizeNumber(number, account.getUsername()); - return new Pair<>(canonicalizedNumber, account.getSignalProtocolStore().getIdentities(canonicalizedNumber)); + public List getIdentities(String number) throws InvalidNumberException { + return account.getSignalProtocolStore().getIdentities(canonicalizeAndResolveSignalServiceAddress(number)); } /** @@ -1931,8 +1939,9 @@ public class Manager implements Signal { * @param name username of the identity * @param fingerprint Fingerprint */ - public boolean trustIdentityVerified(String name, byte[] fingerprint) { - List ids = account.getSignalProtocolStore().getIdentities(name); + public boolean trustIdentityVerified(String name, byte[] fingerprint) throws InvalidNumberException { + SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(name); + List ids = account.getSignalProtocolStore().getIdentities(address); if (ids == null) { return false; } @@ -1941,9 +1950,9 @@ public class Manager implements Signal { continue; } - account.getSignalProtocolStore().saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); + account.getSignalProtocolStore().setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); try { - sendVerifiedMessage(new SignalServiceAddress(null, name), id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); + sendVerifiedMessage(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); } catch (IOException | UntrustedIdentityException e) { e.printStackTrace(); } @@ -1959,19 +1968,20 @@ public class Manager implements Signal { * @param name username of the identity * @param safetyNumber Safety number */ - public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) { - List ids = account.getSignalProtocolStore().getIdentities(name); + public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) throws InvalidNumberException { + SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(name); + List ids = account.getSignalProtocolStore().getIdentities(address); if (ids == null) { return false; } for (JsonIdentityKeyStore.Identity id : ids) { - if (!safetyNumber.equals(computeSafetyNumber(name, id.getIdentityKey()))) { + if (!safetyNumber.equals(computeSafetyNumber(address, id.getIdentityKey()))) { continue; } - account.getSignalProtocolStore().saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); + account.getSignalProtocolStore().setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); try { - sendVerifiedMessage(new SignalServiceAddress(null, name), id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); + sendVerifiedMessage(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); } catch (IOException | UntrustedIdentityException e) { e.printStackTrace(); } @@ -1987,15 +1997,16 @@ public class Manager implements Signal { * @param name username of the identity */ public boolean trustIdentityAllKeys(String name) { - List ids = account.getSignalProtocolStore().getIdentities(name); + SignalServiceAddress address = resolveSignalServiceAddress(name); + List ids = account.getSignalProtocolStore().getIdentities(address); if (ids == null) { return false; } for (JsonIdentityKeyStore.Identity id : ids) { if (id.getTrustLevel() == TrustLevel.UNTRUSTED) { - account.getSignalProtocolStore().saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED); + account.getSignalProtocolStore().setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED); try { - sendVerifiedMessage(new SignalServiceAddress(null, name), id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED); + sendVerifiedMessage(address, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED); } catch (IOException | UntrustedIdentityException e) { e.printStackTrace(); } @@ -2005,8 +2016,26 @@ public class Manager implements Signal { return true; } - public String computeSafetyNumber(String theirUsername, IdentityKey theirIdentityKey) { - return Utils.computeSafetyNumber(account.getUsername(), getIdentity(), theirUsername, theirIdentityKey); + public String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { + return Utils.computeSafetyNumber(account.getSelfAddress(), getIdentity(), theirAddress, theirIdentityKey); + } + + public SignalServiceAddress canonicalizeAndResolveSignalServiceAddress(String identifier) throws InvalidNumberException { + String canonicalizedNumber = UuidUtil.isUuid(identifier) ? identifier : Util.canonicalizeNumber(identifier, account.getUsername()); + return resolveSignalServiceAddress(canonicalizedNumber); + } + + public SignalServiceAddress resolveSignalServiceAddress(String identifier) { + SignalServiceAddress address = Util.getSignalServiceAddressFromIdentifier(identifier); + if (address.matches(account.getSelfAddress())) { + return account.getSelfAddress(); + } + + ContactInfo contactInfo = account.getContactStore().getContact(address); + if (contactInfo == null) { + return address; + } + return contactInfo.getAddress(); } public interface ReceiveMessageHandler { diff --git a/src/main/java/org/asamk/signal/manager/Utils.java b/src/main/java/org/asamk/signal/manager/Utils.java index 4396e0ca..0b74f01d 100644 --- a/src/main/java/org/asamk/signal/manager/Utils.java +++ b/src/main/java/org/asamk/signal/manager/Utils.java @@ -13,9 +13,8 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.util.InvalidNumberException; -import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.api.util.StreamDetails; +import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.util.Base64; import java.io.BufferedInputStream; @@ -38,6 +37,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import static org.whispersystems.signalservice.internal.util.Util.isEmpty; @@ -146,19 +146,19 @@ class Utils { return new DeviceLinkInfo(deviceIdentifier, deviceKey); } - static String canonicalizeNumber(String number, String localNumber) throws InvalidNumberException { - return PhoneNumberFormatter.formatNumber(number, localNumber); - } - static SignalServiceEnvelope loadEnvelope(File file) throws IOException { try (FileInputStream f = new FileInputStream(file)) { DataInputStream in = new DataInputStream(f); int version = in.readInt(); - if (version > 2) { + if (version > 3) { return null; } int type = in.readInt(); String source = in.readUTF(); + UUID sourceUuid = null; + if (version >= 3) { + sourceUuid = UuidUtil.parseOrNull(in.readUTF()); + } int sourceDevice = in.readInt(); if (version == 1) { // read legacy relay field @@ -186,16 +186,20 @@ class Utils { uuid = null; } } - return new SignalServiceEnvelope(type, Optional.of(new SignalServiceAddress(null, source)), sourceDevice, timestamp, legacyMessage, content, serverTimestamp, uuid); + Optional addressOptional = sourceUuid == null && source.isEmpty() + ? Optional.absent() + : Optional.of(new SignalServiceAddress(sourceUuid, source)); + return new SignalServiceEnvelope(type, addressOptional, sourceDevice, timestamp, legacyMessage, content, serverTimestamp, uuid); } } static void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException { try (FileOutputStream f = new FileOutputStream(file)) { try (DataOutputStream out = new DataOutputStream(f)) { - out.writeInt(2); // version + out.writeInt(3); // version out.writeInt(envelope.getType()); - out.writeUTF(envelope.getSourceE164().get()); + out.writeUTF(envelope.getSourceE164().isPresent() ? envelope.getSourceE164().get() : ""); + out.writeUTF(envelope.getSourceUuid().isPresent() ? envelope.getSourceUuid().get() : ""); out.writeInt(envelope.getSourceDevice()); out.writeLong(envelope.getTimestamp()); if (envelope.hasContent()) { @@ -234,10 +238,25 @@ class Utils { return outputFile; } - static String computeSafetyNumber(String ownUsername, IdentityKey ownIdentityKey, String theirUsername, IdentityKey theirIdentityKey) { - // Version 1: E164 user - // Version 2: UUID user - Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(1, ownUsername.getBytes(), ownIdentityKey, theirUsername.getBytes(), theirIdentityKey); + static String computeSafetyNumber(SignalServiceAddress ownAddress, IdentityKey ownIdentityKey, SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { + int version; + byte[] ownId; + byte[] theirId; + + if (BaseConfig.capabilities.isUuid() + && ownAddress.getUuid().isPresent() && theirAddress.getUuid().isPresent()) { + // Version 2: UUID user + version = 2; + ownId = UuidUtil.toByteArray(ownAddress.getUuid().get()); + theirId = UuidUtil.toByteArray(theirAddress.getUuid().get()); + } else { + // Version 1: E164 user + version = 1; + ownId = ownAddress.getNumber().get().getBytes(); + theirId = theirAddress.getNumber().get().getBytes(); + } + + Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(version, ownId, ownIdentityKey, theirId, theirIdentityKey); return fingerprint.getDisplayableFingerprint().getDisplayText(); } diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java index e806794f..73f79a48 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -15,6 +15,7 @@ import org.asamk.signal.storage.contacts.JsonContactsStore; import org.asamk.signal.storage.groups.GroupInfo; import org.asamk.signal.storage.groups.JsonGroupStore; import org.asamk.signal.storage.protocol.JsonSignalProtocolStore; +import org.asamk.signal.storage.protocol.SignalServiceAddressResolver; import org.asamk.signal.storage.threads.LegacyJsonThreadStore; import org.asamk.signal.storage.threads.ThreadInfo; import org.asamk.signal.util.IOUtils; @@ -273,6 +274,10 @@ public class SignalAccount { } } + public void setResolver(final SignalServiceAddressResolver resolver) { + signalProtocolStore.setResolver(resolver); + } + public void addPreKeys(Collection records) { for (PreKeyRecord record : records) { signalProtocolStore.storePreKey(record.getId(), record); diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java index 53d6bf23..ddb69096 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java @@ -9,32 +9,48 @@ import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import org.asamk.signal.TrustLevel; +import org.asamk.signal.util.Util; 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 org.whispersystems.util.Base64; import java.io.IOException; import java.util.ArrayList; import java.util.Date; -import java.util.HashMap; import java.util.List; -import java.util.Map; +import java.util.UUID; public class JsonIdentityKeyStore implements IdentityKeyStore { - private final Map> trustedKeys = new HashMap<>(); + 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 Util.getSignalServiceAddressFromIdentifier(identifier); + } + } + @Override public IdentityKeyPair getIdentityKeyPair() { return identityKeyPair; @@ -47,85 +63,116 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { @Override public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) { - return saveIdentity(address.getName(), identityKey, TrustLevel.TRUSTED_UNVERIFIED, null); + return saveIdentity(resolveSignalServiceAddress(address.getName()), identityKey, TrustLevel.TRUSTED_UNVERIFIED, null); } /** - * Adds or updates the given identityKey for the user name and sets the trustLevel and added timestamp. + * 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 name User name, i.e. phone number - * @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. + * @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(String name, IdentityKey identityKey, TrustLevel trustLevel, Date added) { - List identities = trustedKeys.get(name); - if (identities == null) { - identities = new ArrayList<>(); - trustedKeys.put(name, identities); - } else { - for (Identity id : identities) { - if (!id.identityKey.equals(identityKey)) - continue; + public boolean saveIdentity(SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel, Date added) { + for (Identity id : identities) { + if (!id.address.matches(serviceAddress) || !id.identityKey.equals(identityKey)) { + continue; + } - if (id.trustLevel.compareTo(trustLevel) < 0) { - id.trustLevel = trustLevel; - } - if (added != null) { - id.added = added; - } - return true; + 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 Identity(identityKey, trustLevel, added != null ? added : new Date())); + + identities.add(new Identity(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 (Identity 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 Identity(serviceAddress, identityKey, trustLevel, new Date())); + } + @Override public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) { // TODO implement possibility for different handling of incoming/outgoing trust decisions - List identities = trustedKeys.get(address.getName()); - if (identities == null) { - // Trust on first use - return true; - } + SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName()); + boolean trustOnFirstUse = true; for (Identity id : identities) { + if (!id.address.matches(serviceAddress)) { + continue; + } + if (id.identityKey.equals(identityKey)) { return id.isTrusted(); + } else { + trustOnFirstUse = false; } } - return false; + return trustOnFirstUse; } @Override public IdentityKey getIdentity(SignalProtocolAddress address) { - List identities = trustedKeys.get(address.getName()); - if (identities == null || identities.size() == 0) { - return null; - } + SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName()); + Identity identity = getIdentity(serviceAddress); + return identity == null ? null : identity.getIdentityKey(); + } + public Identity getIdentity(SignalServiceAddress serviceAddress) { long maxDate = 0; Identity maxIdentity = null; - for (Identity id : identities) { + for (Identity id : this.identities) { + if (!id.address.matches(serviceAddress)) { + continue; + } + final long time = id.getDateAdded().getTime(); if (maxIdentity == null || maxDate <= time) { maxDate = time; maxIdentity = id; } } - return maxIdentity.getIdentityKey(); + return maxIdentity; } - public Map> getIdentities() { + public List getIdentities() { // TODO deep copy - return trustedKeys; + return identities; } - public List getIdentities(String name) { - // TODO deep copy - return trustedKeys.get(name); + public List getIdentities(SignalServiceAddress serviceAddress) { + List identities = new ArrayList<>(); + for (Identity identity : this.identities) { + if (identity.address.matches(serviceAddress)) { + identities.add(identity); + } + } + return identities; } public static class JsonIdentityKeyStoreDeserializer extends JsonDeserializer { @@ -143,12 +190,26 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { JsonNode trustedKeysNode = node.get("trustedKeys"); if (trustedKeysNode.isArray()) { for (JsonNode trustedKey : trustedKeysNode) { - String trustedKeyName = trustedKey.get("name").asText(); + String trustedKeyName = trustedKey.has("name") + ? trustedKey.get("name").asText() + : null; + + if (UuidUtil.isUuid(trustedKeyName)) { + // Ignore identities that were incorrectly created with UUIDs as name + continue; + } + + UUID uuid = trustedKey.hasNonNull("uuid") + ? UuidUtil.parseOrNull(trustedKey.get("uuid").asText()) + : null; + final SignalServiceAddress serviceAddress = uuid == null + ? Util.getSignalServiceAddressFromIdentifier(trustedKeyName) + : new SignalServiceAddress(uuid, trustedKeyName); try { IdentityKey id = new IdentityKey(Base64.decode(trustedKey.get("identityKey").asText()), 0); TrustLevel trustLevel = trustedKey.has("trustLevel") ? TrustLevel.fromInt(trustedKey.get("trustLevel").asInt()) : TrustLevel.TRUSTED_UNVERIFIED; Date added = trustedKey.has("addedTimestamp") ? new Date(trustedKey.get("addedTimestamp").asLong()) : new Date(); - keyStore.saveIdentity(trustedKeyName, id, trustLevel, added); + keyStore.saveIdentity(serviceAddress, id, trustLevel, added); } catch (InvalidKeyException | IOException e) { System.out.println(String.format("Error while decoding key for: %s", trustedKeyName)); } @@ -170,15 +231,18 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { json.writeNumberField("registrationId", jsonIdentityKeyStore.getLocalRegistrationId()); json.writeStringField("identityKey", Base64.encodeBytes(jsonIdentityKeyStore.getIdentityKeyPair().serialize())); json.writeArrayFieldStart("trustedKeys"); - for (Map.Entry> trustedKey : jsonIdentityKeyStore.trustedKeys.entrySet()) { - for (Identity id : trustedKey.getValue()) { - json.writeStartObject(); - json.writeStringField("name", trustedKey.getKey()); - json.writeStringField("identityKey", Base64.encodeBytes(id.identityKey.serialize())); - json.writeNumberField("trustLevel", id.trustLevel.ordinal()); - json.writeNumberField("addedTimestamp", id.added.getTime()); - json.writeEndObject(); + for (Identity 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.encodeBytes(trustedKey.identityKey.serialize())); + json.writeNumberField("trustLevel", trustedKey.trustLevel.ordinal()); + json.writeNumberField("addedTimestamp", trustedKey.added.getTime()); + json.writeEndObject(); } json.writeEndArray(); json.writeEndObject(); @@ -187,22 +251,33 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { public static class Identity { + SignalServiceAddress address; IdentityKey identityKey; TrustLevel trustLevel; Date added; - public Identity(IdentityKey identityKey, TrustLevel trustLevel) { + public Identity(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel) { + this.address = address; this.identityKey = identityKey; this.trustLevel = trustLevel; this.added = new Date(); } - Identity(IdentityKey identityKey, TrustLevel trustLevel, Date added) { + Identity(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel, Date added) { + this.address = address; this.identityKey = identityKey; this.trustLevel = trustLevel; this.added = added; } + public SignalServiceAddress getAddress() { + return address; + } + + public void setAddress(final SignalServiceAddress address) { + this.address = address; + } + boolean isTrusted() { return trustLevel == TrustLevel.TRUSTED_UNVERIFIED || trustLevel == TrustLevel.TRUSTED_VERIFIED; diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java index f7bbf204..90229575 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java @@ -8,51 +8,68 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; +import org.asamk.signal.util.Util; import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.state.SessionRecord; import org.whispersystems.libsignal.state.SessionStore; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.util.Base64; import java.io.IOException; import java.util.ArrayList; -import java.util.HashMap; import java.util.LinkedList; import java.util.List; -import java.util.Map; +import java.util.UUID; class JsonSessionStore implements SessionStore { - private final Map sessions = new HashMap<>(); + private final List sessions = new ArrayList<>(); + + private SignalServiceAddressResolver resolver; public JsonSessionStore() { + } + public void setResolver(final SignalServiceAddressResolver resolver) { + this.resolver = resolver; } - private void addSessions(Map sessions) { - this.sessions.putAll(sessions); + private SignalServiceAddress resolveSignalServiceAddress(String identifier) { + if (resolver != null) { + return resolver.resolveSignalServiceAddress(identifier); + } else { + return Util.getSignalServiceAddressFromIdentifier(identifier); + } } @Override - public synchronized SessionRecord loadSession(SignalProtocolAddress remoteAddress) { - try { - if (containsSession(remoteAddress)) { - return new SessionRecord(sessions.get(remoteAddress)); - } else { - return new SessionRecord(); + public synchronized SessionRecord loadSession(SignalProtocolAddress address) { + SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName()); + for (SessionInfo info : sessions) { + if (info.address.matches(serviceAddress) && info.deviceId == address.getDeviceId()) { + try { + return new SessionRecord(info.sessionRecord); + } catch (IOException e) { + System.err.println("Failed to load session, resetting session: " + e); + final SessionRecord sessionRecord = new SessionRecord(); + info.sessionRecord = sessionRecord.serialize(); + return sessionRecord; + } } - } catch (IOException e) { - throw new AssertionError(e); } + + return new SessionRecord(); } @Override public synchronized List getSubDeviceSessions(String name) { - List deviceIds = new LinkedList<>(); + SignalServiceAddress serviceAddress = resolveSignalServiceAddress(name); - for (SignalProtocolAddress key : sessions.keySet()) { - if (key.getName().equals(name) && - key.getDeviceId() != 1) { - deviceIds.add(key.getDeviceId()); + List deviceIds = new LinkedList<>(); + for (SessionInfo info : sessions) { + if (info.address.matches(serviceAddress) && info.deviceId != 1) { + deviceIds.add(info.deviceId); } } @@ -61,26 +78,45 @@ class JsonSessionStore implements SessionStore { @Override public synchronized void storeSession(SignalProtocolAddress address, SessionRecord record) { - sessions.put(address, record.serialize()); + SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName()); + for (SessionInfo info : sessions) { + if (info.address.matches(serviceAddress) && info.deviceId == address.getDeviceId()) { + if (!info.address.getUuid().isPresent() || !info.address.getNumber().isPresent()) { + info.address = serviceAddress; + } + info.sessionRecord = record.serialize(); + return; + } + } + + sessions.add(new SessionInfo(serviceAddress, address.getDeviceId(), record.serialize())); } @Override public synchronized boolean containsSession(SignalProtocolAddress address) { - return sessions.containsKey(address); + SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName()); + for (SessionInfo info : sessions) { + if (info.address.matches(serviceAddress) && info.deviceId == address.getDeviceId()) { + return true; + } + } + return false; } @Override public synchronized void deleteSession(SignalProtocolAddress address) { - sessions.remove(address); + SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName()); + sessions.removeIf(info -> info.address.matches(serviceAddress) && info.deviceId == address.getDeviceId()); } @Override public synchronized void deleteAllSessions(String name) { - for (SignalProtocolAddress key : new ArrayList<>(sessions.keySet())) { - if (key.getName().equals(name)) { - sessions.remove(key); - } - } + SignalServiceAddress serviceAddress = resolveSignalServiceAddress(name); + deleteAllSessions(serviceAddress); + } + + public synchronized void deleteAllSessions(SignalServiceAddress serviceAddress) { + sessions.removeIf(info -> info.address.matches(serviceAddress)); } public static class JsonSessionStoreDeserializer extends JsonDeserializer { @@ -89,23 +125,36 @@ class JsonSessionStore implements SessionStore { public JsonSessionStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { JsonNode node = jsonParser.getCodec().readTree(jsonParser); - Map sessionMap = new HashMap<>(); + JsonSessionStore sessionStore = new JsonSessionStore(); + if (node.isArray()) { for (JsonNode session : node) { - String sessionName = session.get("name").asText(); + String sessionName = session.has("name") + ? session.get("name").asText() + : null; + if (UuidUtil.isUuid(sessionName)) { + // Ignore sessions that were incorrectly created with UUIDs as name + continue; + } + + UUID uuid = session.hasNonNull("uuid") + ? UuidUtil.parseOrNull(session.get("uuid").asText()) + : null; + final SignalServiceAddress serviceAddress = uuid == null + ? Util.getSignalServiceAddressFromIdentifier(sessionName) + : new SignalServiceAddress(uuid, sessionName); + final int deviceId = session.get("deviceId").asInt(); + final String record = session.get("record").asText(); try { - sessionMap.put(new SignalProtocolAddress(sessionName, session.get("deviceId").asInt()), Base64.decode(session.get("record").asText())); + SessionInfo sessionInfo = new SessionInfo(serviceAddress, deviceId, Base64.decode(record)); + sessionStore.sessions.add(sessionInfo); } catch (IOException e) { System.out.println(String.format("Error while decoding session for: %s", sessionName)); } } } - JsonSessionStore sessionStore = new JsonSessionStore(); - sessionStore.addSessions(sessionMap); - return sessionStore; - } } @@ -114,14 +163,20 @@ class JsonSessionStore implements SessionStore { @Override public void serialize(JsonSessionStore jsonSessionStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException { json.writeStartArray(); - for (Map.Entry preKey : jsonSessionStore.sessions.entrySet()) { + for (SessionInfo sessionInfo : jsonSessionStore.sessions) { json.writeStartObject(); - json.writeStringField("name", preKey.getKey().getName()); - json.writeNumberField("deviceId", preKey.getKey().getDeviceId()); - json.writeStringField("record", Base64.encodeBytes(preKey.getValue())); + if (sessionInfo.address.getNumber().isPresent()) { + json.writeStringField("name", sessionInfo.address.getNumber().get()); + } + if (sessionInfo.address.getUuid().isPresent()) { + json.writeStringField("uuid", sessionInfo.address.getUuid().get().toString()); + } + json.writeNumberField("deviceId", sessionInfo.deviceId); + json.writeStringField("record", Base64.encodeBytes(sessionInfo.sessionRecord)); json.writeEndObject(); } json.writeEndArray(); } } + } diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java index 65ee4a6e..c7079078 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java @@ -13,9 +13,9 @@ import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.SessionRecord; import org.whispersystems.libsignal.state.SignalProtocolStore; import org.whispersystems.libsignal.state.SignedPreKeyRecord; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.util.List; -import java.util.Map; public class JsonSignalProtocolStore implements SignalProtocolStore { @@ -56,6 +56,11 @@ public class JsonSignalProtocolStore implements SignalProtocolStore { this.identityKeyStore = new JsonIdentityKeyStore(identityKeyPair, registrationId); } + public void setResolver(final SignalServiceAddressResolver resolver) { + sessionStore.setResolver(resolver); + identityKeyStore.setResolver(resolver); + } + @Override public IdentityKeyPair getIdentityKeyPair() { return identityKeyStore.getIdentityKeyPair(); @@ -71,16 +76,20 @@ public class JsonSignalProtocolStore implements SignalProtocolStore { return identityKeyStore.saveIdentity(address, identityKey); } - public void saveIdentity(String name, IdentityKey identityKey, TrustLevel trustLevel) { - identityKeyStore.saveIdentity(name, identityKey, trustLevel, null); + 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 Map> getIdentities() { + public List getIdentities() { return identityKeyStore.getIdentities(); } - public List getIdentities(String name) { - return identityKeyStore.getIdentities(name); + public List getIdentities(SignalServiceAddress serviceAddress) { + return identityKeyStore.getIdentities(serviceAddress); } @Override @@ -93,6 +102,10 @@ public class JsonSignalProtocolStore implements SignalProtocolStore { return identityKeyStore.getIdentity(address); } + public JsonIdentityKeyStore.Identity getIdentity(SignalServiceAddress serviceAddress) { + return identityKeyStore.getIdentity(serviceAddress); + } + @Override public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { return preKeyStore.loadPreKey(preKeyId); @@ -143,6 +156,10 @@ public class JsonSignalProtocolStore implements SignalProtocolStore { sessionStore.deleteAllSessions(name); } + public void deleteAllSessions(SignalServiceAddress serviceAddress) { + sessionStore.deleteAllSessions(serviceAddress); + } + @Override public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { return signedPreKeyStore.loadSignedPreKey(signedPreKeyId); diff --git a/src/main/java/org/asamk/signal/storage/protocol/SessionInfo.java b/src/main/java/org/asamk/signal/storage/protocol/SessionInfo.java new file mode 100644 index 00000000..00221233 --- /dev/null +++ b/src/main/java/org/asamk/signal/storage/protocol/SessionInfo.java @@ -0,0 +1,18 @@ +package org.asamk.signal.storage.protocol; + +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + +public class SessionInfo { + + public SignalServiceAddress address; + + public int deviceId; + + public byte[] sessionRecord; + + public SessionInfo(final SignalServiceAddress address, final int deviceId, final byte[] sessionRecord) { + this.address = address; + this.deviceId = deviceId; + this.sessionRecord = sessionRecord; + } +} diff --git a/src/main/java/org/asamk/signal/storage/protocol/SignalServiceAddressResolver.java b/src/main/java/org/asamk/signal/storage/protocol/SignalServiceAddressResolver.java new file mode 100644 index 00000000..b1c5fb38 --- /dev/null +++ b/src/main/java/org/asamk/signal/storage/protocol/SignalServiceAddressResolver.java @@ -0,0 +1,13 @@ +package org.asamk.signal.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/src/main/java/org/asamk/signal/util/Util.java b/src/main/java/org/asamk/signal/util/Util.java index 01d8b2b1..847abcc2 100644 --- a/src/main/java/org/asamk/signal/util/Util.java +++ b/src/main/java/org/asamk/signal/util/Util.java @@ -3,6 +3,10 @@ package org.asamk.signal.util; import com.fasterxml.jackson.databind.JsonNode; import org.asamk.signal.GroupIdFormatException; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.InvalidNumberException; +import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; +import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.util.Base64; import java.io.IOException; @@ -51,4 +55,16 @@ public class Util { throw new GroupIdFormatException(groupId, e); } } + + public static String canonicalizeNumber(String number, String localNumber) throws InvalidNumberException { + return PhoneNumberFormatter.formatNumber(number, localNumber); + } + + public static SignalServiceAddress getSignalServiceAddressFromIdentifier(final String identifier) { + if (UuidUtil.isUuid(identifier)) { + return new SignalServiceAddress(UuidUtil.parseOrNull(identifier), null); + } else { + return new SignalServiceAddress(null, identifier); + } + } }