mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 10:30:38 +00:00
Migrate local group to v2 if another member has migrated it
This commit is contained in:
parent
f6061f95de
commit
c10910e466
7 changed files with 101 additions and 34 deletions
|
@ -1,6 +1,7 @@
|
||||||
package org.asamk.signal;
|
package org.asamk.signal;
|
||||||
|
|
||||||
import org.asamk.Signal;
|
import org.asamk.Signal;
|
||||||
|
import org.asamk.signal.manager.GroupUtils;
|
||||||
import org.asamk.signal.manager.Manager;
|
import org.asamk.signal.manager.Manager;
|
||||||
import org.freedesktop.dbus.connections.impl.DBusConnection;
|
import org.freedesktop.dbus.connections.impl.DBusConnection;
|
||||||
import org.freedesktop.dbus.exceptions.DBusException;
|
import org.freedesktop.dbus.exceptions.DBusException;
|
||||||
|
@ -117,7 +118,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
|
||||||
if (message.getGroupContext().get().getGroupV1().isPresent()) {
|
if (message.getGroupContext().get().getGroupV1().isPresent()) {
|
||||||
groupId = message.getGroupContext().get().getGroupV1().get().getGroupId();
|
groupId = message.getGroupContext().get().getGroupV1().get().getGroupId();
|
||||||
} else if (message.getGroupContext().get().getGroupV2().isPresent()) {
|
} 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 {
|
} else {
|
||||||
groupId = null;
|
groupId = null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package org.asamk.signal;
|
package org.asamk.signal;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.GroupUtils;
|
||||||
import org.asamk.signal.manager.Manager;
|
import org.asamk.signal.manager.Manager;
|
||||||
import org.asamk.signal.storage.contacts.ContactInfo;
|
import org.asamk.signal.storage.contacts.ContactInfo;
|
||||||
import org.asamk.signal.storage.groups.GroupInfo;
|
import org.asamk.signal.storage.groups.GroupInfo;
|
||||||
|
@ -380,7 +381,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
|
||||||
}
|
}
|
||||||
} else if (groupContext.getGroupV2().isPresent()) {
|
} else if (groupContext.getGroupV2().isPresent()) {
|
||||||
final SignalServiceGroupV2 groupInfo = groupContext.getGroupV2().get();
|
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));
|
System.out.println(" Id: " + Base64.encodeBytes(groupId));
|
||||||
GroupInfo group = m.getGroup(groupId);
|
GroupInfo group = m.getGroup(groupId);
|
||||||
if (group != null) {
|
if (group != null) {
|
||||||
|
|
|
@ -3,6 +3,10 @@ package org.asamk.signal.manager;
|
||||||
import org.asamk.signal.storage.groups.GroupInfo;
|
import org.asamk.signal.storage.groups.GroupInfo;
|
||||||
import org.asamk.signal.storage.groups.GroupInfoV1;
|
import org.asamk.signal.storage.groups.GroupInfoV1;
|
||||||
import org.asamk.signal.storage.groups.GroupInfoV2;
|
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.SignalServiceDataMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
|
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
|
||||||
|
@ -25,4 +29,19 @@ public class GroupUtils {
|
||||||
messageBuilder.asGroupMessage(group);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -865,7 +865,7 @@ public class Manager implements Closeable {
|
||||||
GroupInfoV1 g;
|
GroupInfoV1 g;
|
||||||
GroupInfo group = getGroupForSending(groupId);
|
GroupInfo group = getGroupForSending(groupId);
|
||||||
if (!(group instanceof GroupInfoV1)) {
|
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;
|
g = (GroupInfoV1) group;
|
||||||
|
|
||||||
|
@ -1450,7 +1450,7 @@ public class Manager implements Closeable {
|
||||||
if (message.getGroupContext().isPresent()) {
|
if (message.getGroupContext().isPresent()) {
|
||||||
if (message.getGroupContext().get().getGroupV1().isPresent()) {
|
if (message.getGroupContext().get().getGroupV1().isPresent()) {
|
||||||
SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
|
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) {
|
if (group == null || group instanceof GroupInfoV1) {
|
||||||
GroupInfoV1 groupV1 = (GroupInfoV1) group;
|
GroupInfoV1 groupV1 = (GroupInfoV1) group;
|
||||||
switch (groupInfo.getType()) {
|
switch (groupInfo.getType()) {
|
||||||
|
@ -1505,7 +1505,7 @@ public class Manager implements Closeable {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} 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()) {
|
if (message.getGroupContext().get().getGroupV2().isPresent()) {
|
||||||
|
@ -1515,9 +1515,18 @@ public class Manager implements Closeable {
|
||||||
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
|
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
|
||||||
|
|
||||||
byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize();
|
byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize();
|
||||||
GroupInfo groupInfo = account.getGroupStore().getGroup(groupId);
|
GroupInfo groupInfo = account.getGroupStore().getGroupByV2Id(groupId);
|
||||||
if (groupInfo instanceof GroupInfoV1) {
|
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) {
|
} else if (groupInfo == null || groupInfo instanceof GroupInfoV2) {
|
||||||
GroupInfoV2 groupInfoV2 = groupInfo == null
|
GroupInfoV2 groupInfoV2 = groupInfo == null
|
||||||
? new GroupInfoV2(groupId, groupMasterKey)
|
? new GroupInfoV2(groupId, groupMasterKey)
|
||||||
|
@ -1526,26 +1535,7 @@ public class Manager implements Closeable {
|
||||||
if (groupInfoV2.getGroup() == null
|
if (groupInfoV2.getGroup() == null
|
||||||
|| groupInfoV2.getGroup().getRevision() < groupContext.getRevision()) {
|
|| groupInfoV2.getGroup().getRevision() < groupContext.getRevision()) {
|
||||||
// TODO check if revision is only 1 behind and a signedGroupChange is available
|
// TODO check if revision is only 1 behind and a signedGroupChange is available
|
||||||
try {
|
groupInfoV2.setGroup(getDecryptedGroup(groupSecretParams));
|
||||||
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 ...");
|
|
||||||
}
|
|
||||||
account.getGroupStore().updateGroup(groupInfoV2);
|
account.getGroupStore().updateGroup(groupInfoV2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1633,6 +1623,26 @@ public class Manager implements Closeable {
|
||||||
return actions;
|
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(
|
private void retryFailedReceivedMessages(
|
||||||
ReceiveMessageHandler handler, boolean ignoreAttachments
|
ReceiveMessageHandler handler, boolean ignoreAttachments
|
||||||
) {
|
) {
|
||||||
|
@ -2314,11 +2324,6 @@ public class Manager implements Closeable {
|
||||||
return account.getGroupStore().getGroup(groupId);
|
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() {
|
public List<JsonIdentityKeyStore.Identity> getIdentities() {
|
||||||
return account.getSignalProtocolStore().getIdentities();
|
return account.getSignalProtocolStore().getIdentities();
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,7 @@ public class ServiceConfig {
|
||||||
} catch (Throwable ignored) {
|
} catch (Throwable ignored) {
|
||||||
zkGroupAvailable = false;
|
zkGroupAvailable = false;
|
||||||
}
|
}
|
||||||
capabilities = new AccountAttributes.Capabilities(false, zkGroupAvailable, false, false);
|
capabilities = new AccountAttributes.Capabilities(false, zkGroupAvailable, false, zkGroupAvailable);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SignalServiceConfiguration createDefaultServiceConfiguration(String userAgent) {
|
public static SignalServiceConfiguration createDefaultServiceConfiguration(String userAgent) {
|
||||||
|
|
|
@ -25,6 +25,9 @@ public class GroupInfoV1 extends GroupInfo {
|
||||||
|
|
||||||
private static final ObjectMapper jsonProcessor = new ObjectMapper();
|
private static final ObjectMapper jsonProcessor = new ObjectMapper();
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
public byte[] expectedV2Id;
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
public String name;
|
public String name;
|
||||||
|
|
||||||
|
@ -54,6 +57,7 @@ public class GroupInfoV1 extends GroupInfo {
|
||||||
|
|
||||||
public GroupInfoV1(
|
public GroupInfoV1(
|
||||||
@JsonProperty("groupId") byte[] groupId,
|
@JsonProperty("groupId") byte[] groupId,
|
||||||
|
@JsonProperty("expectedV2Id") byte[] expectedV2Id,
|
||||||
@JsonProperty("name") String name,
|
@JsonProperty("name") String name,
|
||||||
@JsonProperty("members") Collection<SignalServiceAddress> members,
|
@JsonProperty("members") Collection<SignalServiceAddress> members,
|
||||||
@JsonProperty("avatarId") long _ignored_avatarId,
|
@JsonProperty("avatarId") long _ignored_avatarId,
|
||||||
|
@ -65,6 +69,7 @@ public class GroupInfoV1 extends GroupInfo {
|
||||||
@JsonProperty("active") boolean _ignored_active
|
@JsonProperty("active") boolean _ignored_active
|
||||||
) {
|
) {
|
||||||
super(groupId);
|
super(groupId);
|
||||||
|
this.expectedV2Id = expectedV2Id;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.members.addAll(members);
|
this.members.addAll(members);
|
||||||
this.color = color;
|
this.color = color;
|
||||||
|
|
|
@ -12,6 +12,7 @@ import com.fasterxml.jackson.databind.SerializerProvider;
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.GroupUtils;
|
||||||
import org.asamk.signal.util.Hex;
|
import org.asamk.signal.util.Hex;
|
||||||
import org.asamk.signal.util.IOUtils;
|
import org.asamk.signal.util.IOUtils;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||||
|
@ -24,6 +25,7 @@ import java.io.FileInputStream;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
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) {
|
public GroupInfo getGroup(byte[] groupId) {
|
||||||
final GroupInfo group = groups.get(Base64.encodeBytes(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);
|
loadDecryptedGroup(group);
|
||||||
return group;
|
return group;
|
||||||
}
|
}
|
||||||
|
@ -147,7 +179,11 @@ public class JsonGroupStore {
|
||||||
}
|
}
|
||||||
g.setBlocked(n.get("blocked").asBoolean(false));
|
g.setBlocked(n.get("blocked").asBoolean(false));
|
||||||
} else {
|
} 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);
|
groups.put(Base64.encodeBytes(g.groupId), g);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue