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;
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;
}
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;
}
} 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) {
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;
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);
+ }
+ }
}
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;
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()) {
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()) {
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)
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);
}
}
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
) {
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();
}
} 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) {
private static final ObjectMapper jsonProcessor = new ObjectMapper();
+ @JsonProperty
+ public byte[] expectedV2Id;
+
@JsonProperty
public String name;
public GroupInfoV1(
@JsonProperty("groupId") byte[] groupId,
+ @JsonProperty("expectedV2Id") byte[] expectedV2Id,
@JsonProperty("name") String name,
@JsonProperty("members") Collection<SignalServiceAddress> members,
@JsonProperty("avatarId") long _ignored_avatarId,
@JsonProperty("active") boolean _ignored_active
) {
super(groupId);
+ this.expectedV2Id = expectedV2Id;
this.name = name;
this.members.addAll(members);
this.color = color;
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;
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;
}
}
+ 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;
}
}
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);
}