From e83bfb9e037d5a55d24b6f6f83efe27edb8dd27e Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 1 Sep 2021 20:02:20 +0200 Subject: [PATCH 01/10] Print more information for call messages --- .../java/org/asamk/signal/ReceiveMessageHandler.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 96603b76..4a516197 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -235,6 +235,13 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { final var deviceId = callMessage.getDestinationDeviceId().get(); writer.println("Destination device id: {}", deviceId); } + if (callMessage.getGroupId().isPresent()) { + final var groupId = GroupId.unknownVersion(callMessage.getGroupId().get()); + writer.println("Destination group id: {}", groupId); + } + if (callMessage.getTimestamp().isPresent()) { + writer.println("Timestamp: {}", DateUtils.formatTimestamp(callMessage.getTimestamp().get())); + } if (callMessage.getAnswerMessage().isPresent()) { var answerMessage = callMessage.getAnswerMessage().get(); writer.println("Answer message: {}, sdp: {})", answerMessage.getId(), answerMessage.getSdp()); @@ -260,7 +267,9 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { } if (callMessage.getOpaqueMessage().isPresent()) { final var opaqueMessage = callMessage.getOpaqueMessage().get(); - writer.println("Opaque message: size {}", opaqueMessage.getOpaque().length); + writer.println("Opaque message: size {}, urgency: {}", + opaqueMessage.getOpaque().length, + opaqueMessage.getUrgency().name()); } } From b9031024078d00e89e7a3462665972dbdf52951b Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 3 Sep 2021 20:12:59 +0200 Subject: [PATCH 02/10] Update libsignal-service-java --- graalvm-config-dir/reflect-config.json | 1 - lib/build.gradle.kts | 2 +- .../signal/manager/RegistrationManager.java | 77 +++++++++++++------ .../signal/manager/config/SandboxConfig.java | 2 +- .../signal/manager/helper/SendHelper.java | 11 +++ 5 files changed, 66 insertions(+), 27 deletions(-) diff --git a/graalvm-config-dir/reflect-config.json b/graalvm-config-dir/reflect-config.json index aef740aa..2db7538b 100644 --- a/graalvm-config-dir/reflect-config.json +++ b/graalvm-config-dir/reflect-config.json @@ -2272,7 +2272,6 @@ "fields":[ {"name":"bitField0_"}, {"name":"e164_"}, - {"name":"relay_"}, {"name":"uuid_"} ] }, diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 316ce564..dc6c910e 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -14,7 +14,7 @@ repositories { } dependencies { - api("com.github.turasa:signal-service-java:2.15.3_unofficial_26") + api("com.github.turasa:signal-service-java:2.15.3_unofficial_27") implementation("com.google.protobuf:protobuf-javalite:3.10.0") implementation("org.bouncycastle:bcprov-jdk15on:1.69") implementation("org.slf4j:slf4j-api:1.7.30") 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 95d43fd6..2be3f719 100644 --- a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java @@ -35,7 +35,9 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.UuidUtil; +import org.whispersystems.signalservice.internal.ServiceResponse; import org.whispersystems.signalservice.internal.push.LockedException; +import org.whispersystems.signalservice.internal.push.RequestVerificationCodeResponse; import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; @@ -115,13 +117,19 @@ public class RegistrationManager implements Closeable { } public void register(boolean voiceVerification, String captcha) throws IOException { + final ServiceResponse response; if (voiceVerification) { - accountManager.requestVoiceVerificationCode(getDefaultLocale(), + response = accountManager.requestVoiceVerificationCode(getDefaultLocale(), Optional.fromNullable(captcha), + Optional.absent(), Optional.absent()); } else { - accountManager.requestSmsVerificationCode(false, Optional.fromNullable(captcha), Optional.absent()); + response = accountManager.requestSmsVerificationCode(false, + Optional.fromNullable(captcha), + Optional.absent(), + Optional.absent()); } + handleResponseException(response); } private Locale getDefaultLocale() { @@ -143,7 +151,7 @@ public class RegistrationManager implements Closeable { VerifyAccountResponse response; MasterKey masterKey; try { - response = verifyAccountWithCode(verificationCode, null, null); + response = verifyAccountWithCode(verificationCode, null); masterKey = null; pin = null; @@ -154,17 +162,16 @@ public class RegistrationManager implements Closeable { var registrationLockData = pinHelper.getRegistrationLockData(pin, e); if (registrationLockData == null) { - response = verifyAccountWithCode(verificationCode, pin, null); - masterKey = null; - } else { - var registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock(); - try { - response = verifyAccountWithCode(verificationCode, null, registrationLock); - } catch (LockedException _e) { - throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!"); - } - masterKey = registrationLockData.getMasterKey(); + throw e; } + + var registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock(); + try { + response = verifyAccountWithCode(verificationCode, registrationLock); + } catch (LockedException _e) { + throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!"); + } + masterKey = registrationLockData.getMasterKey(); } // TODO response.isStorageCapable() @@ -192,18 +199,29 @@ public class RegistrationManager implements Closeable { } private VerifyAccountResponse verifyAccountWithCode( - final String verificationCode, final String legacyPin, final String registrationLock + final String verificationCode, final String registrationLock ) throws IOException { - return accountManager.verifyAccountWithCode(verificationCode, - null, - account.getLocalRegistrationId(), - true, - legacyPin, - registrationLock, - account.getSelfUnidentifiedAccessKey(), - account.isUnrestrictedUnidentifiedAccess(), - ServiceConfig.capabilities, - account.isDiscoverableByPhoneNumber()); + final ServiceResponse response; + if (registrationLock == null) { + response = accountManager.verifyAccount(verificationCode, + account.getLocalRegistrationId(), + true, + account.getSelfUnidentifiedAccessKey(), + account.isUnrestrictedUnidentifiedAccess(), + ServiceConfig.capabilities, + account.isDiscoverableByPhoneNumber()); + } else { + response = accountManager.verifyAccountWithRegistrationLockPin(verificationCode, + account.getLocalRegistrationId(), + true, + registrationLock, + account.getSelfUnidentifiedAccessKey(), + account.isUnrestrictedUnidentifiedAccess(), + ServiceConfig.capabilities, + account.isDiscoverableByPhoneNumber()); + } + handleResponseException(response); + return response.getResult().get(); } @Override @@ -213,4 +231,15 @@ public class RegistrationManager implements Closeable { account = null; } } + + private void handleResponseException(final ServiceResponse response) throws IOException { + final var throwableOptional = response.getExecutionError().or(response.getApplicationError()); + if (throwableOptional.isPresent()) { + if (throwableOptional.get() instanceof IOException) { + throw (IOException) throwableOptional.get(); + } else { + throw new IOException(throwableOptional.get()); + } + } + } } diff --git a/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java index 12d87cf5..bedec52c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java @@ -29,7 +29,7 @@ class SandboxConfig { private final static String KEY_BACKUP_ENCLAVE_NAME = "823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9"; private final static byte[] KEY_BACKUP_SERVICE_ID = Hex.decode( - "51a56084c0b21c6b8f62b1bc792ec9bedac4c7c3964bb08ddcab868158c09982"); + "16b94ac6d2b7f7b9d72928f36d798dbb35ed32e7bb14c42b4301ad0344b46f29"); private final static String KEY_BACKUP_MRENCLAVE = "a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87"; private final static String URL = "https://chat.staging.signal.org"; diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java index da901a3d..dd07fc55 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -58,6 +58,16 @@ public class SendHelper { } }; + private final SignalServiceMessageSender.LegacyGroupEvents legacyGroupEvents = new SignalServiceMessageSender.LegacyGroupEvents() { + @Override + public void onMessageSent() { + } + + @Override + public void onSyncMessageSent() { + } + }; + public SendHelper( final SignalAccount account, final SignalDependencies dependencies, @@ -267,6 +277,7 @@ public class SendHelper { isRecipientUpdate, ContentHint.DEFAULT, message, + legacyGroupEvents, sendResult -> logger.trace("Partial message send result: {}", sendResult.isSuccess()), () -> false); } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) { From 43bcc95713f14bdf6103d63d2daed66c3e7df502 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 3 Sep 2021 21:30:45 +0200 Subject: [PATCH 03/10] Add missing isActive check --- .../manager/storage/sessions/SessionStore.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) 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 5738408d..1b94642a 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 @@ -109,11 +109,7 @@ public class SessionStore implements SignalServiceSessionStore { synchronized (cachedSessions) { final var session = loadSessionLocked(key); - if (session == null) { - return false; - } - - return session.hasSenderChain() && session.getSessionVersion() == CiphertextMessage.CURRENT_VERSION; + return isActive(session); } } @@ -158,6 +154,7 @@ public class SessionStore implements SignalServiceSessionStore { return recipientIdToNameMap.keySet() .stream() .flatMap(recipientId -> getKeysLocked(recipientId).stream()) + .filter(key -> isActive(this.loadSessionLocked(key))) .map(key -> new SignalProtocolAddress(recipientIdToNameMap.get(key.recipientId), key.getDeviceId())) .collect(Collectors.toSet()); } @@ -321,6 +318,12 @@ public class SessionStore implements SignalServiceSessionStore { } } + private static boolean isActive(SessionRecord record) { + return record != null + && record.hasSenderChain() + && record.getSessionVersion() == CiphertextMessage.CURRENT_VERSION; + } + private static final class Key { private final RecipientId recipientId; From 891c05210e947db28ce231266893206b4d22eec0 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 4 Sep 2021 10:48:22 +0200 Subject: [PATCH 04/10] Improve comment in SessionStore --- .../asamk/signal/manager/storage/sessions/SessionStore.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 1b94642a..bae4fdf3 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 @@ -179,7 +179,8 @@ public class SessionStore implements SignalServiceSessionStore { public void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) { synchronized (cachedSessions) { - final var otherHasSession = getKeysLocked(toBeMergedRecipientId).size() > 0; + final var keys = getKeysLocked(toBeMergedRecipientId); + final var otherHasSession = keys.size() > 0; if (!otherHasSession) { return; } @@ -189,8 +190,7 @@ public class SessionStore implements SignalServiceSessionStore { logger.debug("To be merged recipient had sessions, deleting."); deleteAllSessions(toBeMergedRecipientId); } else { - logger.debug("To be merged recipient had sessions, re-assigning to the new recipient."); - final var keys = getKeysLocked(toBeMergedRecipientId); + logger.debug("Only to be merged recipient had sessions, re-assigning to the new recipient."); for (var key : keys) { final var session = loadSessionLocked(key); deleteSessionLocked(key); From 35622ac6840defeecbbe382de3f9a0b81df72a1d Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 4 Sep 2021 13:26:52 +0200 Subject: [PATCH 05/10] Use EMPTY send event listeners --- .../signal/manager/helper/SendHelper.java | 30 ++----------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java index dd07fc55..6ebc0254 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -44,30 +44,6 @@ public class SendHelper { private final GroupProvider groupProvider; private final RecipientRegistrationRefresher recipientRegistrationRefresher; - private final SignalServiceMessageSender.IndividualSendEvents sendEvents = new SignalServiceMessageSender.IndividualSendEvents() { - @Override - public void onMessageEncrypted() { - } - - @Override - public void onMessageSent() { - } - - @Override - public void onSyncMessageSent() { - } - }; - - private final SignalServiceMessageSender.LegacyGroupEvents legacyGroupEvents = new SignalServiceMessageSender.LegacyGroupEvents() { - @Override - public void onMessageSent() { - } - - @Override - public void onSyncMessageSent() { - } - }; - public SendHelper( final SignalAccount account, final SignalDependencies dependencies, @@ -277,7 +253,7 @@ public class SendHelper { isRecipientUpdate, ContentHint.DEFAULT, message, - legacyGroupEvents, + SignalServiceMessageSender.LegacyGroupEvents.EMPTY, sendResult -> logger.trace("Partial message send result: {}", sendResult.isSuccess()), () -> false); } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) { @@ -297,14 +273,14 @@ public class SendHelper { unidentifiedAccessHelper.getAccessFor(recipientId), ContentHint.DEFAULT, message, - sendEvents); + SignalServiceMessageSender.IndividualSendEvents.EMPTY); } catch (UnregisteredUserException e) { final var newRecipientId = recipientRegistrationRefresher.refreshRecipientRegistration(recipientId); return messageSender.sendDataMessage(addressResolver.resolveSignalServiceAddress(newRecipientId), unidentifiedAccessHelper.getAccessFor(newRecipientId), ContentHint.DEFAULT, message, - sendEvents); + SignalServiceMessageSender.IndividualSendEvents.EMPTY); } } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) { return SendMessageResult.identityFailure(address, e.getIdentityKey()); From ac18006abb2538b97c4d1e9ad658795748264d90 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 3 Sep 2021 22:38:45 +0200 Subject: [PATCH 06/10] Implement support for receiving sender key messages --- .../signal/manager/config/ServiceConfig.java | 7 +- .../helper/IncomingMessageHandler.java | 11 + .../helper/RecipientAddressResolver.java | 9 + .../signal/manager/storage/SignalAccount.java | 22 ++ .../storage/protocol/SignalProtocolStore.java | 16 +- .../senderKeys/SenderKeyRecordStore.java | 261 +++++++++++++++++ .../senderKeys/SenderKeySharedStore.java | 270 ++++++++++++++++++ .../storage/senderKeys/SenderKeyStore.java | 75 +++++ .../asamk/signal/ReceiveMessageHandler.java | 6 + 9 files changed, 664 insertions(+), 13 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/helper/RecipientAddressResolver.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeySharedStore.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyStore.java diff --git a/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java index 3f97be6b..5324439b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java @@ -34,12 +34,7 @@ public class ServiceConfig { } catch (Throwable ignored) { zkGroupAvailable = false; } - capabilities = new AccountAttributes.Capabilities(false, - zkGroupAvailable, - false, - zkGroupAvailable, - false, - true); + capabilities = new AccountAttributes.Capabilities(false, zkGroupAvailable, false, zkGroupAvailable, true, true); try { TrustStore contactTrustStore = new IasTrustStore(); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index 57b71ee1..e6e43478 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -30,6 +30,7 @@ import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.profiles.ProfileKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; @@ -173,10 +174,20 @@ public final class IncomingMessageHandler { ) { var actions = new ArrayList(); final RecipientId sender; + final int senderDeviceId; if (!envelope.isUnidentifiedSender() && envelope.hasSourceUuid()) { sender = recipientResolver.resolveRecipient(envelope.getSourceAddress()); + senderDeviceId = envelope.getSourceDevice(); } else { sender = recipientResolver.resolveRecipient(content.getSender()); + senderDeviceId = content.getSenderDevice(); + } + + if (content.getSenderKeyDistributionMessage().isPresent()) { + final var message = content.getSenderKeyDistributionMessage().get(); + final var protocolAddress = new SignalProtocolAddress(addressResolver.resolveSignalServiceAddress(sender) + .getIdentifier(), senderDeviceId); + dependencies.getMessageSender().processSenderKeyDistributionMessage(protocolAddress, message); } if (content.getDataMessage().isPresent()) { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/RecipientAddressResolver.java b/lib/src/main/java/org/asamk/signal/manager/helper/RecipientAddressResolver.java new file mode 100644 index 00000000..e2c10f4e --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/RecipientAddressResolver.java @@ -0,0 +1,9 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.storage.recipients.RecipientAddress; +import org.asamk.signal.manager.storage.recipients.RecipientId; + +public interface RecipientAddressResolver { + + RecipientAddress resolveRecipientAddress(RecipientId recipientId); +} 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 4e240887..e75996c5 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 @@ -24,6 +24,7 @@ import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientStore; +import org.asamk.signal.manager.storage.senderKeys.SenderKeyStore; import org.asamk.signal.manager.storage.sessions.SessionStore; import org.asamk.signal.manager.storage.stickers.StickerStore; import org.asamk.signal.manager.storage.threads.LegacyJsonThreadStore; @@ -95,6 +96,7 @@ public class SignalAccount implements Closeable { private SignedPreKeyStore signedPreKeyStore; private SessionStore sessionStore; private IdentityKeyStore identityKeyStore; + private SenderKeyStore senderKeyStore; private GroupStore groupStore; private GroupStore.Storage groupStoreStorage; private RecipientStore recipientStore; @@ -181,10 +183,15 @@ public class SignalAccount implements Closeable { identityKey, registrationId, trustNewIdentity); + senderKeyStore = new SenderKeyStore(getSharedSenderKeysFile(dataPath, username), + getSenderKeysPath(dataPath, username), + recipientStore::resolveRecipientAddress, + recipientStore); signalProtocolStore = new SignalProtocolStore(preKeyStore, signedPreKeyStore, sessionStore, identityKeyStore, + senderKeyStore, this::isMultiDevice); messageCache = new MessageCache(getMessageCachePath(dataPath, username)); @@ -221,6 +228,7 @@ public class SignalAccount implements Closeable { account.setProvisioningData(username, uuid, password, encryptedDeviceName, deviceId, profileKey); account.recipientStore.resolveRecipientTrusted(account.getSelfAddress()); account.sessionStore.archiveAllSessions(); + account.senderKeyStore.deleteAll(); account.clearAllPreKeys(); return account; } @@ -303,6 +311,7 @@ public class SignalAccount implements Closeable { identityKeyStore.mergeRecipients(recipientId, toBeMergedRecipientId); messageCache.mergeRecipients(recipientId, toBeMergedRecipientId); groupStore.mergeRecipients(recipientId, toBeMergedRecipientId); + senderKeyStore.mergeRecipients(recipientId, toBeMergedRecipientId); } public static File getFileName(File dataPath, String username) { @@ -343,6 +352,14 @@ public class SignalAccount implements Closeable { return new File(getUserPath(dataPath, username), "sessions"); } + private static File getSenderKeysPath(File dataPath, String username) { + return new File(getUserPath(dataPath, username), "sender-keys"); + } + + private static File getSharedSenderKeysFile(File dataPath, String username) { + return new File(getUserPath(dataPath, username), "shared-sender-keys-store"); + } + private static File getRecipientsStoreFile(File dataPath, String username) { return new File(getUserPath(dataPath, username), "recipients-store"); } @@ -768,6 +785,10 @@ public class SignalAccount implements Closeable { return stickerStore; } + public SenderKeyStore getSenderKeyStore() { + return senderKeyStore; + } + public MessageCache getMessageCache() { return messageCache; } @@ -932,6 +953,7 @@ public class SignalAccount implements Closeable { save(); getSessionStore().archiveAllSessions(); + senderKeyStore.deleteAll(); final var recipientId = getRecipientStore().resolveRecipientTrusted(getSelfAddress()); final var publicKey = getIdentityKeyPair().getPublicKey(); getIdentityKeyStore().saveIdentity(recipientId, publicKey, new Date()); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalProtocolStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalProtocolStore.java index 77eb764a..7f200459 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalProtocolStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalProtocolStore.java @@ -13,6 +13,7 @@ import org.whispersystems.libsignal.state.SessionRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyStore; import org.whispersystems.signalservice.api.SignalServiceDataStore; +import org.whispersystems.signalservice.api.SignalServiceSenderKeyStore; import org.whispersystems.signalservice.api.SignalServiceSessionStore; import org.whispersystems.signalservice.api.push.DistributionId; @@ -28,6 +29,7 @@ public class SignalProtocolStore implements SignalServiceDataStore { private final SignedPreKeyStore signedPreKeyStore; private final SignalServiceSessionStore sessionStore; private final IdentityKeyStore identityKeyStore; + private final SignalServiceSenderKeyStore senderKeyStore; private final Supplier isMultiDevice; public SignalProtocolStore( @@ -35,12 +37,14 @@ public class SignalProtocolStore implements SignalServiceDataStore { final SignedPreKeyStore signedPreKeyStore, final SignalServiceSessionStore sessionStore, final IdentityKeyStore identityKeyStore, + final SignalServiceSenderKeyStore senderKeyStore, final Supplier isMultiDevice ) { this.preKeyStore = preKeyStore; this.signedPreKeyStore = signedPreKeyStore; this.sessionStore = sessionStore; this.identityKeyStore = identityKeyStore; + this.senderKeyStore = senderKeyStore; this.isMultiDevice = isMultiDevice; } @@ -163,31 +167,29 @@ public class SignalProtocolStore implements SignalServiceDataStore { public void storeSenderKey( final SignalProtocolAddress sender, final UUID distributionId, final SenderKeyRecord record ) { - // TODO + senderKeyStore.storeSenderKey(sender, distributionId, record); } @Override public SenderKeyRecord loadSenderKey(final SignalProtocolAddress sender, final UUID distributionId) { - // TODO - return null; + return senderKeyStore.loadSenderKey(sender, distributionId); } @Override public Set getSenderKeySharedWith(final DistributionId distributionId) { - // TODO - return null; + return senderKeyStore.getSenderKeySharedWith(distributionId); } @Override public void markSenderKeySharedWith( final DistributionId distributionId, final Collection addresses ) { - // TODO + senderKeyStore.markSenderKeySharedWith(distributionId, addresses); } @Override public void clearSenderKeySharedWith(final Collection addresses) { - // TODO + senderKeyStore.clearSenderKeySharedWith(addresses); } @Override diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java new file mode 100644 index 00000000..f84903e4 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java @@ -0,0 +1,261 @@ +package org.asamk.signal.manager.storage.senderKeys; + +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.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.SignalProtocolAddress; +import org.whispersystems.libsignal.groups.state.SenderKeyRecord; + +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.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class SenderKeyRecordStore implements org.whispersystems.libsignal.groups.state.SenderKeyStore { + + private final static Logger logger = LoggerFactory.getLogger(SenderKeyRecordStore.class); + + private final Map cachedSenderKeys = new HashMap<>(); + + private final File senderKeysPath; + + private final RecipientResolver resolver; + + public SenderKeyRecordStore( + final File senderKeysPath, final RecipientResolver resolver + ) { + this.senderKeysPath = senderKeysPath; + this.resolver = resolver; + } + + @Override + public SenderKeyRecord loadSenderKey(final SignalProtocolAddress address, final UUID distributionId) { + final var key = getKey(address, distributionId); + + synchronized (cachedSenderKeys) { + return loadSenderKeyLocked(key); + } + } + + @Override + public void storeSenderKey( + final SignalProtocolAddress address, final UUID distributionId, final SenderKeyRecord record + ) { + final var key = getKey(address, distributionId); + + synchronized (cachedSenderKeys) { + storeSenderKeyLocked(key, record); + } + } + + public void deleteAll() { + synchronized (cachedSenderKeys) { + cachedSenderKeys.clear(); + final var files = senderKeysPath.listFiles((_file, s) -> senderKeyFileNamePattern.matcher(s).matches()); + if (files == null) { + return; + } + + for (final var file : files) { + try { + Files.delete(file.toPath()); + } catch (IOException e) { + logger.error("Failed to delete sender key file {}: {}", file, e.getMessage()); + } + } + } + } + + public void deleteAllFor(final RecipientId recipientId) { + synchronized (cachedSenderKeys) { + cachedSenderKeys.clear(); + final var keys = getKeysLocked(recipientId); + for (var key : keys) { + deleteSenderKeyLocked(key); + } + } + } + + public void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) { + synchronized (cachedSenderKeys) { + final var keys = getKeysLocked(toBeMergedRecipientId); + final var otherHasSenderKeys = keys.size() > 0; + if (!otherHasSenderKeys) { + return; + } + + logger.debug("Only to be merged recipient had sender keys, re-assigning to the new recipient."); + for (var key : keys) { + final var toBeMergedSenderKey = loadSenderKeyLocked(key); + deleteSenderKeyLocked(key); + if (toBeMergedSenderKey == null) { + continue; + } + + final var newKey = new Key(recipientId, key.getDeviceId(), key.distributionId); + final var senderKeyRecord = loadSenderKeyLocked(newKey); + if (senderKeyRecord != null) { + continue; + } + storeSenderKeyLocked(newKey, senderKeyRecord); + } + } + } + + /** + * @param identifier can be either a serialized uuid or a e164 phone number + */ + private RecipientId resolveRecipient(String identifier) { + return resolver.resolveRecipient(identifier); + } + + private Key getKey(final SignalProtocolAddress address, final UUID distributionId) { + final var recipientId = resolveRecipient(address.getName()); + return new Key(recipientId, address.getDeviceId(), distributionId); + } + + private List getKeysLocked(RecipientId recipientId) { + final var files = senderKeysPath.listFiles((_file, s) -> s.startsWith(recipientId.getId() + "_")); + if (files == null) { + return List.of(); + } + return parseFileNames(files); + } + + final Pattern senderKeyFileNamePattern = Pattern.compile("([0-9]+)_([0-9]+)_([0-9a-z\\-]+)"); + + private List parseFileNames(final File[] files) { + return Arrays.stream(files) + .map(f -> senderKeyFileNamePattern.matcher(f.getName())) + .filter(Matcher::matches) + .map(matcher -> new Key(RecipientId.of(Long.parseLong(matcher.group(1))), + Integer.parseInt(matcher.group(2)), + UUID.fromString(matcher.group(3)))) + .collect(Collectors.toList()); + } + + private File getSenderKeyFile(Key key) { + try { + IOUtils.createPrivateDirectories(senderKeysPath); + } catch (IOException e) { + throw new AssertionError("Failed to create sender keys path", e); + } + return new File(senderKeysPath, + key.getRecipientId().getId() + "_" + key.getDeviceId() + "_" + key.distributionId.toString()); + } + + private SenderKeyRecord loadSenderKeyLocked(final Key key) { + { + final var senderKeyRecord = cachedSenderKeys.get(key); + if (senderKeyRecord != null) { + return senderKeyRecord; + } + } + + final var file = getSenderKeyFile(key); + if (!file.exists()) { + return null; + } + try (var inputStream = new FileInputStream(file)) { + final var senderKeyRecord = new SenderKeyRecord(inputStream.readAllBytes()); + cachedSenderKeys.put(key, senderKeyRecord); + return senderKeyRecord; + } catch (IOException e) { + logger.warn("Failed to load sender key, resetting sender key: {}", e.getMessage()); + return null; + } + } + + private void storeSenderKeyLocked(final Key key, final SenderKeyRecord senderKeyRecord) { + cachedSenderKeys.put(key, senderKeyRecord); + + final var file = getSenderKeyFile(key); + try { + try (var outputStream = new FileOutputStream(file)) { + outputStream.write(senderKeyRecord.serialize()); + } + } catch (IOException e) { + logger.warn("Failed to store sender key, trying to delete file and retry: {}", e.getMessage()); + try { + Files.delete(file.toPath()); + try (var outputStream = new FileOutputStream(file)) { + outputStream.write(senderKeyRecord.serialize()); + } + } catch (IOException e2) { + logger.error("Failed to store sender key file {}: {}", file, e2.getMessage()); + } + } + } + + private void deleteSenderKeyLocked(final Key key) { + cachedSenderKeys.remove(key); + + final var file = getSenderKeyFile(key); + if (!file.exists()) { + return; + } + try { + Files.delete(file.toPath()); + } catch (IOException e) { + logger.error("Failed to delete sender key file {}: {}", file, e.getMessage()); + } + } + + private static final class Key { + + private final RecipientId recipientId; + private final int deviceId; + private final UUID distributionId; + + public Key( + final RecipientId recipientId, final int deviceId, final UUID distributionId + ) { + this.recipientId = recipientId; + this.deviceId = deviceId; + this.distributionId = distributionId; + } + + public RecipientId getRecipientId() { + return recipientId; + } + + public int getDeviceId() { + return deviceId; + } + + public UUID getDistributionId() { + return distributionId; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final Key key = (Key) o; + + if (deviceId != key.deviceId) return false; + if (!recipientId.equals(key.recipientId)) return false; + return distributionId.equals(key.distributionId); + } + + @Override + public int hashCode() { + int result = recipientId.hashCode(); + result = 31 * result + deviceId; + result = 31 * result + distributionId.hashCode(); + return result; + } + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeySharedStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeySharedStore.java new file mode 100644 index 00000000..3faf2e74 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeySharedStore.java @@ -0,0 +1,270 @@ +package org.asamk.signal.manager.storage.senderKeys; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.asamk.signal.manager.helper.RecipientAddressResolver; +import org.asamk.signal.manager.storage.Utils; +import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.asamk.signal.manager.storage.recipients.RecipientResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.SignalProtocolAddress; +import org.whispersystems.signalservice.api.push.DistributionId; +import org.whispersystems.signalservice.api.util.UuidUtil; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class SenderKeySharedStore { + + private final static Logger logger = LoggerFactory.getLogger(SenderKeySharedStore.class); + + private final Map> sharedSenderKeys; + + private final ObjectMapper objectMapper; + private final File file; + + private final RecipientResolver resolver; + private final RecipientAddressResolver addressResolver; + + public static SenderKeySharedStore load( + final File file, final RecipientAddressResolver addressResolver, final RecipientResolver resolver + ) throws IOException { + final var objectMapper = Utils.createStorageObjectMapper(); + try (var inputStream = new FileInputStream(file)) { + final var storage = objectMapper.readValue(inputStream, Storage.class); + final var sharedSenderKeys = new HashMap>(); + for (final var senderKey : storage.sharedSenderKeys) { + final var entry = new SenderKeySharedEntry(RecipientId.of(senderKey.recipientId), senderKey.deviceId); + final var uuid = UuidUtil.parseOrNull(senderKey.distributionId); + if (uuid == null) { + logger.warn("Read invalid distribution id from storage {}, ignoring", senderKey.distributionId); + continue; + } + final var distributionId = DistributionId.from(uuid); + var entries = sharedSenderKeys.get(distributionId); + if (entries == null) { + entries = new HashSet<>(); + } + entries.add(entry); + sharedSenderKeys.put(distributionId, entries); + } + + return new SenderKeySharedStore(sharedSenderKeys, objectMapper, file, addressResolver, resolver); + } catch (FileNotFoundException e) { + logger.debug("Creating new shared sender key store."); + return new SenderKeySharedStore(new HashMap<>(), objectMapper, file, addressResolver, resolver); + } + } + + private SenderKeySharedStore( + final Map> sharedSenderKeys, + final ObjectMapper objectMapper, + final File file, + final RecipientAddressResolver addressResolver, + final RecipientResolver resolver + ) { + this.sharedSenderKeys = sharedSenderKeys; + this.objectMapper = objectMapper; + this.file = file; + this.addressResolver = addressResolver; + this.resolver = resolver; + } + + public Set getSenderKeySharedWith(final DistributionId distributionId) { + synchronized (sharedSenderKeys) { + return sharedSenderKeys.get(distributionId) + .stream() + .map(k -> new SignalProtocolAddress(addressResolver.resolveRecipientAddress(k.getRecipientId()) + .getIdentifier(), k.getDeviceId())) + .collect(Collectors.toSet()); + } + } + + public void markSenderKeySharedWith( + final DistributionId distributionId, final Collection addresses + ) { + final var newEntries = addresses.stream() + .map(a -> new SenderKeySharedEntry(resolveRecipient(a.getName()), a.getDeviceId())) + .collect(Collectors.toSet()); + + synchronized (sharedSenderKeys) { + final var previousEntries = sharedSenderKeys.getOrDefault(distributionId, Set.of()); + + sharedSenderKeys.put(distributionId, new HashSet<>() { + { + addAll(previousEntries); + addAll(newEntries); + } + }); + saveLocked(); + } + } + + public void clearSenderKeySharedWith(final Collection addresses) { + final var entriesToDelete = addresses.stream() + .map(a -> new SenderKeySharedEntry(resolveRecipient(a.getName()), a.getDeviceId())) + .collect(Collectors.toSet()); + + synchronized (sharedSenderKeys) { + for (final var distributionId : sharedSenderKeys.keySet()) { + final var entries = sharedSenderKeys.getOrDefault(distributionId, Set.of()); + + sharedSenderKeys.put(distributionId, new HashSet<>(entries) { + { + removeAll(entriesToDelete); + } + }); + } + saveLocked(); + } + } + + public void deleteAll() { + synchronized (sharedSenderKeys) { + sharedSenderKeys.clear(); + saveLocked(); + } + } + + public void deleteAllFor(final RecipientId recipientId) { + synchronized (sharedSenderKeys) { + for (final var distributionId : sharedSenderKeys.keySet()) { + final var entries = sharedSenderKeys.getOrDefault(distributionId, Set.of()); + + sharedSenderKeys.put(distributionId, new HashSet<>(entries) { + { + entries.removeIf(e -> e.getRecipientId().equals(recipientId)); + } + }); + } + saveLocked(); + } + } + + public void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) { + synchronized (sharedSenderKeys) { + for (final var distributionId : sharedSenderKeys.keySet()) { + final var entries = sharedSenderKeys.getOrDefault(distributionId, Set.of()); + + sharedSenderKeys.put(distributionId, + entries.stream() + .map(e -> e.recipientId.equals(toBeMergedRecipientId) ? new SenderKeySharedEntry( + recipientId, + e.getDeviceId()) : e) + .collect(Collectors.toSet())); + } + saveLocked(); + } + } + + /** + * @param identifier can be either a serialized uuid or a e164 phone number + */ + private RecipientId resolveRecipient(String identifier) { + return resolver.resolveRecipient(identifier); + } + + private void saveLocked() { + var storage = new Storage(sharedSenderKeys.entrySet().stream().flatMap(pair -> { + final var sharedWith = pair.getValue(); + return sharedWith.stream() + .map(entry -> new Storage.SharedSenderKey(entry.getRecipientId().getId(), + entry.getDeviceId(), + pair.getKey().asUuid().toString())); + }).collect(Collectors.toList())); + + // 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 shared sender key store file: {}", e.getMessage()); + } + } + + private static class Storage { + + public List sharedSenderKeys; + + // For deserialization + private Storage() { + } + + public Storage(final List sharedSenderKeys) { + this.sharedSenderKeys = sharedSenderKeys; + } + + private static class SharedSenderKey { + + public long recipientId; + public int deviceId; + public String distributionId; + + // For deserialization + private SharedSenderKey() { + } + + public SharedSenderKey(final long recipientId, final int deviceId, final String distributionId) { + this.recipientId = recipientId; + this.deviceId = deviceId; + this.distributionId = distributionId; + } + } + } + + private static final class SenderKeySharedEntry { + + private final RecipientId recipientId; + private final int deviceId; + + public SenderKeySharedEntry( + final RecipientId recipientId, final int deviceId + ) { + this.recipientId = recipientId; + this.deviceId = deviceId; + } + + public RecipientId getRecipientId() { + return recipientId; + } + + public int getDeviceId() { + return deviceId; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final SenderKeySharedEntry that = (SenderKeySharedEntry) o; + + if (deviceId != that.deviceId) return false; + return recipientId.equals(that.recipientId); + } + + @Override + public int hashCode() { + int result = recipientId.hashCode(); + result = 31 * result + deviceId; + return result; + } + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyStore.java new file mode 100644 index 00000000..ab02d755 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyStore.java @@ -0,0 +1,75 @@ +package org.asamk.signal.manager.storage.senderKeys; + +import org.asamk.signal.manager.helper.RecipientAddressResolver; +import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.asamk.signal.manager.storage.recipients.RecipientResolver; +import org.whispersystems.libsignal.SignalProtocolAddress; +import org.whispersystems.libsignal.groups.state.SenderKeyRecord; +import org.whispersystems.signalservice.api.SignalServiceSenderKeyStore; +import org.whispersystems.signalservice.api.push.DistributionId; + +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.Set; +import java.util.UUID; + +public class SenderKeyStore implements SignalServiceSenderKeyStore { + + private final SenderKeyRecordStore senderKeyRecordStore; + private final SenderKeySharedStore senderKeySharedStore; + + public SenderKeyStore( + final File file, + final File senderKeysPath, + final RecipientAddressResolver addressResolver, + final RecipientResolver resolver + ) throws IOException { + this.senderKeyRecordStore = new SenderKeyRecordStore(senderKeysPath, resolver); + this.senderKeySharedStore = SenderKeySharedStore.load(file, addressResolver, resolver); + } + + @Override + public void storeSenderKey( + final SignalProtocolAddress sender, final UUID distributionId, final SenderKeyRecord record + ) { + senderKeyRecordStore.storeSenderKey(sender, distributionId, record); + } + + @Override + public SenderKeyRecord loadSenderKey(final SignalProtocolAddress sender, final UUID distributionId) { + return senderKeyRecordStore.loadSenderKey(sender, distributionId); + } + + @Override + public Set getSenderKeySharedWith(final DistributionId distributionId) { + return senderKeySharedStore.getSenderKeySharedWith(distributionId); + } + + @Override + public void markSenderKeySharedWith( + final DistributionId distributionId, final Collection addresses + ) { + senderKeySharedStore.markSenderKeySharedWith(distributionId, addresses); + } + + @Override + public void clearSenderKeySharedWith(final Collection addresses) { + senderKeySharedStore.clearSenderKeySharedWith(addresses); + } + + public void deleteAll() { + senderKeySharedStore.deleteAll(); + senderKeyRecordStore.deleteAll(); + } + + public void rotateSenderKeys(RecipientId recipientId) { + senderKeySharedStore.deleteAllFor(recipientId); + senderKeyRecordStore.deleteAllFor(recipientId); + } + + public void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) { + senderKeySharedStore.mergeRecipients(recipientId, toBeMergedRecipientId); + senderKeyRecordStore.mergeRecipients(recipientId, toBeMergedRecipientId); + } +} diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 4a516197..15dbd1af 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -82,6 +82,12 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { DateUtils.formatTimestamp(content.getServerReceivedTimestamp()), DateUtils.formatTimestamp(content.getServerDeliveredTimestamp())); + if (content.getSenderKeyDistributionMessage().isPresent()) { + final var message = content.getSenderKeyDistributionMessage().get(); + writer.println("Received a sender key distribution message for distributionId {}", + message.getDistributionId()); + } + if (content.getDataMessage().isPresent()) { var message = content.getDataMessage().get(); printDataMessage(writer, message); From 5a2e37a6e242b920e5647e3d98c2aecb1932f763 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 4 Sep 2021 15:06:25 +0200 Subject: [PATCH 07/10] Only handle jsonRpc requests, after receive thread has caught up with old messages --- .../org/asamk/signal/manager/Manager.java | 24 +++++++++++++++---- .../commands/JsonRpcDispatcherCommand.java | 10 ++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) 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 c40fa7cd..c4c77b34 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -141,6 +141,7 @@ public class Manager implements Closeable { private final IncomingMessageHandler incomingMessageHandler; private final Context context; + private boolean hasCaughtUpWithOldMessages = false; Manager( SignalAccount account, @@ -865,7 +866,7 @@ public class Manager implements Closeable { final var signalWebSocket = dependencies.getSignalWebSocket(); signalWebSocket.connect(); - var hasCaughtUpWithOldMessages = false; + hasCaughtUpWithOldMessages = false; while (!Thread.interrupted()) { SignalServiceEnvelope envelope; @@ -885,11 +886,14 @@ public class Manager implements Closeable { envelope = result.get(); } else { // Received indicator that server queue is empty - hasCaughtUpWithOldMessages = true; - handleQueuedActions(queuedActions); queuedActions.clear(); + hasCaughtUpWithOldMessages = true; + synchronized (this) { + this.notifyAll(); + } + // Continue to wait another timeout for new messages continue; } @@ -936,17 +940,27 @@ public class Manager implements Closeable { handleQueuedActions(queuedActions); } + public boolean hasCaughtUpWithOldMessages() { + return hasCaughtUpWithOldMessages; + } + private void handleQueuedActions(final Collection queuedActions) { + var interrupted = false; for (var action : queuedActions) { try { action.execute(context); } catch (Throwable e) { - if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { - Thread.currentThread().interrupt(); + if ((e instanceof AssertionError || e instanceof RuntimeException) + && e.getCause() instanceof InterruptedException) { + interrupted = true; + continue; } logger.warn("Message action failed.", e); } } + if (interrupted) { + Thread.currentThread().interrupt(); + } } public boolean isContactBlocked(final RecipientIdentifier.Single recipient) { diff --git a/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java b/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java index 16d0cf71..d0e4dfec 100644 --- a/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java +++ b/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java @@ -75,6 +75,16 @@ public class JsonRpcDispatcherCommand implements LocalCommand { objectMapper.valueToTree(s), null)), m, ignoreAttachments); + // Maybe this should be handled inside the Manager + while (!m.hasCaughtUpWithOldMessages()) { + try { + synchronized (m) { + m.wait(); + } + } catch (InterruptedException ignored) { + } + } + final BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); final var jsonRpcReader = new JsonRpcReader(jsonRpcSender, () -> { From 299671480fb79f0abcc67ec5f9ec89fac9605345 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 5 Sep 2021 11:41:38 +0200 Subject: [PATCH 08/10] Add possibility to update the device name --- CHANGELOG.md | 1 + .../java/org/asamk/signal/manager/Manager.java | 17 ++++++++++++----- .../signal/manager/storage/SignalAccount.java | 5 +++++ man/signal-cli.1.adoc | 3 +++ .../signal/commands/UpdateAccountCommand.java | 4 +++- 5 files changed, 24 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2ba6843..fa27507d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Added - New global parameter `--trust-new-identities=always` to allow trusting any new identity key without verification +- New parameter `--device-name` for `updateAccount` command to update the device name ## [0.8.5] - 2021-08-07 ### Added 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 c4c77b34..366fb371 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -311,7 +311,7 @@ public class Manager implements Closeable { if (account.getUuid() == null) { account.setUuid(dependencies.getAccountManager().getOwnUuid()); } - updateAccountAttributes(); + updateAccountAttributes(null); } /** @@ -343,14 +343,21 @@ public class Manager implements Closeable { })); } - public void updateAccountAttributes() throws IOException { + public void updateAccountAttributes(String deviceName) throws IOException { + final String encryptedDeviceName; + if (deviceName == null) { + encryptedDeviceName = account.getEncryptedDeviceName(); + } else { + final var privateKey = account.getIdentityKeyPair().getPrivateKey(); + encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey); + account.setEncryptedDeviceName(encryptedDeviceName); + } dependencies.getAccountManager() - .setAccountAttributes(account.getEncryptedDeviceName(), + .setAccountAttributes(encryptedDeviceName, null, account.getLocalRegistrationId(), true, - // set legacy pin only if no KBS master key is set - account.getPinMasterKey() == null ? account.getRegistrationLockPin() : null, + null, account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(), account.getSelfUnidentifiedAccessKey(), account.isUnrestrictedUnidentifiedAccess(), 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 e75996c5..efdcf798 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 @@ -818,6 +818,11 @@ public class SignalAccount implements Closeable { return encryptedDeviceName; } + public void setEncryptedDeviceName(final String encryptedDeviceName) { + this.encryptedDeviceName = encryptedDeviceName; + save(); + } + public int getDeviceId() { return deviceId; } diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index b8251eb1..b52612be 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -110,6 +110,9 @@ CAUTION: Only delete your account if you won't use this number again! Update the account attributes on the signal server. Can fix problems with receiving messages. +*-n* NAME, *--device-name* NAME:: +Set a new device name for the main or linked device + === setPin Set a registration lock pin, to prevent others from registering this number. diff --git a/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java b/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java index f2ed6a98..600a38c4 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java @@ -20,14 +20,16 @@ public class UpdateAccountCommand implements JsonRpcLocalCommand { @Override public void attachToSubparser(final Subparser subparser) { subparser.help("Update the account attributes on the signal server."); + subparser.addArgument("-n", "--device-name").help("Specify a name to describe this device."); } @Override public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { + var deviceName = ns.getString("device-name"); try { - m.updateAccountAttributes(); + m.updateAccountAttributes(deviceName); } catch (IOException e) { throw new IOErrorException("UpdateAccount error: " + e.getMessage()); } From 2e01a05e7110b4f94abb10489a28b73d9f4be9c0 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 24 May 2021 16:51:36 +0200 Subject: [PATCH 09/10] Implement retrieving data from remote storage Related #604 --- .../org/asamk/signal/manager/Manager.java | 13 +- .../signal/manager/ProvisioningManager.java | 3 + .../signal/manager/RegistrationManager.java | 4 +- .../org/asamk/signal/manager/TrustLevel.java | 15 ++ .../actions/RetrieveStorageDataAction.java | 26 ++ .../manager/actions/SendSyncKeysAction.java | 20 ++ .../helper/IncomingMessageHandler.java | 10 +- .../signal/manager/helper/StorageHelper.java | 222 ++++++++++++++++++ .../signal/manager/helper/SyncHelper.java | 6 + .../asamk/signal/manager/jobs/Context.java | 10 +- .../signal/manager/storage/SignalAccount.java | 22 ++ 11 files changed, 346 insertions(+), 5 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/actions/RetrieveStorageDataAction.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/actions/SendSyncKeysAction.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java 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 366fb371..a7a691cc 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -42,6 +42,7 @@ import org.asamk.signal.manager.helper.IncomingMessageHandler; import org.asamk.signal.manager.helper.PinHelper; import org.asamk.signal.manager.helper.ProfileHelper; import org.asamk.signal.manager.helper.SendHelper; +import org.asamk.signal.manager.helper.StorageHelper; import org.asamk.signal.manager.helper.SyncHelper; import org.asamk.signal.manager.helper.UnidentifiedAccessHelper; import org.asamk.signal.manager.jobs.Context; @@ -133,6 +134,7 @@ public class Manager implements Closeable { private final ProfileHelper profileHelper; private final PinHelper pinHelper; + private final StorageHelper storageHelper; private final SendHelper sendHelper; private final SyncHelper syncHelper; private final AttachmentHelper attachmentHelper; @@ -209,6 +211,7 @@ public class Manager implements Closeable { avatarStore, this::resolveSignalServiceAddress, account.getRecipientStore()); + this.storageHelper = new StorageHelper(account, dependencies, groupHelper); this.contactHelper = new ContactHelper(account); this.syncHelper = new SyncHelper(account, attachmentHelper, @@ -223,7 +226,8 @@ public class Manager implements Closeable { sendHelper, groupHelper, syncHelper, - profileHelper); + profileHelper, + storageHelper); var jobExecutor = new JobExecutor(context); this.incomingMessageHandler = new IncomingMessageHandler(account, @@ -747,6 +751,13 @@ public class Manager implements Closeable { public void requestAllSyncData() throws IOException { syncHelper.requestAllSyncData(); + retrieveRemoteStorage(); + } + + void retrieveRemoteStorage() throws IOException { + if (account.getStorageKey() != null) { + storageHelper.readDataFromStorage(); + } } private byte[] getSenderCertificate() { diff --git a/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java b/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java index 80c214f7..90dc6c66 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java @@ -103,6 +103,7 @@ public class ProvisioningManager { ? null : DeviceNameUtil.encryptDeviceName(deviceName, ret.getIdentity().getPrivateKey()); + logger.debug("Finishing new device registration"); var deviceId = accountManager.finishNewDeviceRegistration(ret.getProvisioningCode(), false, true, @@ -129,6 +130,7 @@ public class ProvisioningManager { try { m = new Manager(account, pathConfig, serviceEnvironmentConfig, userAgent); + logger.debug("Refreshing pre keys"); try { m.refreshPreKeys(); } catch (Exception e) { @@ -136,6 +138,7 @@ public class ProvisioningManager { throw e; } + logger.debug("Requesting sync data"); try { m.requestAllSyncData(); } catch (Exception e) { 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 2be3f719..7cc0a7bc 100644 --- a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java @@ -174,7 +174,6 @@ public class RegistrationManager implements Closeable { masterKey = registrationLockData.getMasterKey(); } - // TODO response.isStorageCapable() //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); account.finishRegistration(UuidUtil.parseOrNull(response.getUuid()), masterKey, pin); @@ -186,6 +185,9 @@ public class RegistrationManager implements Closeable { m.refreshPreKeys(); // Set an initial empty profile so user can be added to groups m.setProfile(null, null, null, null, null); + if (response.isStorageCapable()) { + m.retrieveRemoteStorage(); + } final var result = m; m = null; diff --git a/lib/src/main/java/org/asamk/signal/manager/TrustLevel.java b/lib/src/main/java/org/asamk/signal/manager/TrustLevel.java index c9fa7a5e..5c712866 100644 --- a/lib/src/main/java/org/asamk/signal/manager/TrustLevel.java +++ b/lib/src/main/java/org/asamk/signal/manager/TrustLevel.java @@ -1,6 +1,7 @@ package org.asamk.signal.manager; import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; +import org.whispersystems.signalservice.internal.storage.protos.ContactRecord; public enum TrustLevel { UNTRUSTED, @@ -16,6 +17,20 @@ public enum TrustLevel { return TrustLevel.cachedValues[i]; } + public static TrustLevel fromIdentityState(ContactRecord.IdentityState identityState) { + switch (identityState) { + case DEFAULT: + return TRUSTED_UNVERIFIED; + case UNVERIFIED: + return UNTRUSTED; + case VERIFIED: + return TRUSTED_VERIFIED; + case UNRECOGNIZED: + return null; + } + throw new RuntimeException("Unknown identity state: " + identityState); + } + public static TrustLevel fromVerifiedState(VerifiedMessage.VerifiedState verifiedState) { switch (verifiedState) { case DEFAULT: diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/RetrieveStorageDataAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/RetrieveStorageDataAction.java new file mode 100644 index 00000000..6585a99a --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/RetrieveStorageDataAction.java @@ -0,0 +1,26 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.jobs.Context; + +public class RetrieveStorageDataAction implements HandleAction { + + private static final RetrieveStorageDataAction INSTANCE = new RetrieveStorageDataAction(); + + private RetrieveStorageDataAction() { + } + + public static RetrieveStorageDataAction create() { + return INSTANCE; + } + + @Override + public void execute(Context context) throws Throwable { + if (context.getAccount().getStorageKey() != null) { + context.getStorageHelper().readDataFromStorage(); + } else { + if (!context.getAccount().isMasterDevice()) { + context.getSyncHelper().requestAllSyncData(); + } + } + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncKeysAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncKeysAction.java new file mode 100644 index 00000000..fe609291 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncKeysAction.java @@ -0,0 +1,20 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.jobs.Context; + +public class SendSyncKeysAction implements HandleAction { + + private static final SendSyncKeysAction INSTANCE = new SendSyncKeysAction(); + + private SendSyncKeysAction() { + } + + public static SendSyncKeysAction create() { + return INSTANCE; + } + + @Override + public void execute(Context context) throws Throwable { + context.getSyncHelper().sendKeysMessage(); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index e6e43478..e46effc0 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -8,12 +8,14 @@ import org.asamk.signal.manager.UntrustedIdentityException; import org.asamk.signal.manager.actions.HandleAction; import org.asamk.signal.manager.actions.RenewSessionAction; import org.asamk.signal.manager.actions.RetrieveProfileAction; +import org.asamk.signal.manager.actions.RetrieveStorageDataAction; import org.asamk.signal.manager.actions.SendGroupInfoAction; import org.asamk.signal.manager.actions.SendGroupInfoRequestAction; import org.asamk.signal.manager.actions.SendReceiptAction; import org.asamk.signal.manager.actions.SendSyncBlockedListAction; import org.asamk.signal.manager.actions.SendSyncContactsAction; import org.asamk.signal.manager.actions.SendSyncGroupsAction; +import org.asamk.signal.manager.actions.SendSyncKeysAction; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupUtils; @@ -237,7 +239,10 @@ public final class IncomingMessageHandler { if (rm.isBlockedListRequest()) { actions.add(SendSyncBlockedListAction.create()); } - // TODO Handle rm.isConfigurationRequest(); rm.isKeysRequest(); + if (rm.isKeysRequest()) { + actions.add(SendSyncKeysAction.create()); + } + // TODO Handle rm.isConfigurationRequest(); } if (syncMessage.getGroups().isPresent()) { logger.warn("Received a group v1 sync message, that can't be handled anymore, ignoring."); @@ -307,7 +312,7 @@ public final class IncomingMessageHandler { case LOCAL_PROFILE: actions.add(new RetrieveProfileAction(account.getSelfRecipientId())); case STORAGE_MANIFEST: - // TODO + actions.add(RetrieveStorageDataAction.create()); } } if (syncMessage.getKeys().isPresent()) { @@ -315,6 +320,7 @@ public final class IncomingMessageHandler { if (keysMessage.getStorageService().isPresent()) { final var storageKey = keysMessage.getStorageService().get(); account.setStorageKey(storageKey); + actions.add(RetrieveStorageDataAction.create()); } } if (syncMessage.getConfiguration().isPresent()) { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java new file mode 100644 index 00000000..4caab519 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java @@ -0,0 +1,222 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.SignalDependencies; +import org.asamk.signal.manager.TrustLevel; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.recipients.Contact; +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.groups.GroupMasterKey; +import org.signal.zkgroup.profiles.ProfileKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.storage.SignalAccountRecord; +import org.whispersystems.signalservice.api.storage.SignalStorageManifest; +import org.whispersystems.signalservice.api.storage.SignalStorageRecord; +import org.whispersystems.signalservice.api.storage.StorageId; +import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord; + +import java.io.IOException; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +public class StorageHelper { + + private final static Logger logger = LoggerFactory.getLogger(StorageHelper.class); + + private final SignalAccount account; + private final SignalDependencies dependencies; + private final GroupHelper groupHelper; + + public StorageHelper( + final SignalAccount account, final SignalDependencies dependencies, final GroupHelper groupHelper + ) { + this.account = account; + this.dependencies = dependencies; + this.groupHelper = groupHelper; + } + + public void readDataFromStorage() throws IOException { + logger.debug("Reading data from remote storage"); + Optional manifest; + try { + manifest = dependencies.getAccountManager() + .getStorageManifestIfDifferentVersion(account.getStorageKey(), account.getStorageManifestVersion()); + } catch (InvalidKeyException e) { + logger.warn("Manifest couldn't be decrypted, ignoring."); + return; + } + + if (!manifest.isPresent()) { + logger.debug("Manifest is up to date, does not exist or couldn't be decrypted, ignoring."); + return; + } + + account.setStorageManifestVersion(manifest.get().getVersion()); + + readAccountRecord(manifest.get()); + + final var storageIds = manifest.get() + .getStorageIds() + .stream() + .filter(id -> !id.isUnknown() && id.getType() != ManifestRecord.Identifier.Type.ACCOUNT_VALUE) + .collect(Collectors.toList()); + + for (final var record : getSignalStorageRecords(storageIds)) { + if (record.getType() == ManifestRecord.Identifier.Type.GROUPV2_VALUE) { + readGroupV2Record(record); + } else if (record.getType() == ManifestRecord.Identifier.Type.GROUPV1_VALUE) { + readGroupV1Record(record); + } else if (record.getType() == ManifestRecord.Identifier.Type.CONTACT_VALUE) { + readContactRecord(record); + } + } + } + + private void readContactRecord(final SignalStorageRecord record) { + if (record == null || !record.getContact().isPresent()) { + return; + } + + final var contactRecord = record.getContact().get(); + final var address = contactRecord.getAddress(); + + final var recipientId = account.getRecipientStore().resolveRecipient(address); + final var contact = account.getContactStore().getContact(recipientId); + if (contactRecord.getGivenName().isPresent() || contactRecord.getFamilyName().isPresent() || ( + (contact == null || !contact.isBlocked()) && contactRecord.isBlocked() + )) { + final var newContact = (contact == null ? Contact.newBuilder() : Contact.newBuilder(contact)).withBlocked( + contactRecord.isBlocked()) + .withName((contactRecord.getGivenName().or("") + " " + contactRecord.getFamilyName().or("")).trim()) + .build(); + account.getContactStore().storeContact(recipientId, newContact); + } + + if (contactRecord.getProfileKey().isPresent()) { + try { + final var profileKey = new ProfileKey(contactRecord.getProfileKey().get()); + account.getProfileStore().storeProfileKey(recipientId, profileKey); + } catch (InvalidInputException e) { + logger.warn("Received invalid contact profile key from storage"); + } + } + if (contactRecord.getIdentityKey().isPresent()) { + try { + final var identityKey = new IdentityKey(contactRecord.getIdentityKey().get()); + account.getIdentityKeyStore().saveIdentity(recipientId, identityKey, new Date()); + + final var trustLevel = TrustLevel.fromIdentityState(contactRecord.getIdentityState()); + if (trustLevel != null) { + account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identityKey, trustLevel); + } + } catch (InvalidKeyException e) { + logger.warn("Received invalid contact identity key from storage"); + } + } + } + + private void readGroupV1Record(final SignalStorageRecord record) { + if (record == null || !record.getGroupV1().isPresent()) { + return; + } + + final var groupV1Record = record.getGroupV1().get(); + final var groupIdV1 = GroupId.v1(groupV1Record.getGroupId()); + + final var group = account.getGroupStore().getGroup(groupIdV1); + if (group == null) { + try { + groupHelper.sendGroupInfoRequest(groupIdV1, account.getSelfRecipientId()); + } catch (Throwable e) { + logger.warn("Failed to send group request", e); + } + } + final var groupV1 = account.getGroupStore().getOrCreateGroupV1(groupIdV1); + if (groupV1.isBlocked() != groupV1Record.isBlocked()) { + groupV1.setBlocked(groupV1Record.isBlocked()); + account.getGroupStore().updateGroup(groupV1); + } + } + + private void readGroupV2Record(final SignalStorageRecord record) { + if (record == null || !record.getGroupV2().isPresent()) { + return; + } + + final var groupV2Record = record.getGroupV2().get(); + if (groupV2Record.isArchived()) { + return; + } + + final GroupMasterKey groupMasterKey; + try { + groupMasterKey = new GroupMasterKey(groupV2Record.getMasterKeyBytes()); + } catch (InvalidInputException e) { + logger.warn("Received invalid group master key from storage"); + return; + } + + final var group = groupHelper.getOrMigrateGroup(groupMasterKey, 0, null); + if (group.isBlocked() != groupV2Record.isBlocked()) { + group.setBlocked(groupV2Record.isBlocked()); + account.getGroupStore().updateGroup(group); + } + } + + private void readAccountRecord(final SignalStorageManifest manifest) throws IOException { + Optional accountId = manifest.getAccountStorageId(); + if (!accountId.isPresent()) { + logger.warn("Manifest has no account record, ignoring."); + return; + } + + SignalStorageRecord record = getSignalStorageRecord(accountId.get()); + if (record == null) { + logger.warn("Could not find account record, even though we had an ID, ignoring."); + return; + } + + SignalAccountRecord accountRecord = record.getAccount().orNull(); + if (accountRecord == null) { + logger.warn("The storage record didn't actually have an account, ignoring."); + return; + } + + if (accountRecord.getProfileKey().isPresent()) { + try { + account.setProfileKey(new ProfileKey(accountRecord.getProfileKey().get())); + } catch (InvalidInputException e) { + logger.warn("Received invalid profile key from storage"); + } + } + } + + private SignalStorageRecord getSignalStorageRecord(final StorageId accountId) throws IOException { + List records; + try { + records = dependencies.getAccountManager() + .readStorageRecords(account.getStorageKey(), Collections.singletonList(accountId)); + } catch (InvalidKeyException e) { + logger.warn("Failed to read storage records, ignoring."); + return null; + } + return records.size() > 0 ? records.get(0) : null; + } + + private List getSignalStorageRecords(final List storageIds) throws IOException { + List records; + try { + records = dependencies.getAccountManager().readStorageRecords(account.getStorageKey(), storageIds); + } catch (InvalidKeyException e) { + logger.warn("Failed to read storage records, ignoring."); + return List.of(); + } + return records; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java index bcdf6ab1..6db1ca7d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java @@ -20,6 +20,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsI import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsOutputStream; import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup; import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream; +import org.whispersystems.signalservice.api.messages.multidevice.KeysMessage; import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; @@ -215,6 +216,11 @@ public class SyncHelper { sendHelper.sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage)); } + public void sendKeysMessage() throws IOException { + var keysMessage = new KeysMessage(Optional.fromNullable(account.getStorageKey())); + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forKeys(keysMessage)); + } + public void handleSyncDeviceContacts(final InputStream input) throws IOException { final var s = new DeviceContactsInputStream(input); DeviceContact c; diff --git a/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java b/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java index 142c148a..beb41969 100644 --- a/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java +++ b/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java @@ -5,6 +5,7 @@ import org.asamk.signal.manager.StickerPackStore; import org.asamk.signal.manager.helper.GroupHelper; import org.asamk.signal.manager.helper.ProfileHelper; import org.asamk.signal.manager.helper.SendHelper; +import org.asamk.signal.manager.helper.StorageHelper; import org.asamk.signal.manager.helper.SyncHelper; import org.asamk.signal.manager.storage.SignalAccount; @@ -17,6 +18,7 @@ public class Context { private final GroupHelper groupHelper; private final SyncHelper syncHelper; private final ProfileHelper profileHelper; + private final StorageHelper storageHelper; public Context( final SignalAccount account, @@ -25,7 +27,8 @@ public class Context { final SendHelper sendHelper, final GroupHelper groupHelper, final SyncHelper syncHelper, - final ProfileHelper profileHelper + final ProfileHelper profileHelper, + final StorageHelper storageHelper ) { this.account = account; this.dependencies = dependencies; @@ -34,6 +37,7 @@ public class Context { this.groupHelper = groupHelper; this.syncHelper = syncHelper; this.profileHelper = profileHelper; + this.storageHelper = storageHelper; } public SignalAccount getAccount() { @@ -63,4 +67,8 @@ public class Context { public ProfileHelper getProfileHelper() { return profileHelper; } + + public StorageHelper getStorageHelper() { + return storageHelper; + } } 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 efdcf798..fd4ec597 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 @@ -84,6 +84,7 @@ public class SignalAccount implements Closeable { private String registrationLockPin; private MasterKey pinMasterKey; private StorageKey storageKey; + private long storageManifestVersion = -1; private ProfileKey profileKey; private int preKeyIdOffset; private int nextSignedPreKeyId; @@ -291,6 +292,9 @@ public class SignalAccount implements Closeable { this.registered = true; this.isMultiDevice = true; this.lastReceiveTimestamp = 0; + this.pinMasterKey = null; + this.storageManifestVersion = -1; + this.storageKey = null; } private void migrateLegacyConfigs() { @@ -432,6 +436,9 @@ public class SignalAccount implements Closeable { if (rootNode.hasNonNull("storageKey")) { storageKey = new StorageKey(Base64.getDecoder().decode(rootNode.get("storageKey").asText())); } + if (rootNode.hasNonNull("storageManifestVersion")) { + storageManifestVersion = rootNode.get("storageManifestVersion").asLong(); + } if (rootNode.hasNonNull("preKeyIdOffset")) { preKeyIdOffset = rootNode.get("preKeyIdOffset").asInt(0); } else { @@ -693,6 +700,7 @@ public class SignalAccount implements Closeable { pinMasterKey == null ? null : Base64.getEncoder().encodeToString(pinMasterKey.serialize())) .put("storageKey", storageKey == null ? null : Base64.getEncoder().encodeToString(storageKey.serialize())) + .put("storageManifestVersion", storageManifestVersion == -1 ? null : storageManifestVersion) .put("preKeyIdOffset", preKeyIdOffset) .put("nextSignedPreKeyId", nextSignedPreKeyId) .put("profileKey", @@ -877,6 +885,18 @@ public class SignalAccount implements Closeable { save(); } + public long getStorageManifestVersion() { + return this.storageManifestVersion; + } + + public void setStorageManifestVersion(final long storageManifestVersion) { + if (storageManifestVersion == this.storageManifestVersion) { + return; + } + this.storageManifestVersion = storageManifestVersion; + save(); + } + public ProfileKey getProfileKey() { return profileKey; } @@ -948,6 +968,8 @@ public class SignalAccount implements Closeable { public void finishRegistration(final UUID uuid, final MasterKey masterKey, final String pin) { this.pinMasterKey = masterKey; + this.storageManifestVersion = -1; + this.storageKey = null; this.encryptedDeviceName = null; this.deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID; this.isMultiDevice = false; From 656ca6b5e4f5fe131f3477e39fcc20372655ad59 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 5 Sep 2021 16:06:13 +0200 Subject: [PATCH 10/10] Prevent creation of RecipientAddress with UNKNOWN_UUID --- .../manager/storage/recipients/RecipientAddress.java | 7 +++---- .../signal/manager/storage/recipients/RecipientStore.java | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java index 29e964b0..88877d83 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java @@ -18,6 +18,7 @@ public class RecipientAddress { * @param e164 The phone number of the user, if available. */ public RecipientAddress(Optional uuid, Optional e164) { + uuid = uuid.isPresent() && uuid.get().equals(UuidUtil.UNKNOWN_UUID) ? Optional.empty() : uuid; if (!uuid.isPresent() && !e164.isPresent()) { throw new AssertionError("Must have either a UUID or E164 number!"); } @@ -31,13 +32,11 @@ public class RecipientAddress { } public RecipientAddress(SignalServiceAddress address) { - this.uuid = Optional.of(address.getUuid()); - this.e164 = Optional.ofNullable(address.getNumber().orNull()); + this(Optional.of(address.getUuid()), Optional.ofNullable(address.getNumber().orNull())); } public RecipientAddress(UUID uuid) { - this.uuid = Optional.of(uuid); - this.e164 = Optional.empty(); + this(Optional.of(uuid), Optional.empty()); } public Optional getNumber() { 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 86164d58..bace6a6b 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 @@ -308,7 +308,7 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile final var byNumber = address.getNumber().isEmpty() ? Optional.empty() : findByNumberLocked(address.getNumber().get()); - final var byUuid = address.getUuid().isEmpty() || address.getUuid().get().equals(UuidUtil.UNKNOWN_UUID) + final var byUuid = address.getUuid().isEmpty() ? Optional.empty() : findByUuidLocked(address.getUuid().get());