]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/manager/helper/GroupHelper.java
Implement updating of v2 groups
[signal-cli] / src / main / java / org / asamk / signal / manager / helper / GroupHelper.java
1 package org.asamk.signal.manager.helper;
2
3 import com.google.protobuf.InvalidProtocolBufferException;
4
5 import org.asamk.signal.storage.groups.GroupInfoV2;
6 import org.asamk.signal.util.IOUtils;
7 import org.signal.storageservice.protos.groups.GroupChange;
8 import org.signal.storageservice.protos.groups.Member;
9 import org.signal.storageservice.protos.groups.local.DecryptedGroup;
10 import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
11 import org.signal.zkgroup.VerificationFailedException;
12 import org.signal.zkgroup.groups.GroupMasterKey;
13 import org.signal.zkgroup.groups.GroupSecretParams;
14 import org.signal.zkgroup.profiles.ProfileKeyCredential;
15 import org.whispersystems.libsignal.util.Pair;
16 import org.whispersystems.libsignal.util.guava.Optional;
17 import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
18 import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
19 import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
20 import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
21 import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
22 import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
23 import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException;
24 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
25 import org.whispersystems.signalservice.api.util.UuidUtil;
26
27 import java.io.FileInputStream;
28 import java.io.IOException;
29 import java.io.InputStream;
30 import java.util.Collection;
31 import java.util.Set;
32 import java.util.UUID;
33 import java.util.stream.Collectors;
34
35 public class GroupHelper {
36
37 private final ProfileKeyCredentialProvider profileKeyCredentialProvider;
38
39 private final ProfileProvider profileProvider;
40
41 private final SelfAddressProvider selfAddressProvider;
42
43 private final GroupsV2Operations groupsV2Operations;
44
45 private final GroupsV2Api groupsV2Api;
46
47 private final GroupAuthorizationProvider groupAuthorizationProvider;
48
49 public GroupHelper(
50 final ProfileKeyCredentialProvider profileKeyCredentialProvider,
51 final ProfileProvider profileProvider,
52 final SelfAddressProvider selfAddressProvider,
53 final GroupsV2Operations groupsV2Operations,
54 final GroupsV2Api groupsV2Api,
55 final GroupAuthorizationProvider groupAuthorizationProvider
56 ) {
57 this.profileKeyCredentialProvider = profileKeyCredentialProvider;
58 this.profileProvider = profileProvider;
59 this.selfAddressProvider = selfAddressProvider;
60 this.groupsV2Operations = groupsV2Operations;
61 this.groupsV2Api = groupsV2Api;
62 this.groupAuthorizationProvider = groupAuthorizationProvider;
63 }
64
65 public GroupInfoV2 createGroupV2(
66 String name, Collection<SignalServiceAddress> members, String avatarFile
67 ) throws IOException {
68 final byte[] avatarBytes = readAvatarBytes(avatarFile);
69 final GroupsV2Operations.NewGroup newGroup = buildNewGroupV2(name, members, avatarBytes);
70 if (newGroup == null) {
71 return null;
72 }
73
74 final GroupSecretParams groupSecretParams = newGroup.getGroupSecretParams();
75
76 final GroupsV2AuthorizationString groupAuthForToday;
77 final DecryptedGroup decryptedGroup;
78 try {
79 groupAuthForToday = groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams);
80 groupsV2Api.putNewGroup(newGroup, groupAuthForToday);
81 decryptedGroup = groupsV2Api.getGroup(groupSecretParams, groupAuthForToday);
82 } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
83 System.err.println("Failed to create V2 group: " + e.getMessage());
84 return null;
85 }
86 if (decryptedGroup == null) {
87 System.err.println("Failed to create V2 group!");
88 return null;
89 }
90
91 final byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize();
92 final GroupMasterKey masterKey = groupSecretParams.getMasterKey();
93 GroupInfoV2 g = new GroupInfoV2(groupId, masterKey);
94 g.setGroup(decryptedGroup);
95
96 return g;
97 }
98
99 private byte[] readAvatarBytes(final String avatarFile) throws IOException {
100 final byte[] avatarBytes;
101 try (InputStream avatar = avatarFile == null ? null : new FileInputStream(avatarFile)) {
102 avatarBytes = avatar == null ? null : IOUtils.readFully(avatar);
103 }
104 return avatarBytes;
105 }
106
107 private GroupsV2Operations.NewGroup buildNewGroupV2(
108 String name, Collection<SignalServiceAddress> members, byte[] avatar
109 ) {
110 final ProfileKeyCredential profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(
111 selfAddressProvider.getSelfAddress());
112 if (profileKeyCredential == null) {
113 System.err.println("Cannot create a V2 group as self does not have a versioned profile");
114 return null;
115 }
116
117 final int noUuidCapability = members.stream()
118 .filter(address -> !address.getUuid().isPresent())
119 .collect(Collectors.toUnmodifiableSet())
120 .size();
121 if (noUuidCapability > 0) {
122 System.err.println("Cannot create a V2 group as " + noUuidCapability + " members don't have a UUID.");
123 return null;
124 }
125
126 final int noGv2Capability = members.stream()
127 .map(profileProvider::getProfile)
128 .filter(profile -> !profile.getCapabilities().gv2)
129 .collect(Collectors.toUnmodifiableSet())
130 .size();
131 if (noGv2Capability > 0) {
132 System.err.println("Cannot create a V2 group as " + noGv2Capability + " members don't support Groups V2.");
133 return null;
134 }
135
136 GroupCandidate self = new GroupCandidate(selfAddressProvider.getSelfAddress().getUuid().orNull(),
137 Optional.fromNullable(profileKeyCredential));
138 Set<GroupCandidate> candidates = members.stream()
139 .map(member -> new GroupCandidate(member.getUuid().get(),
140 Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member))))
141 .collect(Collectors.toSet());
142
143 final GroupSecretParams groupSecretParams = GroupSecretParams.generate();
144 return groupsV2Operations.createNewGroup(groupSecretParams,
145 name,
146 Optional.fromNullable(avatar),
147 self,
148 candidates,
149 Member.Role.DEFAULT,
150 0);
151 }
152
153 public Pair<DecryptedGroup, GroupChange> updateGroupV2(
154 GroupInfoV2 groupInfoV2, String name, String avatarFile
155 ) throws IOException {
156 final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
157 GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
158
159 GroupChange.Actions.Builder change = name != null
160 ? groupOperations.createModifyGroupTitle(name)
161 : GroupChange.Actions.newBuilder();
162
163 if (avatarFile != null) {
164 final byte[] avatarBytes = readAvatarBytes(avatarFile);
165 String avatarCdnKey = groupsV2Api.uploadAvatar(avatarBytes,
166 groupSecretParams,
167 groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams));
168 change.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder().setAvatar(avatarCdnKey));
169 }
170
171 final Optional<UUID> uuid = this.selfAddressProvider.getSelfAddress().getUuid();
172 if (uuid.isPresent()) {
173 change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
174 }
175
176 return commitChange(groupInfoV2, change);
177 }
178
179 public Pair<DecryptedGroup, GroupChange> updateGroupV2(
180 GroupInfoV2 groupInfoV2, Set<SignalServiceAddress> newMembers
181 ) throws IOException {
182 final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
183 GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
184
185 Set<GroupCandidate> candidates = newMembers.stream()
186 .map(member -> new GroupCandidate(member.getUuid().get(),
187 Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member))))
188 .collect(Collectors.toSet());
189
190 final GroupChange.Actions.Builder change = groupOperations.createModifyGroupMembershipChange(candidates,
191 selfAddressProvider.getSelfAddress().getUuid().get());
192
193 final Optional<UUID> uuid = this.selfAddressProvider.getSelfAddress().getUuid();
194 if (uuid.isPresent()) {
195 change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
196 }
197
198 return commitChange(groupInfoV2, change);
199 }
200
201 private Pair<DecryptedGroup, GroupChange> commitChange(
202 GroupInfoV2 groupInfoV2, GroupChange.Actions.Builder change
203 ) throws IOException {
204 final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
205 final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
206 final DecryptedGroup previousGroupState = groupInfoV2.getGroup();
207 final int nextRevision = previousGroupState.getRevision() + 1;
208 final GroupChange.Actions changeActions = change.setRevision(nextRevision).build();
209 final DecryptedGroupChange decryptedChange;
210 final DecryptedGroup decryptedGroupState;
211
212 try {
213 decryptedChange = groupOperations.decryptChange(changeActions,
214 selfAddressProvider.getSelfAddress().getUuid().get());
215 decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange);
216 } catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
217 throw new IOException(e);
218 }
219
220 GroupChange signedGroupChange = groupsV2Api.patchGroup(change.build(),
221 groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams),
222 Optional.absent());
223
224 return new Pair<>(decryptedGroupState, signedGroupChange);
225 }
226
227 public DecryptedGroup getUpdatedDecryptedGroup(
228 DecryptedGroup group, byte[] signedGroupChange, GroupMasterKey groupMasterKey
229 ) {
230 try {
231 final DecryptedGroupChange decryptedGroupChange = getDecryptedGroupChange(signedGroupChange,
232 groupMasterKey);
233 if (decryptedGroupChange == null) {
234 return null;
235 }
236 return DecryptedGroupUtil.apply(group, decryptedGroupChange);
237 } catch (NotAbleToApplyGroupV2ChangeException e) {
238 return null;
239 }
240 }
241
242 private DecryptedGroupChange getDecryptedGroupChange(byte[] signedGroupChange, GroupMasterKey groupMasterKey) {
243 if (signedGroupChange != null) {
244 GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(GroupSecretParams.deriveFromMasterKey(
245 groupMasterKey));
246
247 try {
248 return groupOperations.decryptChange(GroupChange.parseFrom(signedGroupChange), true).orNull();
249 } catch (VerificationFailedException | InvalidGroupStateException | InvalidProtocolBufferException e) {
250 return null;
251 }
252 }
253
254 return null;
255 }
256 }