Implement updating of v2 groups

This commit is contained in:
AsamK 2020-12-13 12:01:18 +01:00
parent 98dee97cc6
commit 1fd62ee342
3 changed files with 193 additions and 50 deletions

View file

@ -43,6 +43,7 @@ import org.signal.libsignal.metadata.ProtocolLegacyMessageException;
import org.signal.libsignal.metadata.ProtocolNoSessionException; import org.signal.libsignal.metadata.ProtocolNoSessionException;
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
import org.signal.libsignal.metadata.SelfSendException; 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.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedMember; import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.InvalidInputException;
@ -213,7 +214,9 @@ public class Manager implements Closeable {
this.groupHelper = new GroupHelper(this::getRecipientProfileKeyCredential, this.groupHelper = new GroupHelper(this::getRecipientProfileKeyCredential,
this::getRecipientProfile, this::getRecipientProfile,
account::getSelfAddress, account::getSelfAddress,
groupsV2Operations); groupsV2Operations,
groupsV2Api,
this::getGroupAuthForToday);
} }
public String getUsername() { public String getUsername() {
@ -752,36 +755,6 @@ public class Manager implements Closeable {
return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress())); 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( private Pair<byte[], List<SendMessageResult>> sendUpdateGroupMessage(
byte[] groupId, String name, Collection<SignalServiceAddress> members, String avatarFile byte[] groupId, String name, Collection<SignalServiceAddress> members, String avatarFile
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
@ -789,8 +762,7 @@ public class Manager implements Closeable {
SignalServiceDataMessage.Builder messageBuilder; SignalServiceDataMessage.Builder messageBuilder;
if (groupId == null) { if (groupId == null) {
// Create new group // Create new group
InputStream avatar = avatarFile == null ? null : new FileInputStream(avatarFile); GroupInfoV2 gv2 = groupHelper.createGroupV2(name, members, avatarFile);
GroupInfoV2 gv2 = createGroupV2(name, members, avatar);
if (gv2 == null) { if (gv2 == null) {
GroupInfoV1 gv1 = new GroupInfoV1(KeyUtils.createGroupId()); GroupInfoV1 gv1 = new GroupInfoV1(KeyUtils.createGroupId());
gv1.addMembers(Collections.singleton(account.getSelfAddress())); gv1.addMembers(Collections.singleton(account.getSelfAddress()));
@ -798,19 +770,42 @@ public class Manager implements Closeable {
messageBuilder = getGroupUpdateMessageBuilder(gv1); messageBuilder = getGroupUpdateMessageBuilder(gv1);
g = gv1; g = gv1;
} else { } else {
messageBuilder = getGroupUpdateMessageBuilder(gv2); messageBuilder = getGroupUpdateMessageBuilder(gv2, null);
g = gv2; g = gv2;
} }
} else { } else {
GroupInfo group = getGroupForSending(groupId); GroupInfo group = getGroupForSending(groupId);
if (!(group instanceof GroupInfoV1)) { if (group instanceof GroupInfoV2) {
throw new RuntimeException("TODO Not implemented!"); 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; GroupInfoV1 gv1 = (GroupInfoV1) group;
updateGroupV1(gv1, name, members, avatarFile); updateGroupV1(gv1, name, members, avatarFile);
messageBuilder = getGroupUpdateMessageBuilder(gv1); messageBuilder = getGroupUpdateMessageBuilder(gv1);
g = gv1; g = gv1;
} }
}
account.getGroupStore().updateGroup(g); account.getGroupStore().updateGroup(g);
@ -899,11 +894,10 @@ public class Manager implements Closeable {
.withExpiration(g.getMessageExpirationTime()); .withExpiration(g.getMessageExpirationTime());
} }
private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g) { private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) {
SignalServiceGroupV2.Builder group = SignalServiceGroupV2.newBuilder(g.getMasterKey()) SignalServiceGroupV2.Builder group = SignalServiceGroupV2.newBuilder(g.getMasterKey())
.withRevision(g.getGroup().getRevision()) .withRevision(g.getGroup().getRevision())
// .withSignedGroupChange() // TODO .withSignedGroupChange(signedGroupChange);
;
return SignalServiceDataMessage.newBuilder() return SignalServiceDataMessage.newBuilder()
.asGroupMessage(group.build()) .asGroupMessage(group.build())
.withExpiration(g.getMessageExpirationTime()); .withExpiration(g.getMessageExpirationTime());
@ -1427,16 +1421,20 @@ public class Manager implements Closeable {
private GroupsV2AuthorizationString getGroupAuthForToday( private GroupsV2AuthorizationString getGroupAuthForToday(
final GroupSecretParams groupSecretParams final GroupSecretParams groupSecretParams
) throws IOException, VerificationFailedException { ) throws IOException {
final int today = currentTimeDays(); final int today = currentTimeDays();
// Returns credentials for the next 7 days // Returns credentials for the next 7 days
final HashMap<Integer, AuthCredentialResponse> credentials = groupsV2Api.getCredentials(today); final HashMap<Integer, AuthCredentialResponse> credentials = groupsV2Api.getCredentials(today);
// TODO cache credentials until they expire // TODO cache credentials until they expire
AuthCredentialResponse authCredentialResponse = credentials.get(today); AuthCredentialResponse authCredentialResponse = credentials.get(today);
try {
return groupsV2Api.getGroupsV2AuthorizationString(account.getUuid(), return groupsV2Api.getGroupsV2AuthorizationString(account.getUuid(),
today, today,
groupSecretParams, groupSecretParams,
authCredentialResponse); authCredentialResponse);
} catch (VerificationFailedException e) {
throw new IOException(e);
}
} }
private List<HandleAction> handleSignalServiceDataMessage( private List<HandleAction> handleSignalServiceDataMessage(

View file

@ -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;
}

View file

@ -2,6 +2,8 @@ package org.asamk.signal.manager.helper;
import com.google.protobuf.InvalidProtocolBufferException; 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.GroupChange;
import org.signal.storageservice.protos.groups.Member; import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.local.DecryptedGroup; 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.GroupMasterKey;
import org.signal.zkgroup.groups.GroupSecretParams; import org.signal.zkgroup.groups.GroupSecretParams;
import org.signal.zkgroup.profiles.ProfileKeyCredential; import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import org.whispersystems.signalservice.api.groupsv2.GroupCandidate; 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.GroupsV2Operations;
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException; import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException; import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; 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.Collection;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class GroupHelper { public class GroupHelper {
@ -32,19 +42,69 @@ public class GroupHelper {
private final GroupsV2Operations groupsV2Operations; private final GroupsV2Operations groupsV2Operations;
private final GroupsV2Api groupsV2Api;
private final GroupAuthorizationProvider groupAuthorizationProvider;
public GroupHelper( public GroupHelper(
final ProfileKeyCredentialProvider profileKeyCredentialProvider, final ProfileKeyCredentialProvider profileKeyCredentialProvider,
final ProfileProvider profileProvider, final ProfileProvider profileProvider,
final SelfAddressProvider selfAddressProvider, final SelfAddressProvider selfAddressProvider,
final GroupsV2Operations groupsV2Operations final GroupsV2Operations groupsV2Operations,
final GroupsV2Api groupsV2Api,
final GroupAuthorizationProvider groupAuthorizationProvider
) { ) {
this.profileKeyCredentialProvider = profileKeyCredentialProvider; this.profileKeyCredentialProvider = profileKeyCredentialProvider;
this.profileProvider = profileProvider; this.profileProvider = profileProvider;
this.selfAddressProvider = selfAddressProvider; this.selfAddressProvider = selfAddressProvider;
this.groupsV2Operations = groupsV2Operations; this.groupsV2Operations = groupsV2Operations;
this.groupsV2Api = groupsV2Api;
this.groupAuthorizationProvider = groupAuthorizationProvider;
} }
public GroupsV2Operations.NewGroup createGroupV2( 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;
}
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 String name, Collection<SignalServiceAddress> members, byte[] avatar
) { ) {
final ProfileKeyCredential profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential( final ProfileKeyCredential profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(
@ -90,6 +150,80 @@ public class GroupHelper {
0); 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( public DecryptedGroup getUpdatedDecryptedGroup(
DecryptedGroup group, byte[] signedGroupChange, GroupMasterKey groupMasterKey DecryptedGroup group, byte[] signedGroupChange, GroupMasterKey groupMasterKey
) { ) {