]> nmode's Git Repositories - signal-cli/commitdiff
Only update profile keys from authoritative group changes
authorAsamK <asamk@gmx.de>
Thu, 19 May 2022 10:23:35 +0000 (12:23 +0200)
committerAsamK <asamk@gmx.de>
Thu, 19 May 2022 10:55:37 +0000 (12:55 +0200)
lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java
lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java

index ab3e1264ca658483bbab6a8e63de093f4cf5965b..9346372c1924aa80d52b271c6393f23dbbbd1977 100644 (file)
@@ -31,9 +31,11 @@ import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
 import org.signal.libsignal.zkgroup.profiles.ProfileKey;
 import org.signal.storageservice.protos.groups.GroupChange;
 import org.signal.storageservice.protos.groups.local.DecryptedGroup;
+import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
 import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupHistoryEntry;
 import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
 import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
 import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
@@ -50,8 +52,10 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.file.Files;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 
@@ -123,12 +127,22 @@ public class GroupHelper {
             if (signedGroupChange != null
                     && groupInfoV2.getGroup() != null
                     && groupInfoV2.getGroup().getRevision() + 1 == revision) {
-                group = context.getGroupV2Helper()
-                        .getUpdatedDecryptedGroup(groupInfoV2.getGroup(), signedGroupChange, groupMasterKey);
+                final var decryptedGroupChange = context.getGroupV2Helper()
+                        .getDecryptedGroupChange(signedGroupChange, groupMasterKey);
+
+                if (decryptedGroupChange != null) {
+                    storeProfileKeyFromChange(decryptedGroupChange);
+                    group = context.getGroupV2Helper()
+                            .getUpdatedDecryptedGroup(groupInfoV2.getGroup(), decryptedGroupChange);
+                }
             }
             if (group == null) {
                 try {
                     group = context.getGroupV2Helper().getDecryptedGroup(groupSecretParams);
+
+                    if (group != null) {
+                        storeProfileKeysFromHistory(groupSecretParams, groupInfoV2, group);
+                    }
                 } catch (NotAGroupMemberException ignored) {
                 }
             }
@@ -373,6 +387,17 @@ public class GroupHelper {
                     groupInfoV2.setPermissionDenied(true);
                     decryptedGroup = null;
                 }
+                if (decryptedGroup != null) {
+                    try {
+                        storeProfileKeysFromHistory(groupSecretParams, groupInfoV2, decryptedGroup);
+                    } catch (NotAGroupMemberException ignored) {
+                    }
+                    storeProfileKeysFromMembers(decryptedGroup);
+                    final var avatar = decryptedGroup.getAvatar();
+                    if (avatar != null && !avatar.isEmpty()) {
+                        downloadGroupAvatar(groupInfoV2.getGroupId(), groupSecretParams, avatar);
+                    }
+                }
                 groupInfoV2.setGroup(decryptedGroup, account.getRecipientStore());
                 account.getGroupStore().updateGroup(group);
             }
@@ -417,14 +442,63 @@ public class GroupHelper {
         for (var member : group.getMembersList()) {
             final var serviceId = ServiceId.fromByteString(member.getUuid());
             final var recipientId = account.getRecipientStore().resolveRecipient(serviceId);
+            final var profileStore = account.getProfileStore();
+            if (profileStore.getProfileKey(recipientId) != null) {
+                // We already have a profile key, not updating it from a non-authoritative source
+                continue;
+            }
             try {
-                account.getProfileStore()
-                        .storeProfileKey(recipientId, new ProfileKey(member.getProfileKey().toByteArray()));
+                profileStore.storeProfileKey(recipientId, new ProfileKey(member.getProfileKey().toByteArray()));
             } catch (InvalidInputException ignored) {
             }
         }
     }
 
+    private void storeProfileKeyFromChange(final DecryptedGroupChange decryptedGroupChange) {
+        final var profileKeyFromChange = context.getGroupV2Helper()
+                .getAuthoritativeProfileKeyFromChange(decryptedGroupChange);
+
+        if (profileKeyFromChange != null) {
+            final var serviceId = profileKeyFromChange.first();
+            final var profileKey = profileKeyFromChange.second();
+            final var recipientId = account.getRecipientStore().resolveRecipient(serviceId);
+            account.getProfileStore().storeProfileKey(recipientId, profileKey);
+        }
+    }
+
+    private void storeProfileKeysFromHistory(
+            final GroupSecretParams groupSecretParams,
+            final GroupInfoV2 localGroup,
+            final DecryptedGroup newDecryptedGroup
+    ) throws NotAGroupMemberException {
+        final var revisionWeWereAdded = context.getGroupV2Helper().findRevisionWeWereAdded(newDecryptedGroup);
+        final var localRevision = localGroup.getGroup() == null ? 0 : localGroup.getGroup().getRevision();
+        var fromRevision = Math.max(revisionWeWereAdded, localRevision);
+        final var newProfileKeys = new HashMap<RecipientId, ProfileKey>();
+        while (true) {
+            final var page = context.getGroupV2Helper().getDecryptedGroupHistoryPage(groupSecretParams, fromRevision);
+            page.getResults()
+                    .stream()
+                    .map(DecryptedGroupHistoryEntry::getChange)
+                    .filter(Optional::isPresent)
+                    .map(Optional::get)
+                    .map(context.getGroupV2Helper()::getAuthoritativeProfileKeyFromChange)
+                    .filter(Objects::nonNull)
+                    .forEach(p -> {
+                        final var serviceId = p.first();
+                        final var profileKey = p.second();
+                        final var recipientId = account.getRecipientStore().resolveRecipient(serviceId);
+                        newProfileKeys.put(recipientId, profileKey);
+                    });
+            if (!page.getPagingData().hasMorePages()) {
+                break;
+            }
+            fromRevision = page.getPagingData().getNextPageRevision();
+        }
+
+        newProfileKeys.forEach(account.getProfileStore()::storeProfileKey);
+    }
+
     private GroupInfo getGroupForUpdating(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
         var g = getGroup(groupId);
         if (g == null) {
index 385acc5998d239f1138d09dc19b46c9e7de3462c..9b934580aaad0b293104e317866670de4e73bc2e 100644 (file)
@@ -1,5 +1,6 @@
 package org.asamk.signal.manager.helper;
 
+import com.google.protobuf.ByteString;
 import com.google.protobuf.InvalidProtocolBufferException;
 
 import org.asamk.signal.manager.SignalDependencies;
@@ -19,6 +20,7 @@ import org.signal.libsignal.zkgroup.auth.AuthCredentialResponse;
 import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
 import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
 import org.signal.libsignal.zkgroup.groups.UuidCiphertext;
+import org.signal.libsignal.zkgroup.profiles.ProfileKey;
 import org.signal.storageservice.protos.groups.AccessControl;
 import org.signal.storageservice.protos.groups.GroupChange;
 import org.signal.storageservice.protos.groups.Member;
@@ -27,10 +29,12 @@ import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
 import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
 import org.signal.storageservice.protos.groups.local.DecryptedMember;
 import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
+import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
 import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
+import org.whispersystems.signalservice.api.groupsv2.GroupHistoryPage;
 import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
 import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
 import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
@@ -40,6 +44,7 @@ import org.whispersystems.signalservice.api.push.ACI;
 import org.whispersystems.signalservice.api.push.ServiceId;
 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
 import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
+import org.whispersystems.signalservice.api.util.UuidUtil;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -53,7 +58,9 @@ import java.util.Optional;
 import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 class GroupV2Helper {
 
@@ -96,6 +103,35 @@ class GroupV2Helper {
                         getGroupAuthForToday(groupSecretParams));
     }
 
+    GroupHistoryPage getDecryptedGroupHistoryPage(
+            final GroupSecretParams groupSecretParams, int fromRevision
+    ) throws NotAGroupMemberException {
+        try {
+            final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams);
+            return dependencies.getGroupsV2Api()
+                    .getGroupHistoryPage(groupSecretParams, fromRevision, groupsV2AuthorizationString, false);
+        } catch (NonSuccessfulResponseCodeException e) {
+            if (e.getCode() == 403) {
+                throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
+            }
+            logger.warn("Failed to retrieve Group V2 history, ignoring: {}", e.getMessage());
+            return null;
+        } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
+            logger.warn("Failed to retrieve Group V2 history, ignoring: {}", e.getMessage());
+            return null;
+        }
+    }
+
+    int findRevisionWeWereAdded(DecryptedGroup partialDecryptedGroup) {
+        ByteString bytes = UuidUtil.toByteString(getSelfAci().uuid());
+        for (DecryptedMember decryptedMember : partialDecryptedGroup.getMembersList()) {
+            if (decryptedMember.getUuid().equals(bytes)) {
+                return decryptedMember.getJoinedAtRevision();
+            }
+        }
+        return partialDecryptedGroup.getRevision();
+    }
+
     Pair<GroupInfoV2, DecryptedGroup> createGroup(
             String name, Set<RecipientId> members, File avatarFile
     ) throws IOException {
@@ -522,21 +558,43 @@ class GroupV2Helper {
                         Optional.ofNullable(password).map(GroupLinkPassword::serialize));
     }
 
-    DecryptedGroup getUpdatedDecryptedGroup(
-            DecryptedGroup group, byte[] signedGroupChange, GroupMasterKey groupMasterKey
-    ) {
+    Pair<ServiceId, ProfileKey> getAuthoritativeProfileKeyFromChange(final DecryptedGroupChange change) {
+        UUID editor = UuidUtil.fromByteStringOrNull(change.getEditor());
+        final var editorProfileKeyBytes = Stream.concat(Stream.of(change.getNewMembersList().stream(),
+                                change.getPromotePendingMembersList().stream(),
+                                change.getModifiedProfileKeysList().stream())
+                        .flatMap(Function.identity())
+                        .filter(m -> UuidUtil.fromByteString(m.getUuid()).equals(editor))
+                        .map(DecryptedMember::getProfileKey),
+                change.getNewRequestingMembersList()
+                        .stream()
+                        .filter(m -> UuidUtil.fromByteString(m.getUuid()).equals(editor))
+                        .map(DecryptedRequestingMember::getProfileKey)).findFirst();
+
+        if (editorProfileKeyBytes.isEmpty()) {
+            return null;
+        }
+
+        ProfileKey profileKey;
+        try {
+            profileKey = new ProfileKey(editorProfileKeyBytes.get().toByteArray());
+        } catch (InvalidInputException e) {
+            logger.debug("Bad profile key in group");
+            return null;
+        }
+
+        return new Pair<>(ServiceId.from(editor), profileKey);
+    }
+
+    DecryptedGroup getUpdatedDecryptedGroup(DecryptedGroup group, DecryptedGroupChange decryptedGroupChange) {
         try {
-            final var decryptedGroupChange = getDecryptedGroupChange(signedGroupChange, groupMasterKey);
-            if (decryptedGroupChange == null) {
-                return null;
-            }
             return DecryptedGroupUtil.apply(group, decryptedGroupChange);
         } catch (NotAbleToApplyGroupV2ChangeException e) {
             return null;
         }
     }
 
-    private DecryptedGroupChange getDecryptedGroupChange(byte[] signedGroupChange, GroupMasterKey groupMasterKey) {
+    DecryptedGroupChange getDecryptedGroupChange(byte[] signedGroupChange, GroupMasterKey groupMasterKey) {
         if (signedGroupChange != null) {
             var groupOperations = dependencies.getGroupsV2Operations()
                     .forGroup(GroupSecretParams.deriveFromMasterKey(groupMasterKey));