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