]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/manager/helper/GroupHelper.java
6fd3500365177c5f0859095d9b10c86595f5bfe2
[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.manager.GroupIdV2;
6 import org.asamk.signal.manager.GroupLinkPassword;
7 import org.asamk.signal.manager.GroupUtils;
8 import org.asamk.signal.storage.groups.GroupInfoV2;
9 import org.asamk.signal.util.IOUtils;
10 import org.signal.storageservice.protos.groups.AccessControl;
11 import org.signal.storageservice.protos.groups.GroupChange;
12 import org.signal.storageservice.protos.groups.Member;
13 import org.signal.storageservice.protos.groups.local.DecryptedGroup;
14 import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
15 import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
16 import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
17 import org.signal.zkgroup.InvalidInputException;
18 import org.signal.zkgroup.VerificationFailedException;
19 import org.signal.zkgroup.groups.GroupMasterKey;
20 import org.signal.zkgroup.groups.GroupSecretParams;
21 import org.signal.zkgroup.groups.UuidCiphertext;
22 import org.signal.zkgroup.profiles.ProfileKeyCredential;
23 import org.whispersystems.libsignal.util.Pair;
24 import org.whispersystems.libsignal.util.guava.Optional;
25 import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
26 import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
27 import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
28 import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
29 import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
30 import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
31 import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
32 import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException;
33 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
34 import org.whispersystems.signalservice.api.util.UuidUtil;
35
36 import java.io.FileInputStream;
37 import java.io.IOException;
38 import java.io.InputStream;
39 import java.util.Collection;
40 import java.util.List;
41 import java.util.Set;
42 import java.util.UUID;
43 import java.util.stream.Collectors;
44
45 public class GroupHelper {
46
47 private final ProfileKeyCredentialProvider profileKeyCredentialProvider;
48
49 private final ProfileProvider profileProvider;
50
51 private final SelfAddressProvider selfAddressProvider;
52
53 private final GroupsV2Operations groupsV2Operations;
54
55 private final GroupsV2Api groupsV2Api;
56
57 private final GroupAuthorizationProvider groupAuthorizationProvider;
58
59 public GroupHelper(
60 final ProfileKeyCredentialProvider profileKeyCredentialProvider,
61 final ProfileProvider profileProvider,
62 final SelfAddressProvider selfAddressProvider,
63 final GroupsV2Operations groupsV2Operations,
64 final GroupsV2Api groupsV2Api,
65 final GroupAuthorizationProvider groupAuthorizationProvider
66 ) {
67 this.profileKeyCredentialProvider = profileKeyCredentialProvider;
68 this.profileProvider = profileProvider;
69 this.selfAddressProvider = selfAddressProvider;
70 this.groupsV2Operations = groupsV2Operations;
71 this.groupsV2Api = groupsV2Api;
72 this.groupAuthorizationProvider = groupAuthorizationProvider;
73 }
74
75 public DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) {
76 try {
77 final GroupsV2AuthorizationString groupsV2AuthorizationString = groupAuthorizationProvider.getAuthorizationForToday(
78 groupSecretParams);
79 return groupsV2Api.getGroup(groupSecretParams, groupsV2AuthorizationString);
80 } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
81 System.err.println("Failed to retrieve Group V2 info, ignoring ...");
82 return null;
83 }
84 }
85
86 public DecryptedGroupJoinInfo getDecryptedGroupJoinInfo(
87 GroupMasterKey groupMasterKey, GroupLinkPassword password
88 ) throws IOException, GroupLinkNotActiveException {
89 GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
90
91 return groupsV2Api.getGroupJoinInfo(groupSecretParams,
92 Optional.fromNullable(password).transform(GroupLinkPassword::serialize),
93 groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams));
94 }
95
96 public GroupInfoV2 createGroupV2(
97 String name, Collection<SignalServiceAddress> members, String avatarFile
98 ) throws IOException {
99 final byte[] avatarBytes = readAvatarBytes(avatarFile);
100 final GroupsV2Operations.NewGroup newGroup = buildNewGroupV2(name, members, avatarBytes);
101 if (newGroup == null) {
102 return null;
103 }
104
105 final GroupSecretParams groupSecretParams = newGroup.getGroupSecretParams();
106
107 final GroupsV2AuthorizationString groupAuthForToday;
108 final DecryptedGroup decryptedGroup;
109 try {
110 groupAuthForToday = groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams);
111 groupsV2Api.putNewGroup(newGroup, groupAuthForToday);
112 decryptedGroup = groupsV2Api.getGroup(groupSecretParams, groupAuthForToday);
113 } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
114 System.err.println("Failed to create V2 group: " + e.getMessage());
115 return null;
116 }
117 if (decryptedGroup == null) {
118 System.err.println("Failed to create V2 group!");
119 return null;
120 }
121
122 final GroupIdV2 groupId = GroupUtils.getGroupIdV2(groupSecretParams);
123 final GroupMasterKey masterKey = groupSecretParams.getMasterKey();
124 GroupInfoV2 g = new GroupInfoV2(groupId, masterKey);
125 g.setGroup(decryptedGroup);
126
127 return g;
128 }
129
130 private byte[] readAvatarBytes(final String avatarFile) throws IOException {
131 final byte[] avatarBytes;
132 try (InputStream avatar = avatarFile == null ? null : new FileInputStream(avatarFile)) {
133 avatarBytes = avatar == null ? null : IOUtils.readFully(avatar);
134 }
135 return avatarBytes;
136 }
137
138 private GroupsV2Operations.NewGroup buildNewGroupV2(
139 String name, Collection<SignalServiceAddress> members, byte[] avatar
140 ) {
141 final ProfileKeyCredential profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(
142 selfAddressProvider.getSelfAddress());
143 if (profileKeyCredential == null) {
144 System.err.println("Cannot create a V2 group as self does not have a versioned profile");
145 return null;
146 }
147
148 if (!areMembersValid(members)) return null;
149
150 GroupCandidate self = new GroupCandidate(selfAddressProvider.getSelfAddress().getUuid().orNull(),
151 Optional.fromNullable(profileKeyCredential));
152 Set<GroupCandidate> candidates = members.stream()
153 .map(member -> new GroupCandidate(member.getUuid().get(),
154 Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member))))
155 .collect(Collectors.toSet());
156
157 final GroupSecretParams groupSecretParams = GroupSecretParams.generate();
158 return groupsV2Operations.createNewGroup(groupSecretParams,
159 name,
160 Optional.fromNullable(avatar),
161 self,
162 candidates,
163 Member.Role.DEFAULT,
164 0);
165 }
166
167 private boolean areMembersValid(final Collection<SignalServiceAddress> members) {
168 final int noUuidCapability = members.stream()
169 .filter(address -> !address.getUuid().isPresent())
170 .collect(Collectors.toUnmodifiableSet())
171 .size();
172 if (noUuidCapability > 0) {
173 System.err.println("Cannot create a V2 group as " + noUuidCapability + " members don't have a UUID.");
174 return false;
175 }
176
177 final int noGv2Capability = members.stream()
178 .map(profileProvider::getProfile)
179 .filter(profile -> profile != null && !profile.getCapabilities().gv2)
180 .collect(Collectors.toUnmodifiableSet())
181 .size();
182 if (noGv2Capability > 0) {
183 System.err.println("Cannot create a V2 group as " + noGv2Capability + " members don't support Groups V2.");
184 return false;
185 }
186
187 return true;
188 }
189
190 public Pair<DecryptedGroup, GroupChange> updateGroupV2(
191 GroupInfoV2 groupInfoV2, String name, String avatarFile
192 ) throws IOException {
193 final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
194 GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
195
196 GroupChange.Actions.Builder change = name != null
197 ? groupOperations.createModifyGroupTitle(name)
198 : GroupChange.Actions.newBuilder();
199
200 if (avatarFile != null) {
201 final byte[] avatarBytes = readAvatarBytes(avatarFile);
202 String avatarCdnKey = groupsV2Api.uploadAvatar(avatarBytes,
203 groupSecretParams,
204 groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams));
205 change.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder().setAvatar(avatarCdnKey));
206 }
207
208 final Optional<UUID> uuid = this.selfAddressProvider.getSelfAddress().getUuid();
209 if (uuid.isPresent()) {
210 change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
211 }
212
213 return commitChange(groupInfoV2, change);
214 }
215
216 public Pair<DecryptedGroup, GroupChange> updateGroupV2(
217 GroupInfoV2 groupInfoV2, Set<SignalServiceAddress> newMembers
218 ) throws IOException {
219 final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
220 GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
221
222 if (!areMembersValid(newMembers)) return null;
223
224 Set<GroupCandidate> candidates = newMembers.stream()
225 .map(member -> new GroupCandidate(member.getUuid().get(),
226 Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member))))
227 .collect(Collectors.toSet());
228
229 final GroupChange.Actions.Builder change = groupOperations.createModifyGroupMembershipChange(candidates,
230 selfAddressProvider.getSelfAddress().getUuid().get());
231
232 final Optional<UUID> uuid = this.selfAddressProvider.getSelfAddress().getUuid();
233 if (uuid.isPresent()) {
234 change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
235 }
236
237 return commitChange(groupInfoV2, change);
238 }
239
240 public Pair<DecryptedGroup, GroupChange> leaveGroup(GroupInfoV2 groupInfoV2) throws IOException {
241 List<DecryptedPendingMember> pendingMembersList = groupInfoV2.getGroup().getPendingMembersList();
242 final UUID selfUuid = selfAddressProvider.getSelfAddress().getUuid().get();
243 Optional<DecryptedPendingMember> selfPendingMember = DecryptedGroupUtil.findPendingByUuid(pendingMembersList,
244 selfUuid);
245
246 if (selfPendingMember.isPresent()) {
247 return revokeInvites(groupInfoV2, Set.of(selfPendingMember.get()));
248 } else {
249 return ejectMembers(groupInfoV2, Set.of(selfUuid));
250 }
251 }
252
253 public GroupChange joinGroup(
254 GroupMasterKey groupMasterKey,
255 GroupLinkPassword groupLinkPassword,
256 DecryptedGroupJoinInfo decryptedGroupJoinInfo
257 ) throws IOException {
258 final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
259 final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
260
261 final SignalServiceAddress selfAddress = this.selfAddressProvider.getSelfAddress();
262 final ProfileKeyCredential profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(
263 selfAddress);
264 if (profileKeyCredential == null) {
265 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
266 }
267
268 boolean requestToJoin = decryptedGroupJoinInfo.getAddFromInviteLink()
269 == AccessControl.AccessRequired.ADMINISTRATOR;
270 GroupChange.Actions.Builder change = requestToJoin
271 ? groupOperations.createGroupJoinRequest(profileKeyCredential)
272 : groupOperations.createGroupJoinDirect(profileKeyCredential);
273
274 change.setSourceUuid(UuidUtil.toByteString(selfAddress.getUuid().get()));
275
276 return commitChange(groupSecretParams, decryptedGroupJoinInfo.getRevision(), change, groupLinkPassword);
277 }
278
279 public Pair<DecryptedGroup, GroupChange> acceptInvite(GroupInfoV2 groupInfoV2) throws IOException {
280 final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
281 final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
282
283 final SignalServiceAddress selfAddress = this.selfAddressProvider.getSelfAddress();
284 final ProfileKeyCredential profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(
285 selfAddress);
286 if (profileKeyCredential == null) {
287 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
288 }
289
290 final GroupChange.Actions.Builder change = groupOperations.createAcceptInviteChange(profileKeyCredential);
291
292 final Optional<UUID> uuid = selfAddress.getUuid();
293 if (uuid.isPresent()) {
294 change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
295 }
296
297 return commitChange(groupInfoV2, change);
298 }
299
300 public Pair<DecryptedGroup, GroupChange> revokeInvites(
301 GroupInfoV2 groupInfoV2, Set<DecryptedPendingMember> pendingMembers
302 ) throws IOException {
303 final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
304 final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
305 final Set<UuidCiphertext> uuidCipherTexts = pendingMembers.stream().map(member -> {
306 try {
307 return new UuidCiphertext(member.getUuidCipherText().toByteArray());
308 } catch (InvalidInputException e) {
309 throw new AssertionError(e);
310 }
311 }).collect(Collectors.toSet());
312 return commitChange(groupInfoV2, groupOperations.createRemoveInvitationChange(uuidCipherTexts));
313 }
314
315 public Pair<DecryptedGroup, GroupChange> ejectMembers(GroupInfoV2 groupInfoV2, Set<UUID> uuids) throws IOException {
316 final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
317 final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
318 return commitChange(groupInfoV2, groupOperations.createRemoveMembersChange(uuids));
319 }
320
321 private Pair<DecryptedGroup, GroupChange> commitChange(
322 GroupInfoV2 groupInfoV2, GroupChange.Actions.Builder change
323 ) throws IOException {
324 final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
325 final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
326 final DecryptedGroup previousGroupState = groupInfoV2.getGroup();
327 final int nextRevision = previousGroupState.getRevision() + 1;
328 final GroupChange.Actions changeActions = change.setRevision(nextRevision).build();
329 final DecryptedGroupChange decryptedChange;
330 final DecryptedGroup decryptedGroupState;
331
332 try {
333 decryptedChange = groupOperations.decryptChange(changeActions,
334 selfAddressProvider.getSelfAddress().getUuid().get());
335 decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange);
336 } catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
337 throw new IOException(e);
338 }
339
340 GroupChange signedGroupChange = groupsV2Api.patchGroup(changeActions,
341 groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams),
342 Optional.absent());
343
344 return new Pair<>(decryptedGroupState, signedGroupChange);
345 }
346
347 private GroupChange commitChange(
348 GroupSecretParams groupSecretParams,
349 int currentRevision,
350 GroupChange.Actions.Builder change,
351 GroupLinkPassword password
352 ) throws IOException {
353 final int nextRevision = currentRevision + 1;
354 final GroupChange.Actions changeActions = change.setRevision(nextRevision).build();
355
356 return groupsV2Api.patchGroup(changeActions,
357 groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams),
358 Optional.fromNullable(password).transform(GroupLinkPassword::serialize));
359 }
360
361 public DecryptedGroup getUpdatedDecryptedGroup(
362 DecryptedGroup group, byte[] signedGroupChange, GroupMasterKey groupMasterKey
363 ) {
364 try {
365 final DecryptedGroupChange decryptedGroupChange = getDecryptedGroupChange(signedGroupChange,
366 groupMasterKey);
367 if (decryptedGroupChange == null) {
368 return null;
369 }
370 return DecryptedGroupUtil.apply(group, decryptedGroupChange);
371 } catch (NotAbleToApplyGroupV2ChangeException e) {
372 return null;
373 }
374 }
375
376 private DecryptedGroupChange getDecryptedGroupChange(byte[] signedGroupChange, GroupMasterKey groupMasterKey) {
377 if (signedGroupChange != null) {
378 GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(GroupSecretParams.deriveFromMasterKey(
379 groupMasterKey));
380
381 try {
382 return groupOperations.decryptChange(GroupChange.parseFrom(signedGroupChange), true).orNull();
383 } catch (VerificationFailedException | InvalidGroupStateException | InvalidProtocolBufferException e) {
384 return null;
385 }
386 }
387
388 return null;
389 }
390 }