]> nmode's Git Repositories - signal-cli/commitdiff
Implement updating of v2 groups
authorAsamK <asamk@gmx.de>
Sun, 13 Dec 2020 11:01:18 +0000 (12:01 +0100)
committerAsamK <asamk@gmx.de>
Sun, 13 Dec 2020 11:01:18 +0000 (12:01 +0100)
src/main/java/org/asamk/signal/manager/Manager.java
src/main/java/org/asamk/signal/manager/helper/GroupAuthorizationProvider.java [new file with mode: 0644]
src/main/java/org/asamk/signal/manager/helper/GroupHelper.java

index 000455ac169740d0ec9ad6b9099030f3dc708b92..887f9e425642e21f1b024fb103d33ce9b3a07260 100644 (file)
@@ -43,6 +43,7 @@ import org.signal.libsignal.metadata.ProtocolLegacyMessageException;
 import org.signal.libsignal.metadata.ProtocolNoSessionException;
 import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
 import org.signal.libsignal.metadata.SelfSendException;
+import org.signal.storageservice.protos.groups.GroupChange;
 import org.signal.storageservice.protos.groups.local.DecryptedGroup;
 import org.signal.storageservice.protos.groups.local.DecryptedMember;
 import org.signal.zkgroup.InvalidInputException;
@@ -213,7 +214,9 @@ public class Manager implements Closeable {
         this.groupHelper = new GroupHelper(this::getRecipientProfileKeyCredential,
                 this::getRecipientProfile,
                 account::getSelfAddress,
-                groupsV2Operations);
+                groupsV2Operations,
+                groupsV2Api,
+                this::getGroupAuthForToday);
     }
 
     public String getUsername() {
@@ -752,36 +755,6 @@ public class Manager implements Closeable {
         return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
     }
 
-    private GroupInfoV2 createGroupV2(
-            String name, Collection<SignalServiceAddress> members, InputStream avatar
-    ) throws IOException {
-        byte[] avatarBytes = avatar == null ? null : IOUtils.readFully(avatar);
-        final GroupsV2Operations.NewGroup newGroup = groupHelper.createGroupV2(name, members, avatarBytes);
-        final GroupSecretParams groupSecretParams = newGroup.getGroupSecretParams();
-
-        final GroupsV2AuthorizationString groupAuthForToday;
-        final DecryptedGroup decryptedGroup;
-        try {
-            groupAuthForToday = getGroupAuthForToday(groupSecretParams);
-            groupsV2Api.putNewGroup(newGroup, groupAuthForToday);
-            decryptedGroup = groupsV2Api.getGroup(groupSecretParams, groupAuthForToday);
-        } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
-            System.err.println("Failed to create V2 group: " + e.getMessage());
-            return null;
-        }
-        if (decryptedGroup == null) {
-            System.err.println("Failed to create V2 group!");
-            return null;
-        }
-
-        final byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize();
-        final GroupMasterKey masterKey = groupSecretParams.getMasterKey();
-        GroupInfoV2 g = new GroupInfoV2(groupId, masterKey);
-        g.setGroup(decryptedGroup);
-
-        return g;
-    }
-
     private Pair<byte[], List<SendMessageResult>> sendUpdateGroupMessage(
             byte[] groupId, String name, Collection<SignalServiceAddress> members, String avatarFile
     ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
@@ -789,8 +762,7 @@ public class Manager implements Closeable {
         SignalServiceDataMessage.Builder messageBuilder;
         if (groupId == null) {
             // Create new group
-            InputStream avatar = avatarFile == null ? null : new FileInputStream(avatarFile);
-            GroupInfoV2 gv2 = createGroupV2(name, members, avatar);
+            GroupInfoV2 gv2 = groupHelper.createGroupV2(name, members, avatarFile);
             if (gv2 == null) {
                 GroupInfoV1 gv1 = new GroupInfoV1(KeyUtils.createGroupId());
                 gv1.addMembers(Collections.singleton(account.getSelfAddress()));
@@ -798,18 +770,41 @@ public class Manager implements Closeable {
                 messageBuilder = getGroupUpdateMessageBuilder(gv1);
                 g = gv1;
             } else {
-                messageBuilder = getGroupUpdateMessageBuilder(gv2);
+                messageBuilder = getGroupUpdateMessageBuilder(gv2, null);
                 g = gv2;
             }
         } else {
             GroupInfo group = getGroupForSending(groupId);
-            if (!(group instanceof GroupInfoV1)) {
-                throw new RuntimeException("TODO Not implemented!");
+            if (group instanceof GroupInfoV2) {
+                Pair<DecryptedGroup, GroupChange> groupGroupChangePair = null;
+                if (members != null) {
+                    final Set<SignalServiceAddress> newMembers = new HashSet<>(members);
+                    newMembers.removeAll(group.getMembers());
+                    if (newMembers.size() > 0) {
+                        groupGroupChangePair = groupHelper.updateGroupV2((GroupInfoV2) group, newMembers);
+                    }
+                }
+                if (groupGroupChangePair == null || name != null || avatarFile != null) {
+                    if (groupGroupChangePair != null) {
+                        ((GroupInfoV2) group).setGroup(groupGroupChangePair.first());
+                        messageBuilder = getGroupUpdateMessageBuilder((GroupInfoV2) group,
+                                groupGroupChangePair.second().toByteArray());
+                        sendMessage(messageBuilder, group.getMembersWithout(account.getSelfAddress()));
+                    }
+
+                    groupGroupChangePair = groupHelper.updateGroupV2((GroupInfoV2) group, name, avatarFile);
+                }
+
+                ((GroupInfoV2) group).setGroup(groupGroupChangePair.first());
+                messageBuilder = getGroupUpdateMessageBuilder((GroupInfoV2) group,
+                        groupGroupChangePair.second().toByteArray());
+                g = group;
+            } else {
+                GroupInfoV1 gv1 = (GroupInfoV1) group;
+                updateGroupV1(gv1, name, members, avatarFile);
+                messageBuilder = getGroupUpdateMessageBuilder(gv1);
+                g = gv1;
             }
-            GroupInfoV1 gv1 = (GroupInfoV1) group;
-            updateGroupV1(gv1, name, members, avatarFile);
-            messageBuilder = getGroupUpdateMessageBuilder(gv1);
-            g = gv1;
         }
 
         account.getGroupStore().updateGroup(g);
@@ -899,11 +894,10 @@ public class Manager implements Closeable {
                 .withExpiration(g.getMessageExpirationTime());
     }
 
-    private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g) {
+    private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) {
         SignalServiceGroupV2.Builder group = SignalServiceGroupV2.newBuilder(g.getMasterKey())
                 .withRevision(g.getGroup().getRevision())
-//                .withSignedGroupChange() // TODO
-                ;
+                .withSignedGroupChange(signedGroupChange);
         return SignalServiceDataMessage.newBuilder()
                 .asGroupMessage(group.build())
                 .withExpiration(g.getMessageExpirationTime());
@@ -1427,16 +1421,20 @@ public class Manager implements Closeable {
 
     private GroupsV2AuthorizationString getGroupAuthForToday(
             final GroupSecretParams groupSecretParams
-    ) throws IOException, VerificationFailedException {
+    ) throws IOException {
         final int today = currentTimeDays();
         // Returns credentials for the next 7 days
         final HashMap<Integer, AuthCredentialResponse> credentials = groupsV2Api.getCredentials(today);
         // TODO cache credentials until they expire
         AuthCredentialResponse authCredentialResponse = credentials.get(today);
-        return groupsV2Api.getGroupsV2AuthorizationString(account.getUuid(),
-                today,
-                groupSecretParams,
-                authCredentialResponse);
+        try {
+            return groupsV2Api.getGroupsV2AuthorizationString(account.getUuid(),
+                    today,
+                    groupSecretParams,
+                    authCredentialResponse);
+        } catch (VerificationFailedException e) {
+            throw new IOException(e);
+        }
     }
 
     private List<HandleAction> handleSignalServiceDataMessage(
diff --git a/src/main/java/org/asamk/signal/manager/helper/GroupAuthorizationProvider.java b/src/main/java/org/asamk/signal/manager/helper/GroupAuthorizationProvider.java
new file mode 100644 (file)
index 0000000..d26ebb0
--- /dev/null
@@ -0,0 +1,11 @@
+package org.asamk.signal.manager.helper;
+
+import org.signal.zkgroup.groups.GroupSecretParams;
+import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
+
+import java.io.IOException;
+
+public interface GroupAuthorizationProvider {
+
+    GroupsV2AuthorizationString getAuthorizationForToday(GroupSecretParams groupSecretParams) throws IOException;
+}
index 334cacd8d88320df63a4cd32dd7ab04062777567..153f8cd0d45db157bab7e5dbafd514e652967c69 100644 (file)
@@ -2,6 +2,8 @@ package org.asamk.signal.manager.helper;
 
 import com.google.protobuf.InvalidProtocolBufferException;
 
+import org.asamk.signal.storage.groups.GroupInfoV2;
+import org.asamk.signal.util.IOUtils;
 import org.signal.storageservice.protos.groups.GroupChange;
 import org.signal.storageservice.protos.groups.Member;
 import org.signal.storageservice.protos.groups.local.DecryptedGroup;
@@ -10,16 +12,24 @@ import org.signal.zkgroup.VerificationFailedException;
 import org.signal.zkgroup.groups.GroupMasterKey;
 import org.signal.zkgroup.groups.GroupSecretParams;
 import org.signal.zkgroup.profiles.ProfileKeyCredential;
+import org.whispersystems.libsignal.util.Pair;
 import org.whispersystems.libsignal.util.guava.Optional;
 import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
 import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
+import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
+import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
 import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
 import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
 import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException;
 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.whispersystems.signalservice.api.util.UuidUtil;
 
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
 import java.util.Collection;
 import java.util.Set;
+import java.util.UUID;
 import java.util.stream.Collectors;
 
 public class GroupHelper {
@@ -32,19 +42,69 @@ public class GroupHelper {
 
     private final GroupsV2Operations groupsV2Operations;
 
+    private final GroupsV2Api groupsV2Api;
+
+    private final GroupAuthorizationProvider groupAuthorizationProvider;
+
     public GroupHelper(
             final ProfileKeyCredentialProvider profileKeyCredentialProvider,
             final ProfileProvider profileProvider,
             final SelfAddressProvider selfAddressProvider,
-            final GroupsV2Operations groupsV2Operations
+            final GroupsV2Operations groupsV2Operations,
+            final GroupsV2Api groupsV2Api,
+            final GroupAuthorizationProvider groupAuthorizationProvider
     ) {
         this.profileKeyCredentialProvider = profileKeyCredentialProvider;
         this.profileProvider = profileProvider;
         this.selfAddressProvider = selfAddressProvider;
         this.groupsV2Operations = groupsV2Operations;
+        this.groupsV2Api = groupsV2Api;
+        this.groupAuthorizationProvider = groupAuthorizationProvider;
+    }
+
+    public GroupInfoV2 createGroupV2(
+            String name, Collection<SignalServiceAddress> members, String avatarFile
+    ) throws IOException {
+        final byte[] avatarBytes = readAvatarBytes(avatarFile);
+        final GroupsV2Operations.NewGroup newGroup = buildNewGroupV2(name, members, avatarBytes);
+        if (newGroup == null) {
+            return null;
+        }
+
+        final GroupSecretParams groupSecretParams = newGroup.getGroupSecretParams();
+
+        final GroupsV2AuthorizationString groupAuthForToday;
+        final DecryptedGroup decryptedGroup;
+        try {
+            groupAuthForToday = groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams);
+            groupsV2Api.putNewGroup(newGroup, groupAuthForToday);
+            decryptedGroup = groupsV2Api.getGroup(groupSecretParams, groupAuthForToday);
+        } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
+            System.err.println("Failed to create V2 group: " + e.getMessage());
+            return null;
+        }
+        if (decryptedGroup == null) {
+            System.err.println("Failed to create V2 group!");
+            return null;
+        }
+
+        final byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize();
+        final GroupMasterKey masterKey = groupSecretParams.getMasterKey();
+        GroupInfoV2 g = new GroupInfoV2(groupId, masterKey);
+        g.setGroup(decryptedGroup);
+
+        return g;
     }
 
-    public GroupsV2Operations.NewGroup createGroupV2(
+    private byte[] readAvatarBytes(final String avatarFile) throws IOException {
+        final byte[] avatarBytes;
+        try (InputStream avatar = avatarFile == null ? null : new FileInputStream(avatarFile)) {
+            avatarBytes = avatar == null ? null : IOUtils.readFully(avatar);
+        }
+        return avatarBytes;
+    }
+
+    private GroupsV2Operations.NewGroup buildNewGroupV2(
             String name, Collection<SignalServiceAddress> members, byte[] avatar
     ) {
         final ProfileKeyCredential profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(
@@ -90,6 +150,80 @@ public class GroupHelper {
                 0);
     }
 
+    public Pair<DecryptedGroup, GroupChange> updateGroupV2(
+            GroupInfoV2 groupInfoV2, String name, String avatarFile
+    ) throws IOException {
+        final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
+        GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
+
+        GroupChange.Actions.Builder change = name != null
+                ? groupOperations.createModifyGroupTitle(name)
+                : GroupChange.Actions.newBuilder();
+
+        if (avatarFile != null) {
+            final byte[] avatarBytes = readAvatarBytes(avatarFile);
+            String avatarCdnKey = groupsV2Api.uploadAvatar(avatarBytes,
+                    groupSecretParams,
+                    groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams));
+            change.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder().setAvatar(avatarCdnKey));
+        }
+
+        final Optional<UUID> uuid = this.selfAddressProvider.getSelfAddress().getUuid();
+        if (uuid.isPresent()) {
+            change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
+        }
+
+        return commitChange(groupInfoV2, change);
+    }
+
+    public Pair<DecryptedGroup, GroupChange> updateGroupV2(
+            GroupInfoV2 groupInfoV2, Set<SignalServiceAddress> newMembers
+    ) throws IOException {
+        final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
+        GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
+
+        Set<GroupCandidate> candidates = newMembers.stream()
+                .map(member -> new GroupCandidate(member.getUuid().get(),
+                        Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member))))
+                .collect(Collectors.toSet());
+
+        final GroupChange.Actions.Builder change = groupOperations.createModifyGroupMembershipChange(candidates,
+                selfAddressProvider.getSelfAddress().getUuid().get());
+
+        final Optional<UUID> uuid = this.selfAddressProvider.getSelfAddress().getUuid();
+        if (uuid.isPresent()) {
+            change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
+        }
+
+        return commitChange(groupInfoV2, change);
+    }
+
+    private Pair<DecryptedGroup, GroupChange> commitChange(
+            GroupInfoV2 groupInfoV2, GroupChange.Actions.Builder change
+    ) throws IOException {
+        final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
+        final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
+        final DecryptedGroup previousGroupState = groupInfoV2.getGroup();
+        final int nextRevision = previousGroupState.getRevision() + 1;
+        final GroupChange.Actions changeActions = change.setRevision(nextRevision).build();
+        final DecryptedGroupChange decryptedChange;
+        final DecryptedGroup decryptedGroupState;
+
+        try {
+            decryptedChange = groupOperations.decryptChange(changeActions,
+                    selfAddressProvider.getSelfAddress().getUuid().get());
+            decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange);
+        } catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
+            throw new IOException(e);
+        }
+
+        GroupChange signedGroupChange = groupsV2Api.patchGroup(change.build(),
+                groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams),
+                Optional.absent());
+
+        return new Pair<>(decryptedGroupState, signedGroupChange);
+    }
+
     public DecryptedGroup getUpdatedDecryptedGroup(
             DecryptedGroup group, byte[] signedGroupChange, GroupMasterKey groupMasterKey
     ) {