]> nmode's Git Repositories - signal-cli/commitdiff
Implementing sending group messages with sender keys
authorAsamK <asamk@gmx.de>
Mon, 20 Dec 2021 11:26:03 +0000 (12:26 +0100)
committerAsamK <asamk@gmx.de>
Mon, 20 Dec 2021 13:38:48 +0000 (14:38 +0100)
16 files changed:
lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java
lib/src/main/java/org/asamk/signal/manager/TrustLevel.java
lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java
lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java
lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java
lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java
lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java
lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java
lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java
lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java
lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java
lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java
lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java
lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeySharedStore.java
lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyStore.java
lib/src/main/java/org/asamk/signal/manager/util/IOUtils.java

index df757a8b92e26f4862d25b13688a7424a90b83de..5c3f0e0b691c2483ff046e2df9078f7646799c3c 100644 (file)
@@ -195,7 +195,7 @@ public class ManagerImpl implements Manager {
                 unidentifiedAccessHelper::getAccessFor,
                 this::resolveSignalServiceAddress);
         final GroupV2Helper groupV2Helper = new GroupV2Helper(profileHelper::getRecipientProfileKeyCredential,
-                this::getRecipientProfile,
+                profileHelper::getRecipientProfile,
                 account::getSelfRecipientId,
                 dependencies.getGroupsV2Operations(),
                 dependencies.getGroupsV2Api(),
@@ -207,6 +207,7 @@ public class ManagerImpl implements Manager {
                 account.getRecipientStore(),
                 this::handleIdentityFailure,
                 this::getGroupInfo,
+                profileHelper::getRecipientProfile,
                 this::refreshRegisteredUser);
         this.groupHelper = new GroupHelper(account,
                 dependencies,
@@ -245,7 +246,7 @@ public class ManagerImpl implements Manager {
                 contactHelper,
                 attachmentHelper,
                 syncHelper,
-                this::getRecipientProfile,
+                profileHelper::getRecipientProfile,
                 jobExecutor);
         this.identityHelper = new IdentityHelper(account,
                 dependencies,
index fead442cd2f537f1cda9cd244120301d5f41045a..9d58754cb46355d089497290a69344ac854f7e75 100644 (file)
@@ -41,4 +41,11 @@ public enum TrustLevel {
             case TRUSTED_VERIFIED -> VerifiedMessage.VerifiedState.VERIFIED;
         };
     }
+
+    public boolean isTrusted() {
+        return switch (this) {
+            case TRUSTED_UNVERIFIED, TRUSTED_VERIFIED -> true;
+            case UNTRUSTED -> false;
+        };
+    }
 }
index a0f6c89b7769f0b51e0d61d54d60f18bf6b1b092..809788d5d5dab0833345bd7e3745d06b00c2333f 100644 (file)
@@ -44,6 +44,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
 import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
 import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
 import org.whispersystems.signalservice.api.push.ACI;
+import org.whispersystems.signalservice.api.push.DistributionId;
 import org.whispersystems.signalservice.api.push.exceptions.ConflictException;
 
 import java.io.File;
@@ -200,7 +201,9 @@ public class GroupHelper {
 
         final var messageBuilder = getGroupUpdateMessageBuilder(gv2, null);
 
-        final var result = sendGroupMessage(messageBuilder, gv2.getMembersIncludingPendingWithout(selfRecipientId));
+        final var result = sendGroupMessage(messageBuilder,
+                gv2.getMembersIncludingPendingWithout(selfRecipientId),
+                gv2.getDistributionId());
         return new Pair<>(gv2.getGroupId(), result);
     }
 
@@ -333,7 +336,7 @@ public class GroupHelper {
         var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build());
 
         // Send group info request message to the recipient who sent us a message with this groupId
-        return sendGroupMessage(messageBuilder, Set.of(recipientId));
+        return sendGroupMessage(messageBuilder, Set.of(recipientId), null);
     }
 
     public SendGroupMessageResults sendGroupInfoMessage(
@@ -353,7 +356,7 @@ public class GroupHelper {
         var messageBuilder = getGroupUpdateMessageBuilder(g);
 
         // Send group message only to the recipient who requested it
-        return sendGroupMessage(messageBuilder, Set.of(recipientId));
+        return sendGroupMessage(messageBuilder, Set.of(recipientId), null);
     }
 
     private GroupInfo getGroup(GroupId groupId, boolean forceUpdate) {
@@ -438,7 +441,9 @@ public class GroupHelper {
         account.getGroupStore().updateGroup(gv1);
 
         var messageBuilder = getGroupUpdateMessageBuilder(gv1);
-        return sendGroupMessage(messageBuilder, gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
+        return sendGroupMessage(messageBuilder,
+                gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId()),
+                gv1.getDistributionId());
     }
 
     private void updateGroupV1Details(
@@ -600,7 +605,8 @@ public class GroupHelper {
         groupInfoV1.removeMember(account.getSelfRecipientId());
         account.getGroupStore().updateGroup(groupInfoV1);
         return sendGroupMessage(messageBuilder,
-                groupInfoV1.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
+                groupInfoV1.getMembersIncludingPendingWithout(account.getSelfRecipientId()),
+                groupInfoV1.getDistributionId());
     }
 
     private SendGroupMessageResults quitGroupV2(
@@ -622,7 +628,8 @@ public class GroupHelper {
 
         var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray());
         return sendGroupMessage(messageBuilder,
-                groupInfoV2.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
+                groupInfoV2.getMembersIncludingPendingWithout(account.getSelfRecipientId()),
+                groupInfoV2.getDistributionId());
     }
 
     private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException {
@@ -664,15 +671,17 @@ public class GroupHelper {
         account.getGroupStore().updateGroup(group);
 
         final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray());
-        return sendGroupMessage(messageBuilder, members);
+        return sendGroupMessage(messageBuilder, members, group.getDistributionId());
     }
 
     private SendGroupMessageResults sendGroupMessage(
-            final SignalServiceDataMessage.Builder messageBuilder, final Set<RecipientId> members
+            final SignalServiceDataMessage.Builder messageBuilder,
+            final Set<RecipientId> members,
+            final DistributionId distributionId
     ) throws IOException {
         final var timestamp = System.currentTimeMillis();
         messageBuilder.withTimestamp(timestamp);
-        final var results = sendHelper.sendGroupMessage(messageBuilder.build(), members);
+        final var results = sendHelper.sendGroupMessage(messageBuilder.build(), members, distributionId);
         return new SendGroupMessageResults(timestamp,
                 results.stream()
                         .map(sendMessageResult -> SendMessageResult.from(sendMessageResult,
index 531870d9e63f51906d966c5af05afd4b578a014c..79afe8635720c0362e19702aaa31d4b75ac428dd 100644 (file)
@@ -126,6 +126,7 @@ public class IdentityHelper {
             final var newIdentity = account.getIdentityKeyStore().saveIdentity(recipientId, identityKey, new Date());
             if (newIdentity) {
                 account.getSessionStore().archiveSessions(recipientId);
+                account.getSenderKeyStore().deleteSharedWith(recipientId);
             }
         } else {
             // Retrieve profile to get the current identity key from the server
index bf788598057bf4c4045e0ee47d52d15fc3443764..e903d8073feb9367ab0324705adc8669b74c0e08 100644 (file)
@@ -247,6 +247,7 @@ public final class ProfileHelper {
 
             if (newIdentity) {
                 account.getSessionStore().archiveSessions(recipientId);
+                account.getSenderKeyStore().deleteSharedWith(recipientId);
             }
         } catch (InvalidKeyException ignored) {
             logger.warn("Got invalid identity key in profile for {}",
index c8d8bbb767aa9e6d62c48acd9c9cb2ed4ca09d61..41dccaa474a9a3a4b55cbf5cae95065b6c319a87 100644 (file)
@@ -8,14 +8,19 @@ import org.asamk.signal.manager.groups.GroupUtils;
 import org.asamk.signal.manager.groups.NotAGroupMemberException;
 import org.asamk.signal.manager.storage.SignalAccount;
 import org.asamk.signal.manager.storage.groups.GroupInfo;
+import org.asamk.signal.manager.storage.recipients.Profile;
 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.InvalidKeyException;
+import org.whispersystems.libsignal.InvalidRegistrationIdException;
+import org.whispersystems.libsignal.NoSessionException;
 import org.whispersystems.libsignal.protocol.DecryptionErrorMessage;
 import org.whispersystems.libsignal.util.guava.Optional;
 import org.whispersystems.signalservice.api.SignalServiceMessageSender;
 import org.whispersystems.signalservice.api.crypto.ContentHint;
+import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
 import org.whispersystems.signalservice.api.messages.SendMessageResult;
 import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
@@ -23,16 +28,22 @@ import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage
 import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
 import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
 import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
+import org.whispersystems.signalservice.api.push.DistributionId;
 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
 import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
 import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
 import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
+import org.whispersystems.signalservice.internal.push.exceptions.InvalidUnidentifiedAccessHeaderException;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 public class SendHelper {
 
@@ -45,6 +56,7 @@ public class SendHelper {
     private final RecipientResolver recipientResolver;
     private final IdentityFailureHandler identityFailureHandler;
     private final GroupProvider groupProvider;
+    private final ProfileProvider profileProvider;
     private final RecipientRegistrationRefresher recipientRegistrationRefresher;
 
     public SendHelper(
@@ -55,6 +67,7 @@ public class SendHelper {
             final RecipientResolver recipientResolver,
             final IdentityFailureHandler identityFailureHandler,
             final GroupProvider groupProvider,
+            final ProfileProvider profileProvider,
             final RecipientRegistrationRefresher recipientRegistrationRefresher
     ) {
         this.account = account;
@@ -64,6 +77,7 @@ public class SendHelper {
         this.recipientResolver = recipientResolver;
         this.identityFailureHandler = identityFailureHandler;
         this.groupProvider = groupProvider;
+        this.profileProvider = profileProvider;
         this.recipientRegistrationRefresher = recipientRegistrationRefresher;
     }
 
@@ -81,7 +95,7 @@ public class SendHelper {
 
         final var message = messageBuilder.build();
         final var result = sendMessage(message, recipientId);
-        handlePossibleIdentityFailure(result);
+        handleSendMessageResult(result);
         return result;
     }
 
@@ -116,7 +130,7 @@ public class SendHelper {
             }
         }
 
-        return sendGroupMessage(message, recipients);
+        return sendGroupMessage(message, recipients, g.getDistributionId());
     }
 
     /**
@@ -124,12 +138,14 @@ public class SendHelper {
      * This method should only be used for create/update/quit group messages.
      */
     public List<SendMessageResult> sendGroupMessage(
-            final SignalServiceDataMessage message, final Set<RecipientId> recipientIds
+            final SignalServiceDataMessage message,
+            final Set<RecipientId> recipientIds,
+            final DistributionId distributionId
     ) throws IOException {
-        List<SendMessageResult> result = sendGroupMessageInternal(message, recipientIds);
+        List<SendMessageResult> result = sendGroupMessageInternal(message, recipientIds, distributionId);
 
         for (var r : result) {
-            handlePossibleIdentityFailure(r);
+            handleSendMessageResult(r);
         }
 
         return result;
@@ -245,27 +261,189 @@ public class SendHelper {
     }
 
     private List<SendMessageResult> sendGroupMessageInternal(
-            final SignalServiceDataMessage message, final Set<RecipientId> recipientIds
+            final SignalServiceDataMessage message,
+            final Set<RecipientId> recipientIds,
+            final DistributionId distributionId
     ) throws IOException {
+        // isRecipientUpdate is true if we've already sent this message to some recipients in the past, otherwise false.
+        final var isRecipientUpdate = false;
+        Set<RecipientId> senderKeyTargets = distributionId == null
+                ? Set.of()
+                : getSenderKeyCapableRecipientIds(recipientIds);
+        final var allResults = new ArrayList<SendMessageResult>(recipientIds.size());
+
+        if (senderKeyTargets.size() > 0) {
+            final var results = sendGroupMessageInternalWithSenderKey(message,
+                    senderKeyTargets,
+                    distributionId,
+                    isRecipientUpdate);
+
+            if (results == null) {
+                senderKeyTargets = Set.of();
+            } else {
+                results.stream().filter(SendMessageResult::isSuccess).forEach(allResults::add);
+                final var failedTargets = results.stream()
+                        .filter(r -> !r.isSuccess())
+                        .map(r -> recipientResolver.resolveRecipient(r.getAddress()))
+                        .toList();
+                if (failedTargets.size() > 0) {
+                    senderKeyTargets = new HashSet<>(senderKeyTargets);
+                    failedTargets.forEach(senderKeyTargets::remove);
+                }
+            }
+        }
+
+        final var legacyTargets = new HashSet<>(recipientIds);
+        legacyTargets.removeAll(senderKeyTargets);
+        final boolean onlyTargetIsSelfWithLinkedDevice = recipientIds.isEmpty() && account.isMultiDevice();
+
+        if (legacyTargets.size() > 0 || onlyTargetIsSelfWithLinkedDevice) {
+            if (legacyTargets.size() > 0) {
+                logger.debug("Need to do {} legacy sends.", legacyTargets.size());
+            } else {
+                logger.debug("Need to do a legacy send to send a sync message for a group of only ourselves.");
+            }
+
+            final List<SendMessageResult> results = sendGroupMessageInternalWithLegacy(message,
+                    legacyTargets,
+                    isRecipientUpdate || allResults.size() > 0);
+            allResults.addAll(results);
+        }
+
+        return allResults;
+    }
+
+    private Set<RecipientId> getSenderKeyCapableRecipientIds(final Set<RecipientId> recipientIds) {
+        final var selfProfile = profileProvider.getProfile(account.getSelfRecipientId());
+        if (selfProfile == null || !selfProfile.getCapabilities().contains(Profile.Capability.senderKey)) {
+            logger.debug("Not all of our devices support sender key. Using legacy.");
+            return Set.of();
+        }
+
+        final var senderKeyTargets = new HashSet<RecipientId>();
+        for (final var recipientId : recipientIds) {
+            // TODO filter out unregistered
+            final var profile = profileProvider.getProfile(recipientId);
+            if (profile == null || !profile.getCapabilities().contains(Profile.Capability.senderKey)) {
+                continue;
+            }
+
+            final var access = unidentifiedAccessHelper.getAccessFor(recipientId);
+            if (!access.isPresent() || !access.get().getTargetUnidentifiedAccess().isPresent()) {
+                continue;
+            }
+
+            final var identity = account.getIdentityKeyStore().getIdentity(recipientId);
+            if (identity == null || !identity.getTrustLevel().isTrusted()) {
+                continue;
+            }
+
+            senderKeyTargets.add(recipientId);
+        }
+
+        if (senderKeyTargets.size() < 2) {
+            logger.debug("Too few sender-key-capable users ({}). Doing all legacy sends.", senderKeyTargets.size());
+            return Set.of();
+        }
+
+        logger.debug("Can use sender key for {}/{} recipients.", senderKeyTargets.size(), recipientIds.size());
+        return senderKeyTargets;
+    }
+
+    private List<SendMessageResult> sendGroupMessageInternalWithLegacy(
+            final SignalServiceDataMessage message, final Set<RecipientId> recipientIds, final boolean isRecipientUpdate
+    ) throws IOException {
+        final var recipientIdList = new ArrayList<>(recipientIds);
+        final var addresses = recipientIdList.stream().map(addressResolver::resolveSignalServiceAddress).toList();
+        final var unidentifiedAccesses = unidentifiedAccessHelper.getAccessFor(recipientIdList);
+        final var messageSender = dependencies.getMessageSender();
         try {
-            var messageSender = dependencies.getMessageSender();
-            // isRecipientUpdate is true if we've already sent this message to some recipients in the past, otherwise false.
-            final var isRecipientUpdate = false;
-            final var recipientIdList = new ArrayList<>(recipientIds);
-            final var addresses = recipientIdList.stream().map(addressResolver::resolveSignalServiceAddress).toList();
-            return messageSender.sendDataMessage(addresses,
-                    unidentifiedAccessHelper.getAccessFor(recipientIdList),
+            final var results = messageSender.sendDataMessage(addresses,
+                    unidentifiedAccesses,
                     isRecipientUpdate,
                     ContentHint.DEFAULT,
                     message,
                     SignalServiceMessageSender.LegacyGroupEvents.EMPTY,
                     sendResult -> logger.trace("Partial message send result: {}", sendResult.isSuccess()),
                     () -> false);
+
+            final var successCount = results.stream().filter(SendMessageResult::isSuccess).count();
+            logger.debug("Successfully sent using 1:1 to {}/{} legacy targets.", successCount, recipientIdList.size());
+            return results;
         } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
             return List.of();
         }
     }
 
+    private List<SendMessageResult> sendGroupMessageInternalWithSenderKey(
+            final SignalServiceDataMessage message,
+            final Set<RecipientId> recipientIds,
+            final DistributionId distributionId,
+            final boolean isRecipientUpdate
+    ) throws IOException {
+        final var recipientIdList = new ArrayList<>(recipientIds);
+        final var messageSender = dependencies.getMessageSender();
+
+        long keyCreateTime = account.getSenderKeyStore()
+                .getCreateTimeForOurKey(account.getSelfRecipientId(), account.getDeviceId(), distributionId);
+        long keyAge = System.currentTimeMillis() - keyCreateTime;
+
+        if (keyCreateTime != -1 && keyAge > TimeUnit.DAYS.toMillis(14)) {
+            logger.debug("DistributionId {} was created at {} and is {} ms old (~{} days). Rotating.",
+                    distributionId,
+                    keyCreateTime,
+                    keyAge,
+                    TimeUnit.MILLISECONDS.toDays(keyAge));
+            account.getSenderKeyStore().deleteOurKey(account.getSelfRecipientId(), distributionId);
+        }
+
+        List<SignalServiceAddress> addresses = recipientIdList.stream()
+                .map(addressResolver::resolveSignalServiceAddress)
+                .collect(Collectors.toList());
+        List<UnidentifiedAccess> unidentifiedAccesses = recipientIdList.stream()
+                .map(unidentifiedAccessHelper::getAccessFor)
+                .map(Optional::get)
+                .map(UnidentifiedAccessPair::getTargetUnidentifiedAccess)
+                .map(Optional::get)
+                .collect(Collectors.toList());
+
+        try {
+            List<SendMessageResult> results = messageSender.sendGroupDataMessage(distributionId,
+                    addresses,
+                    unidentifiedAccesses,
+                    isRecipientUpdate,
+                    ContentHint.DEFAULT,
+                    message,
+                    SignalServiceMessageSender.SenderKeyGroupEvents.EMPTY);
+
+            final var successCount = results.stream().filter(SendMessageResult::isSuccess).count();
+            logger.debug("Successfully sent using sender key to {}/{} sender key targets.",
+                    successCount,
+                    addresses.size());
+
+            return results;
+        } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
+            return null;
+        } catch (InvalidUnidentifiedAccessHeaderException e) {
+            logger.warn("Someone had a bad UD header. Falling back to legacy sends.", e);
+            return null;
+        } catch (NoSessionException e) {
+            logger.warn("No session. Falling back to legacy sends.", e);
+            account.getSenderKeyStore().deleteOurKey(account.getSelfRecipientId(), distributionId);
+            return null;
+        } catch (InvalidKeyException e) {
+            logger.warn("Invalid key. Falling back to legacy sends.", e);
+            account.getSenderKeyStore().deleteOurKey(account.getSelfRecipientId(), distributionId);
+            return null;
+        } catch (InvalidRegistrationIdException e) {
+            logger.warn("Invalid registrationId. Falling back to legacy sends.", e);
+            return null;
+        } catch (NotFoundException e) {
+            logger.warn("Someone was unregistered. Falling back to legacy sends.", e);
+            return null;
+        }
+    }
+
     private SendMessageResult sendMessage(
             SignalServiceDataMessage message, RecipientId recipientId
     ) {
@@ -317,7 +495,7 @@ public class SendHelper {
         return sendSyncMessage(syncMessage);
     }
 
-    private void handlePossibleIdentityFailure(final SendMessageResult r) {
+    private void handleSendMessageResult(final SendMessageResult r) {
         if (r.getIdentityFailure() != null) {
             final var recipientId = recipientResolver.resolveRecipient(r.getAddress());
             identityFailureHandler.handleIdentityFailure(recipientId, r.getIdentityFailure());
index 364c61e914be1d04f9d0310a2ec9917e02b730e6..d823f641187b925d03271da0ffd666abf8b9c195 100644 (file)
@@ -10,6 +10,7 @@ import org.asamk.signal.manager.storage.configuration.ConfigurationStore;
 import org.asamk.signal.manager.storage.contacts.ContactsStore;
 import org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore;
 import org.asamk.signal.manager.storage.groups.GroupInfoV1;
+import org.asamk.signal.manager.storage.groups.GroupInfoV2;
 import org.asamk.signal.manager.storage.groups.GroupStore;
 import org.asamk.signal.manager.storage.identities.IdentityKeyStore;
 import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
@@ -45,6 +46,7 @@ import org.whispersystems.libsignal.util.Medium;
 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
 import org.whispersystems.signalservice.api.kbs.MasterKey;
 import org.whispersystems.signalservice.api.push.ACI;
+import org.whispersystems.signalservice.api.push.DistributionId;
 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
 import org.whispersystems.signalservice.api.storage.StorageKey;
 import org.whispersystems.signalservice.api.util.UuidUtil;
@@ -69,7 +71,9 @@ public class SignalAccount implements Closeable {
     private final static Logger logger = LoggerFactory.getLogger(SignalAccount.class);
 
     private static final int MINIMUM_STORAGE_VERSION = 1;
-    private static final int CURRENT_STORAGE_VERSION = 2;
+    private static final int CURRENT_STORAGE_VERSION = 3;
+
+    private int previousStorageVersion;
 
     private final ObjectMapper jsonProcessor = Utils.createStorageObjectMapper();
 
@@ -166,6 +170,7 @@ public class SignalAccount implements Closeable {
 
         signalAccount.registered = false;
 
+        signalAccount.previousStorageVersion = CURRENT_STORAGE_VERSION;
         signalAccount.migrateLegacyConfigs();
         signalAccount.save();
 
@@ -274,6 +279,7 @@ public class SignalAccount implements Closeable {
         signalAccount.configurationStore = new ConfigurationStore(signalAccount::saveConfigurationStore);
 
         signalAccount.recipientStore.resolveRecipientTrusted(signalAccount.getSelfAddress());
+        signalAccount.previousStorageVersion = CURRENT_STORAGE_VERSION;
         signalAccount.migrateLegacyConfigs();
         signalAccount.save();
 
@@ -307,12 +313,20 @@ public class SignalAccount implements Closeable {
             setPassword(KeyUtils.createPassword());
         }
 
-        if (getProfileKey() == null && isRegistered()) {
+        if (getProfileKey() == null) {
             // Old config file, creating new profile key
             setProfileKey(KeyUtils.createProfileKey());
         }
         // Ensure our profile key is stored in profile store
         getProfileStore().storeProfileKey(getSelfRecipientId(), getProfileKey());
+        if (previousStorageVersion < 3) {
+            for (final var group : groupStore.getGroups()) {
+                if (group instanceof GroupInfoV2 && group.getDistributionId() == null) {
+                    ((GroupInfoV2) group).setDistributionId(DistributionId.create());
+                    groupStore.updateGroup(group);
+                }
+            }
+        }
     }
 
     private void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
@@ -405,10 +419,13 @@ public class SignalAccount implements Closeable {
             } else if (accountVersion < MINIMUM_STORAGE_VERSION) {
                 throw new IOException("Config file was created by a no longer supported older version!");
             }
+            previousStorageVersion = accountVersion;
         }
 
         account = Utils.getNotNullNode(rootNode, "username").asText();
-        password = Utils.getNotNullNode(rootNode, "password").asText();
+        if (rootNode.hasNonNull("password")) {
+            password = rootNode.get("password").asText();
+        }
         registered = Utils.getNotNullNode(rootNode, "registered").asBoolean();
         if (rootNode.hasNonNull("uuid")) {
             try {
index 2c2102595fb23c8b56f0e929c3c0ee49dfc2cca6..4d94eb01fbe141ca43fee9cbb349b8262b68cc52 100644 (file)
@@ -4,6 +4,7 @@ import org.asamk.signal.manager.groups.GroupId;
 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
 import org.asamk.signal.manager.groups.GroupPermission;
 import org.asamk.signal.manager.storage.recipients.RecipientId;
+import org.whispersystems.signalservice.api.push.DistributionId;
 
 import java.util.Set;
 import java.util.stream.Collectors;
@@ -13,6 +14,8 @@ public sealed abstract class GroupInfo permits GroupInfoV1, GroupInfoV2 {
 
     public abstract GroupId getGroupId();
 
+    public abstract DistributionId getDistributionId();
+
     public abstract String getTitle();
 
     public String getDescription() {
index 4e759e5fc32d5dce4c1c5c4ad250de0400af0b1c..8b10397619d117988be4d5a5c3aaa2effb1c61d9 100644 (file)
@@ -6,6 +6,7 @@ import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
 import org.asamk.signal.manager.groups.GroupPermission;
 import org.asamk.signal.manager.groups.GroupUtils;
 import org.asamk.signal.manager.storage.recipients.RecipientId;
+import org.whispersystems.signalservice.api.push.DistributionId;
 
 import java.util.Collection;
 import java.util.HashSet;
@@ -54,6 +55,11 @@ public final class GroupInfoV1 extends GroupInfo {
         return groupId;
     }
 
+    @Override
+    public DistributionId getDistributionId() {
+        return null;
+    }
+
     public GroupIdV2 getExpectedV2Id() {
         if (expectedV2Id == null) {
             expectedV2Id = GroupUtils.getGroupIdV2(groupId);
index 752d42baf279d7d6e1a00df8c68ea8557dee34aa..eb6d5f29f2d695f93c0b2781e84b55504a146ae8 100644 (file)
@@ -11,6 +11,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedGroup;
 import org.signal.storageservice.protos.groups.local.EnabledState;
 import org.signal.zkgroup.groups.GroupMasterKey;
 import org.whispersystems.signalservice.api.push.ACI;
+import org.whispersystems.signalservice.api.push.DistributionId;
 
 import java.util.Set;
 import java.util.stream.Collectors;
@@ -19,25 +20,29 @@ public final class GroupInfoV2 extends GroupInfo {
 
     private final GroupIdV2 groupId;
     private final GroupMasterKey masterKey;
-
+    private DistributionId distributionId;
     private boolean blocked;
-    private DecryptedGroup group; // stored as a file with hexadecimal groupId as name
-    private RecipientResolver recipientResolver;
+    private DecryptedGroup group; // stored as a file with base64 groupId as name
     private boolean permissionDenied;
 
+    private RecipientResolver recipientResolver;
+
     public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey) {
         this.groupId = groupId;
         this.masterKey = masterKey;
+        this.distributionId = DistributionId.create();
     }
 
     public GroupInfoV2(
             final GroupIdV2 groupId,
             final GroupMasterKey masterKey,
+            final DistributionId distributionId,
             final boolean blocked,
             final boolean permissionDenied
     ) {
         this.groupId = groupId;
         this.masterKey = masterKey;
+        this.distributionId = distributionId;
         this.blocked = blocked;
         this.permissionDenied = permissionDenied;
     }
@@ -51,6 +56,14 @@ public final class GroupInfoV2 extends GroupInfo {
         return masterKey;
     }
 
+    public DistributionId getDistributionId() {
+        return distributionId;
+    }
+
+    public void setDistributionId(final DistributionId distributionId) {
+        this.distributionId = distributionId;
+    }
+
     public void setGroup(final DecryptedGroup group, final RecipientResolver recipientResolver) {
         if (group != null) {
             this.permissionDenied = false;
index d77c99188d36017a117faead27b012c69305912a..78c8b23e906b4505b303892814f1f586c78188d6 100644 (file)
@@ -23,6 +23,7 @@ import org.signal.zkgroup.InvalidInputException;
 import org.signal.zkgroup.groups.GroupMasterKey;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.whispersystems.signalservice.api.push.DistributionId;
 import org.whispersystems.signalservice.api.util.UuidUtil;
 import org.whispersystems.signalservice.internal.util.Hex;
 
@@ -105,7 +106,11 @@ public class GroupStore {
                 throw new AssertionError("Invalid master key for group " + groupId.toBase64());
             }
 
-            return new GroupInfoV2(groupId, masterKey, g2.blocked, g2.permissionDenied);
+            return new GroupInfoV2(groupId,
+                    masterKey,
+                    g2.distributionId == null ? null : DistributionId.from(g2.distributionId),
+                    g2.blocked,
+                    g2.permissionDenied);
         }).collect(Collectors.toMap(GroupInfo::getGroupId, g -> g));
 
         return new GroupStore(groupCachePath, groups, recipientResolver, saver);
@@ -268,6 +273,7 @@ public class GroupStore {
             final var g2 = (GroupInfoV2) g;
             return new Storage.GroupV2(g2.getGroupId().toBase64(),
                     Base64.getEncoder().encodeToString(g2.getMasterKey().serialize()),
+                    g2.getDistributionId() == null ? null : g2.getDistributionId().toString(),
                     g2.isBlocked(),
                     g2.isPermissionDenied());
         }).toList());
@@ -334,7 +340,9 @@ public class GroupStore {
             }
         }
 
-        private record GroupV2(String groupId, String masterKey, boolean blocked, boolean permissionDenied) {}
+        private record GroupV2(
+                String groupId, String masterKey, String distributionId, boolean blocked, boolean permissionDenied
+        ) {}
     }
 
     private static class GroupsDeserializer extends JsonDeserializer<List<Object>> {
index 0330b3b80f930aba5808bb7b78b530bff221ec67..d7b25d237a6c855baceef466526d71e4bfd4cffd 100644 (file)
@@ -120,16 +120,20 @@ public class IdentityKeyStore implements org.whispersystems.libsignal.state.Iden
         var recipientId = resolveRecipient(address.getName());
 
         synchronized (cachedIdentities) {
-            final var identityInfo = loadIdentityLocked(recipientId);
+            // TODO implement possibility for different handling of incoming/outgoing trust decisions
+            var identityInfo = loadIdentityLocked(recipientId);
             if (identityInfo == null) {
                 // Identity not found
+                saveIdentity(address, identityKey);
                 return trustNewIdentity == TrustNewIdentity.ON_FIRST_USE;
             }
 
-            // TODO implement possibility for different handling of incoming/outgoing trust decisions
             if (!identityInfo.getIdentityKey().equals(identityKey)) {
                 // Identity found, but different
-                return false;
+                if (direction == Direction.SENDING) {
+                    saveIdentity(address, identityKey);
+                    identityInfo = loadIdentityLocked(recipientId);
+                }
             }
 
             return identityInfo.isTrusted();
index 9b012e905edc9c75b027d18f4e1a4a583751c397..340a55efbca45769d008a6198d7c7d9ef450967f 100644 (file)
@@ -59,7 +59,28 @@ public class SenderKeyRecordStore implements org.whispersystems.libsignal.groups
         }
     }
 
-    public void deleteAll() {
+    long getCreateTimeForKey(final RecipientId selfRecipientId, final int selfDeviceId, final UUID distributionId) {
+        final var key = getKey(selfRecipientId, selfDeviceId, distributionId);
+        final var senderKeyFile = getSenderKeyFile(key);
+
+        if (!senderKeyFile.exists()) {
+            return -1;
+        }
+
+        return IOUtils.getFileCreateTime(senderKeyFile);
+    }
+
+    void deleteSenderKey(final RecipientId recipientId, final UUID distributionId) {
+        synchronized (cachedSenderKeys) {
+            cachedSenderKeys.clear();
+            final var keys = getKeysLocked(recipientId);
+            for (var key : keys) {
+                if (key.distributionId.equals(distributionId)) deleteSenderKeyLocked(key);
+            }
+        }
+    }
+
+    void deleteAll() {
         synchronized (cachedSenderKeys) {
             cachedSenderKeys.clear();
             final var files = senderKeysPath.listFiles((_file, s) -> senderKeyFileNamePattern.matcher(s).matches());
@@ -77,7 +98,7 @@ public class SenderKeyRecordStore implements org.whispersystems.libsignal.groups
         }
     }
 
-    public void deleteAllFor(final RecipientId recipientId) {
+    void deleteAllFor(final RecipientId recipientId) {
         synchronized (cachedSenderKeys) {
             cachedSenderKeys.clear();
             final var keys = getKeysLocked(recipientId);
@@ -87,7 +108,7 @@ public class SenderKeyRecordStore implements org.whispersystems.libsignal.groups
         }
     }
 
-    public void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
+    void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
         synchronized (cachedSenderKeys) {
             final var keys = getKeysLocked(toBeMergedRecipientId);
             final var otherHasSenderKeys = keys.size() > 0;
@@ -120,6 +141,10 @@ public class SenderKeyRecordStore implements org.whispersystems.libsignal.groups
         return resolver.resolveRecipient(identifier);
     }
 
+    private Key getKey(final RecipientId recipientId, int deviceId, final UUID distributionId) {
+        return new Key(recipientId, deviceId, distributionId);
+    }
+
     private Key getKey(final SignalProtocolAddress address, final UUID distributionId) {
         final var recipientId = resolveRecipient(address.getName());
         return new Key(recipientId, address.getDeviceId(), distributionId);
@@ -217,7 +242,5 @@ public class SenderKeyRecordStore implements org.whispersystems.libsignal.groups
         }
     }
 
-    private record Key(RecipientId recipientId, int deviceId, UUID distributionId) {
-
-    }
+    private record Key(RecipientId recipientId, int deviceId, UUID distributionId) {}
 }
index c2447d48c99f10f59b8a60050f6477b5a5f92c37..432341651c3fa8da0f83d1b38517647b221ca396 100644 (file)
@@ -25,13 +25,14 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.UUID;
 import java.util.stream.Collectors;
 
 public class SenderKeySharedStore {
 
     private final static Logger logger = LoggerFactory.getLogger(SenderKeySharedStore.class);
 
-    private final Map<DistributionId, Set<SenderKeySharedEntry>> sharedSenderKeys;
+    private final Map<UUID, Set<SenderKeySharedEntry>> sharedSenderKeys;
 
     private final ObjectMapper objectMapper;
     private final File file;
@@ -45,19 +46,18 @@ public class SenderKeySharedStore {
         final var objectMapper = Utils.createStorageObjectMapper();
         try (var inputStream = new FileInputStream(file)) {
             final var storage = objectMapper.readValue(inputStream, Storage.class);
-            final var sharedSenderKeys = new HashMap<DistributionId, Set<SenderKeySharedEntry>>();
+            final var sharedSenderKeys = new HashMap<UUID, Set<SenderKeySharedEntry>>();
             for (final var senderKey : storage.sharedSenderKeys) {
                 final var recipientId = resolver.resolveRecipient(senderKey.recipientId);
                 if (recipientId == null) {
                     continue;
                 }
                 final var entry = new SenderKeySharedEntry(recipientId, senderKey.deviceId);
-                final var uuid = UuidUtil.parseOrNull(senderKey.distributionId);
-                if (uuid == null) {
+                final var distributionId = UuidUtil.parseOrNull(senderKey.distributionId);
+                if (distributionId == 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<>();
@@ -74,7 +74,7 @@ public class SenderKeySharedStore {
     }
 
     private SenderKeySharedStore(
-            final Map<DistributionId, Set<SenderKeySharedEntry>> sharedSenderKeys,
+            final Map<UUID, Set<SenderKeySharedEntry>> sharedSenderKeys,
             final ObjectMapper objectMapper,
             final File file,
             final RecipientAddressResolver addressResolver,
@@ -89,8 +89,11 @@ public class SenderKeySharedStore {
 
     public Set<SignalProtocolAddress> getSenderKeySharedWith(final DistributionId distributionId) {
         synchronized (sharedSenderKeys) {
-            return sharedSenderKeys.get(distributionId)
-                    .stream()
+            final var addresses = sharedSenderKeys.get(distributionId.asUuid());
+            if (addresses == null) {
+                return Set.of();
+            }
+            return addresses.stream()
                     .map(k -> new SignalProtocolAddress(addressResolver.resolveRecipientAddress(k.recipientId())
                             .getIdentifier(), k.deviceId()))
                     .collect(Collectors.toSet());
@@ -105,9 +108,9 @@ public class SenderKeySharedStore {
                 .collect(Collectors.toSet());
 
         synchronized (sharedSenderKeys) {
-            final var previousEntries = sharedSenderKeys.getOrDefault(distributionId, Set.of());
+            final var previousEntries = sharedSenderKeys.getOrDefault(distributionId.asUuid(), Set.of());
 
-            sharedSenderKeys.put(distributionId, new HashSet<>() {
+            sharedSenderKeys.put(distributionId.asUuid(), new HashSet<>() {
                 {
                     addAll(previousEntries);
                     addAll(newEntries);
@@ -158,6 +161,13 @@ public class SenderKeySharedStore {
         }
     }
 
+    public void deleteAllFor(final DistributionId distributionId) {
+        synchronized (sharedSenderKeys) {
+            sharedSenderKeys.remove(distributionId.asUuid());
+            saveLocked();
+        }
+    }
+
     public void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
         synchronized (sharedSenderKeys) {
             for (final var distributionId : sharedSenderKeys.keySet()) {
@@ -187,7 +197,7 @@ public class SenderKeySharedStore {
             return sharedWith.stream()
                     .map(entry -> new Storage.SharedSenderKey(entry.recipientId().id(),
                             entry.deviceId(),
-                            pair.getKey().asUuid().toString()));
+                            pair.getKey().toString()));
         }).toList());
 
         // Write to memory first to prevent corrupting the file in case of serialization errors
index ba2a032217823a9b7c8b5867f0c650d8b5a2e230..3f08c389310da46a41c6f2bf0698193866b24413 100644 (file)
@@ -68,6 +68,19 @@ public class SenderKeyStore implements SignalServiceSenderKeyStore {
         senderKeyRecordStore.deleteAllFor(recipientId);
     }
 
+    public void deleteSharedWith(RecipientId recipientId) {
+        senderKeySharedStore.deleteAllFor(recipientId);
+    }
+
+    public void deleteOurKey(RecipientId selfRecipientId, DistributionId distributionId) {
+        senderKeySharedStore.deleteAllFor(distributionId);
+        senderKeyRecordStore.deleteSenderKey(selfRecipientId, distributionId.asUuid());
+    }
+
+    public long getCreateTimeForOurKey(RecipientId selfRecipientId, int deviceId, DistributionId distributionId) {
+        return senderKeyRecordStore.getCreateTimeForKey(selfRecipientId, deviceId, distributionId.asUuid());
+    }
+
     public void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
         senderKeySharedStore.mergeRecipients(recipientId, toBeMergedRecipientId);
         senderKeyRecordStore.mergeRecipients(recipientId, toBeMergedRecipientId);
index 3cc708d8708d8cd0e8a6062e833d50856fe466ad..eebe64512601fa5b99940f159d121befae544a93 100644 (file)
@@ -7,6 +7,8 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.file.Files;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileTime;
 import java.nio.file.attribute.PosixFilePermission;
 import java.nio.file.attribute.PosixFilePermissions;
 import java.util.EnumSet;
@@ -72,4 +74,14 @@ public class IOUtils {
             output.write(buffer, 0, read);
         }
     }
+
+    public static long getFileCreateTime(final File file) {
+        try {
+            BasicFileAttributes attr = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
+            FileTime fileTime = attr.creationTime();
+            return fileTime.toMillis();
+        } catch (IOException ex) {
+            return -1;
+        }
+    }
 }