]> nmode's Git Repositories - signal-cli/commitdiff
Migrate local group to v2 if another member has migrated it
authorAsamK <asamk@gmx.de>
Sat, 12 Dec 2020 10:14:36 +0000 (11:14 +0100)
committerAsamK <asamk@gmx.de>
Sat, 12 Dec 2020 10:42:38 +0000 (11:42 +0100)
src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java
src/main/java/org/asamk/signal/ReceiveMessageHandler.java
src/main/java/org/asamk/signal/manager/GroupUtils.java
src/main/java/org/asamk/signal/manager/Manager.java
src/main/java/org/asamk/signal/manager/ServiceConfig.java
src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java
src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java

index 55b4841ed200c3760eb8ed27374521176ba95b6d..41b91a4869714ff7ea8733a635c101c1fee2d556 100644 (file)
@@ -1,6 +1,7 @@
 package org.asamk.signal;
 
 import org.asamk.Signal;
+import org.asamk.signal.manager.GroupUtils;
 import org.asamk.signal.manager.Manager;
 import org.freedesktop.dbus.connections.impl.DBusConnection;
 import org.freedesktop.dbus.exceptions.DBusException;
@@ -117,7 +118,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
             if (message.getGroupContext().get().getGroupV1().isPresent()) {
                 groupId = message.getGroupContext().get().getGroupV1().get().getGroupId();
             } else if (message.getGroupContext().get().getGroupV2().isPresent()) {
-                groupId = m.getGroupId(message.getGroupContext().get().getGroupV2().get().getMasterKey());
+                groupId = GroupUtils.getGroupId(message.getGroupContext().get().getGroupV2().get().getMasterKey());
             } else {
                 groupId = null;
             }
index 2e9654a0efbc3529a66bc429102ae41217966ad6..5925b2b8398141e086fa787e0be13ba953866c2b 100644 (file)
@@ -1,5 +1,6 @@
 package org.asamk.signal;
 
+import org.asamk.signal.manager.GroupUtils;
 import org.asamk.signal.manager.Manager;
 import org.asamk.signal.storage.contacts.ContactInfo;
 import org.asamk.signal.storage.groups.GroupInfo;
@@ -380,7 +381,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
                 }
             } else if (groupContext.getGroupV2().isPresent()) {
                 final SignalServiceGroupV2 groupInfo = groupContext.getGroupV2().get();
-                byte[] groupId = m.getGroupId(groupInfo.getMasterKey());
+                byte[] groupId = GroupUtils.getGroupId(groupInfo.getMasterKey());
                 System.out.println("  Id: " + Base64.encodeBytes(groupId));
                 GroupInfo group = m.getGroup(groupId);
                 if (group != null) {
index f4398f94294116ed7e94f2f835cf46183577c823..0d192002e70975ea6778d25c7fbd40256ac2be00 100644 (file)
@@ -3,6 +3,10 @@ package org.asamk.signal.manager;
 import org.asamk.signal.storage.groups.GroupInfo;
 import org.asamk.signal.storage.groups.GroupInfoV1;
 import org.asamk.signal.storage.groups.GroupInfoV2;
+import org.signal.zkgroup.InvalidInputException;
+import org.signal.zkgroup.groups.GroupMasterKey;
+import org.signal.zkgroup.groups.GroupSecretParams;
+import org.whispersystems.libsignal.kdf.HKDFv3;
 import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
 import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
 import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
@@ -25,4 +29,19 @@ public class GroupUtils {
             messageBuilder.asGroupMessage(group);
         }
     }
+
+    public static byte[] getGroupId(GroupMasterKey groupMasterKey) {
+        final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
+        return groupSecretParams.getPublicParams().getGroupIdentifier().serialize();
+    }
+
+    public static GroupMasterKey deriveV2MigrationMasterKey(byte[] groupId) {
+        try {
+            return new GroupMasterKey(new HKDFv3().deriveSecrets(groupId,
+                    "GV2 Migration".getBytes(),
+                    GroupMasterKey.SIZE));
+        } catch (InvalidInputException e) {
+            throw new AssertionError(e);
+        }
+    }
 }
index 01aa9b480d6a18c84df47418b3824e5e92b349de..909100855784f08e1a3c1d0f17e065db6c80eeaa 100644 (file)
@@ -865,7 +865,7 @@ public class Manager implements Closeable {
         GroupInfoV1 g;
         GroupInfo group = getGroupForSending(groupId);
         if (!(group instanceof GroupInfoV1)) {
-            throw new RuntimeException("TODO Not implemented!");
+            throw new RuntimeException("Received an invalid group request for a v2 group!");
         }
         g = (GroupInfoV1) group;
 
@@ -1450,7 +1450,7 @@ public class Manager implements Closeable {
         if (message.getGroupContext().isPresent()) {
             if (message.getGroupContext().get().getGroupV1().isPresent()) {
                 SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
-                GroupInfo group = account.getGroupStore().getGroup(groupInfo.getGroupId());
+                GroupInfo group = account.getGroupStore().getGroupByV1Id(groupInfo.getGroupId());
                 if (group == null || group instanceof GroupInfoV1) {
                     GroupInfoV1 groupV1 = (GroupInfoV1) group;
                     switch (groupInfo.getType()) {
@@ -1505,7 +1505,7 @@ public class Manager implements Closeable {
                             break;
                     }
                 } else {
-                    System.err.println("Received a group v1 message for a v2 group: " + group.getTitle());
+                    // Received a group v1 message for a v2 group
                 }
             }
             if (message.getGroupContext().get().getGroupV2().isPresent()) {
@@ -1515,9 +1515,18 @@ public class Manager implements Closeable {
                 final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
 
                 byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize();
-                GroupInfo groupInfo = account.getGroupStore().getGroup(groupId);
+                GroupInfo groupInfo = account.getGroupStore().getGroupByV2Id(groupId);
                 if (groupInfo instanceof GroupInfoV1) {
-                    // TODO upgrade group
+                    // Received a v2 group message for a v2 group, we need to locally migrate the group
+                    account.getGroupStore().deleteGroup(groupInfo.groupId);
+                    GroupInfoV2 groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey);
+                    groupInfoV2.setGroup(getDecryptedGroup(groupSecretParams));
+                    account.getGroupStore().updateGroup(groupInfoV2);
+                    System.err.println("Locally migrated group "
+                            + Base64.encodeBytes(groupInfo.groupId)
+                            + " to group v2, id: "
+                            + Base64.encodeBytes(groupInfoV2.groupId)
+                            + " !!!");
                 } else if (groupInfo == null || groupInfo instanceof GroupInfoV2) {
                     GroupInfoV2 groupInfoV2 = groupInfo == null
                             ? new GroupInfoV2(groupId, groupMasterKey)
@@ -1526,26 +1535,7 @@ public class Manager implements Closeable {
                     if (groupInfoV2.getGroup() == null
                             || groupInfoV2.getGroup().getRevision() < groupContext.getRevision()) {
                         // TODO check if revision is only 1 behind and a signedGroupChange is available
-                        try {
-                            final GroupsV2AuthorizationString groupsV2AuthorizationString = getGroupAuthForToday(
-                                    groupSecretParams);
-                            final DecryptedGroup group = groupsV2Api.getGroup(groupSecretParams,
-                                    groupsV2AuthorizationString);
-                            groupInfoV2.setGroup(group);
-                            for (DecryptedMember member : group.getMembersList()) {
-                                final SignalServiceAddress address = resolveSignalServiceAddress(new SignalServiceAddress(
-                                        UuidUtil.parseOrThrow(member.getUuid().toByteArray()),
-                                        null));
-                                try {
-                                    account.getProfileStore()
-                                            .storeProfileKey(address,
-                                                    new ProfileKey(member.getProfileKey().toByteArray()));
-                                } catch (InvalidInputException ignored) {
-                                }
-                            }
-                        } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
-                            System.err.println("Failed to retrieve Group V2 info, ignoring ...");
-                        }
+                        groupInfoV2.setGroup(getDecryptedGroup(groupSecretParams));
                         account.getGroupStore().updateGroup(groupInfoV2);
                     }
                 }
@@ -1633,6 +1623,26 @@ public class Manager implements Closeable {
         return actions;
     }
 
+    private DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) {
+        try {
+            final GroupsV2AuthorizationString groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams);
+            DecryptedGroup group = groupsV2Api.getGroup(groupSecretParams, groupsV2AuthorizationString);
+            for (DecryptedMember member : group.getMembersList()) {
+                final SignalServiceAddress address = resolveSignalServiceAddress(new SignalServiceAddress(UuidUtil.parseOrThrow(
+                        member.getUuid().toByteArray()), null));
+                try {
+                    account.getProfileStore()
+                            .storeProfileKey(address, new ProfileKey(member.getProfileKey().toByteArray()));
+                } catch (InvalidInputException ignored) {
+                }
+            }
+            return group;
+        } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
+            System.err.println("Failed to retrieve Group V2 info, ignoring ...");
+            return null;
+        }
+    }
+
     private void retryFailedReceivedMessages(
             ReceiveMessageHandler handler, boolean ignoreAttachments
     ) {
@@ -2314,11 +2324,6 @@ public class Manager implements Closeable {
         return account.getGroupStore().getGroup(groupId);
     }
 
-    public byte[] getGroupId(GroupMasterKey groupMasterKey) {
-        final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
-        return groupSecretParams.getPublicParams().getGroupIdentifier().serialize();
-    }
-
     public List<JsonIdentityKeyStore.Identity> getIdentities() {
         return account.getSignalProtocolStore().getIdentities();
     }
index 490dc51460057e65ec7c1c4082b048d36051be15..7f7d5570062d63367abb4855b6e79747ee73dc35 100644 (file)
@@ -57,7 +57,7 @@ public class ServiceConfig {
         } catch (Throwable ignored) {
             zkGroupAvailable = false;
         }
-        capabilities = new AccountAttributes.Capabilities(false, zkGroupAvailable, false, false);
+        capabilities = new AccountAttributes.Capabilities(false, zkGroupAvailable, false, zkGroupAvailable);
     }
 
     public static SignalServiceConfiguration createDefaultServiceConfiguration(String userAgent) {
index adf4f5553acc77a231a9ed337bbb658a5f0a5199..42c40e949226d1e4376cd4816b34ec3872469046 100644 (file)
@@ -25,6 +25,9 @@ public class GroupInfoV1 extends GroupInfo {
 
     private static final ObjectMapper jsonProcessor = new ObjectMapper();
 
+    @JsonProperty
+    public byte[] expectedV2Id;
+
     @JsonProperty
     public String name;
 
@@ -54,6 +57,7 @@ public class GroupInfoV1 extends GroupInfo {
 
     public GroupInfoV1(
             @JsonProperty("groupId") byte[] groupId,
+            @JsonProperty("expectedV2Id") byte[] expectedV2Id,
             @JsonProperty("name") String name,
             @JsonProperty("members") Collection<SignalServiceAddress> members,
             @JsonProperty("avatarId") long _ignored_avatarId,
@@ -65,6 +69,7 @@ public class GroupInfoV1 extends GroupInfo {
             @JsonProperty("active") boolean _ignored_active
     ) {
         super(groupId);
+        this.expectedV2Id = expectedV2Id;
         this.name = name;
         this.members.addAll(members);
         this.color = color;
index 570282bf3affe9760864651cae06943baff62f21..2175e293cd1c55f99012ff7299bc7ed1c4b5a663 100644 (file)
@@ -12,6 +12,7 @@ import com.fasterxml.jackson.databind.SerializerProvider;
 import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
 import com.fasterxml.jackson.databind.annotation.JsonSerialize;
 
+import org.asamk.signal.manager.GroupUtils;
 import org.asamk.signal.util.Hex;
 import org.asamk.signal.util.IOUtils;
 import org.signal.storageservice.protos.groups.local.DecryptedGroup;
@@ -24,6 +25,7 @@ import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
@@ -60,8 +62,38 @@ public class JsonGroupStore {
         }
     }
 
+    public void deleteGroup(byte[] groupId) {
+        groups.remove(Base64.encodeBytes(groupId));
+    }
+
     public GroupInfo getGroup(byte[] groupId) {
         final GroupInfo group = groups.get(Base64.encodeBytes(groupId));
+        if (group == null & groupId.length == 16) {
+            return getGroupByV1Id(groupId);
+        }
+        loadDecryptedGroup(group);
+        return group;
+    }
+
+    public GroupInfo getGroupByV1Id(byte[] groupIdV1) {
+        GroupInfo group = groups.get(Base64.encodeBytes(groupIdV1));
+        if (group == null) {
+            group = groups.get(Base64.encodeBytes(GroupUtils.getGroupId(GroupUtils.deriveV2MigrationMasterKey(groupIdV1))));
+        }
+        loadDecryptedGroup(group);
+        return group;
+    }
+
+    public GroupInfo getGroupByV2Id(byte[] groupIdV2) {
+        GroupInfo group = groups.get(Base64.encodeBytes(groupIdV2));
+        if (group == null) {
+            for (GroupInfo g : groups.values()) {
+                if (g instanceof GroupInfoV1 && Arrays.equals(groupIdV2, ((GroupInfoV1) g).expectedV2Id)) {
+                    group = g;
+                    break;
+                }
+            }
+        }
         loadDecryptedGroup(group);
         return group;
     }
@@ -147,7 +179,11 @@ public class JsonGroupStore {
                     }
                     g.setBlocked(n.get("blocked").asBoolean(false));
                 } else {
-                    g = jsonProcessor.treeToValue(n, GroupInfoV1.class);
+                    GroupInfoV1 gv1 = jsonProcessor.treeToValue(n, GroupInfoV1.class);
+                    if (gv1.expectedV2Id == null) {
+                        gv1.expectedV2Id = GroupUtils.getGroupId(GroupUtils.deriveV2MigrationMasterKey(gv1.groupId));
+                    }
+                    g = gv1;
                 }
                 groups.put(Base64.encodeBytes(g.groupId), g);
             }