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.ProtocolUntrustedIdentityException;
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.DecryptedMember;
import org.signal.zkgroup.InvalidInputException;
@ -213,7 +214,9 @@ public class Manager implements Closeable {
this.groupHelper = new GroupHelper(this::getRecipientProfileKeyCredential,
this::getRecipientProfile,
account::getSelfAddress,
groupsV2Operations);
groupsV2Operations,
groupsV2Api,
this::getGroupAuthForToday);
}
public String getUsername() {
@ -752,36 +755,6 @@ public class Manager implements Closeable {
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(
byte[] groupId, String name, Collection<SignalServiceAddress> members, String avatarFile
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
@ -789,8 +762,7 @@ public class Manager implements Closeable {
SignalServiceDataMessage.Builder messageBuilder;
if (groupId == null) {
// Create new group
InputStream avatar = avatarFile == null ? null : new FileInputStream(avatarFile);
GroupInfoV2 gv2 = createGroupV2(name, members, avatar);
GroupInfoV2 gv2 = groupHelper.createGroupV2(name, members, avatarFile);
if (gv2 == null) {
GroupInfoV1 gv1 = new GroupInfoV1(KeyUtils.createGroupId());
gv1.addMembers(Collections.singleton(account.getSelfAddress()));
@ -798,19 +770,42 @@ public class Manager implements Closeable {
messageBuilder = getGroupUpdateMessageBuilder(gv1);
g = gv1;
} else {
messageBuilder = getGroupUpdateMessageBuilder(gv2);
messageBuilder = getGroupUpdateMessageBuilder(gv2, null);
g = gv2;
}
} else {
GroupInfo group = getGroupForSending(groupId);
if (!(group instanceof GroupInfoV1)) {
throw new RuntimeException("TODO Not implemented!");
if (group instanceof GroupInfoV2) {
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;
updateGroupV1(gv1, name, members, avatarFile);
messageBuilder = getGroupUpdateMessageBuilder(gv1);
g = gv1;
}
}
account.getGroupStore().updateGroup(g);
@ -899,11 +894,10 @@ public class Manager implements Closeable {
.withExpiration(g.getMessageExpirationTime());
}
private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g) {
private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) {
SignalServiceGroupV2.Builder group = SignalServiceGroupV2.newBuilder(g.getMasterKey())
.withRevision(g.getGroup().getRevision())
// .withSignedGroupChange() // TODO
;
.withSignedGroupChange(signedGroupChange);
return SignalServiceDataMessage.newBuilder()
.asGroupMessage(group.build())
.withExpiration(g.getMessageExpirationTime());
@ -1427,16 +1421,20 @@ public class Manager implements Closeable {
private GroupsV2AuthorizationString getGroupAuthForToday(
final GroupSecretParams groupSecretParams
) throws IOException, VerificationFailedException {
) throws IOException {
final int today = currentTimeDays();
// Returns credentials for the next 7 days
final HashMap<Integer, AuthCredentialResponse> credentials = groupsV2Api.getCredentials(today);
// TODO cache credentials until they expire
AuthCredentialResponse authCredentialResponse = credentials.get(today);
try {
return groupsV2Api.getGroupsV2AuthorizationString(account.getUuid(),
today,
groupSecretParams,
authCredentialResponse);
} catch (VerificationFailedException e) {
throw new IOException(e);
}
}
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 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.Member;
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.GroupSecretParams;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
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.InvalidGroupStateException;
import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException;
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.Set;
import java.util.UUID;
import java.util.stream.Collectors;
public class GroupHelper {
@ -32,19 +42,69 @@ public class GroupHelper {
private final GroupsV2Operations groupsV2Operations;
private final GroupsV2Api groupsV2Api;
private final GroupAuthorizationProvider groupAuthorizationProvider;
public GroupHelper(
final ProfileKeyCredentialProvider profileKeyCredentialProvider,
final ProfileProvider profileProvider,
final SelfAddressProvider selfAddressProvider,
final GroupsV2Operations groupsV2Operations
final GroupsV2Operations groupsV2Operations,
final GroupsV2Api groupsV2Api,
final GroupAuthorizationProvider groupAuthorizationProvider
) {
this.profileKeyCredentialProvider = profileKeyCredentialProvider;
this.profileProvider = profileProvider;
this.selfAddressProvider = selfAddressProvider;
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
) {
final ProfileKeyCredential profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(
@ -90,6 +150,80 @@ public class GroupHelper {
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(
DecryptedGroup group, byte[] signedGroupChange, GroupMasterKey groupMasterKey
) {