From: AsamK Date: Sat, 12 Dec 2020 10:14:36 +0000 (+0100) Subject: Migrate local group to v2 if another member has migrated it X-Git-Tag: v0.7.0~5 X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/c10910e4660e96c24dce35455c31ec8056d9088c?hp=f6061f95dee516bcd9163460e5702bb73a5e763d Migrate local group to v2 if another member has migrated it --- diff --git a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java index 55b4841e..41b91a48 100644 --- a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java @@ -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; } diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 2e9654a0..5925b2b8 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -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) { diff --git a/src/main/java/org/asamk/signal/manager/GroupUtils.java b/src/main/java/org/asamk/signal/manager/GroupUtils.java index f4398f94..0d192002 100644 --- a/src/main/java/org/asamk/signal/manager/GroupUtils.java +++ b/src/main/java/org/asamk/signal/manager/GroupUtils.java @@ -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); + } + } } diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 01aa9b48..90910085 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -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 getIdentities() { return account.getSignalProtocolStore().getIdentities(); } diff --git a/src/main/java/org/asamk/signal/manager/ServiceConfig.java b/src/main/java/org/asamk/signal/manager/ServiceConfig.java index 490dc514..7f7d5570 100644 --- a/src/main/java/org/asamk/signal/manager/ServiceConfig.java +++ b/src/main/java/org/asamk/signal/manager/ServiceConfig.java @@ -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) { diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java b/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java index adf4f555..42c40e94 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java +++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java @@ -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 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; diff --git a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java index 570282bf..2175e293 100644 --- a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java +++ b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java @@ -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); }