mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 10:30:38 +00:00
Implement updating of v2 groups
This commit is contained in:
parent
98dee97cc6
commit
1fd62ee342
3 changed files with 193 additions and 50 deletions
|
@ -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,18 +770,41 @@ 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;
|
||||
}
|
||||
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);
|
||||
return groupsV2Api.getGroupsV2AuthorizationString(account.getUuid(),
|
||||
today,
|
||||
groupSecretParams,
|
||||
authCredentialResponse);
|
||||
try {
|
||||
return groupsV2Api.getGroupsV2AuthorizationString(account.getUuid(),
|
||||
today,
|
||||
groupSecretParams,
|
||||
authCredentialResponse);
|
||||
} catch (VerificationFailedException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private List<HandleAction> handleSignalServiceDataMessage(
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue