]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java
Update libsignal-service
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / helper / GroupV2Helper.java
1 package org.asamk.signal.manager.helper;
2
3 import org.asamk.signal.manager.api.GroupLinkState;
4 import org.asamk.signal.manager.api.GroupPermission;
5 import org.asamk.signal.manager.api.NotAGroupMemberException;
6 import org.asamk.signal.manager.api.Pair;
7 import org.asamk.signal.manager.groups.GroupLinkPassword;
8 import org.asamk.signal.manager.groups.GroupUtils;
9 import org.asamk.signal.manager.internal.SignalDependencies;
10 import org.asamk.signal.manager.storage.groups.GroupInfoV2;
11 import org.asamk.signal.manager.storage.recipients.RecipientId;
12 import org.asamk.signal.manager.util.Utils;
13 import org.signal.libsignal.zkgroup.InvalidInputException;
14 import org.signal.libsignal.zkgroup.VerificationFailedException;
15 import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPniResponse;
16 import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
17 import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
18 import org.signal.libsignal.zkgroup.groups.UuidCiphertext;
19 import org.signal.libsignal.zkgroup.profiles.ProfileKey;
20 import org.signal.storageservice.protos.groups.AccessControl;
21 import org.signal.storageservice.protos.groups.GroupChange;
22 import org.signal.storageservice.protos.groups.Member;
23 import org.signal.storageservice.protos.groups.local.DecryptedGroup;
24 import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
25 import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
26 import org.signal.storageservice.protos.groups.local.DecryptedMember;
27 import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
28 import org.slf4j.Logger;
29 import org.slf4j.LoggerFactory;
30 import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
31 import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
32 import org.whispersystems.signalservice.api.groupsv2.GroupHistoryPage;
33 import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
34 import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
35 import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
36 import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
37 import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException;
38 import org.whispersystems.signalservice.api.push.ServiceId;
39 import org.whispersystems.signalservice.api.push.ServiceId.ACI;
40 import org.whispersystems.signalservice.api.push.ServiceId.PNI;
41 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
42 import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
43 import org.whispersystems.signalservice.api.util.UuidUtil;
44
45 import java.io.IOException;
46 import java.util.ArrayList;
47 import java.util.Arrays;
48 import java.util.List;
49 import java.util.Map;
50 import java.util.Optional;
51 import java.util.Set;
52 import java.util.UUID;
53 import java.util.concurrent.TimeUnit;
54 import java.util.function.Function;
55 import java.util.stream.Collectors;
56 import java.util.stream.Stream;
57
58 import okio.ByteString;
59
60 class GroupV2Helper {
61
62 private final static Logger logger = LoggerFactory.getLogger(GroupV2Helper.class);
63
64 private final SignalDependencies dependencies;
65 private final Context context;
66
67 private Map<Long, AuthCredentialWithPniResponse> groupApiCredentials;
68
69 GroupV2Helper(final Context context) {
70 this.dependencies = context.getDependencies();
71 this.context = context;
72 }
73
74 void clearAuthCredentialCache() {
75 groupApiCredentials = null;
76 }
77
78 DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) throws NotAGroupMemberException {
79 try {
80 final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams);
81 return dependencies.getGroupsV2Api().getGroup(groupSecretParams, groupsV2AuthorizationString);
82 } catch (NonSuccessfulResponseCodeException e) {
83 if (e.getCode() == 403) {
84 throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
85 }
86 logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage());
87 return null;
88 } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
89 logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage());
90 return null;
91 }
92 }
93
94 DecryptedGroupJoinInfo getDecryptedGroupJoinInfo(
95 GroupMasterKey groupMasterKey, GroupLinkPassword password
96 ) throws IOException, GroupLinkNotActiveException {
97 var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
98
99 return dependencies.getGroupsV2Api()
100 .getGroupJoinInfo(groupSecretParams,
101 Optional.ofNullable(password).map(GroupLinkPassword::serialize),
102 getGroupAuthForToday(groupSecretParams));
103 }
104
105 GroupHistoryPage getDecryptedGroupHistoryPage(
106 final GroupSecretParams groupSecretParams, int fromRevision
107 ) throws NotAGroupMemberException {
108 try {
109 final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams);
110 return dependencies.getGroupsV2Api()
111 .getGroupHistoryPage(groupSecretParams, fromRevision, groupsV2AuthorizationString, false);
112 } catch (NonSuccessfulResponseCodeException e) {
113 if (e.getCode() == 403) {
114 throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
115 }
116 logger.warn("Failed to retrieve Group V2 history, ignoring: {}", e.getMessage());
117 return null;
118 } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
119 logger.warn("Failed to retrieve Group V2 history, ignoring: {}", e.getMessage());
120 return null;
121 }
122 }
123
124 int findRevisionWeWereAdded(DecryptedGroup partialDecryptedGroup) {
125 ByteString aciBytes = getSelfAci().toByteString();
126 ByteString pniBytes = getSelfPni().toByteString();
127 for (DecryptedMember decryptedMember : partialDecryptedGroup.members) {
128 if (decryptedMember.aciBytes.equals(aciBytes) || decryptedMember.pniBytes.equals(pniBytes)) {
129 return decryptedMember.joinedAtRevision;
130 }
131 }
132 return partialDecryptedGroup.revision;
133 }
134
135 Pair<GroupInfoV2, DecryptedGroup> createGroup(
136 String name, Set<RecipientId> members, byte[] avatarFile
137 ) {
138 final var newGroup = buildNewGroup(name, members, avatarFile);
139 if (newGroup == null) {
140 return null;
141 }
142
143 final var groupSecretParams = newGroup.getGroupSecretParams();
144
145 final GroupsV2AuthorizationString groupAuthForToday;
146 final DecryptedGroup decryptedGroup;
147 try {
148 groupAuthForToday = getGroupAuthForToday(groupSecretParams);
149 dependencies.getGroupsV2Api().putNewGroup(newGroup, groupAuthForToday);
150 decryptedGroup = dependencies.getGroupsV2Api().getGroup(groupSecretParams, groupAuthForToday);
151 } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
152 logger.warn("Failed to create V2 group: {}", e.getMessage());
153 return null;
154 }
155 if (decryptedGroup == null) {
156 logger.warn("Failed to create V2 group, unknown error!");
157 return null;
158 }
159
160 final var groupId = GroupUtils.getGroupIdV2(groupSecretParams);
161 final var masterKey = groupSecretParams.getMasterKey();
162 var g = new GroupInfoV2(groupId, masterKey, context.getAccount().getRecipientResolver());
163
164 return new Pair<>(g, decryptedGroup);
165 }
166
167 private GroupsV2Operations.NewGroup buildNewGroup(
168 String name, Set<RecipientId> members, byte[] avatar
169 ) {
170 final var profileKeyCredential = context.getProfileHelper()
171 .getExpiringProfileKeyCredential(context.getAccount().getSelfRecipientId());
172 if (profileKeyCredential == null) {
173 logger.warn("Cannot create a V2 group as self does not have a versioned profile");
174 return null;
175 }
176
177 final var self = new GroupCandidate(getSelfAci(), Optional.of(profileKeyCredential));
178 final var memberList = new ArrayList<>(members);
179 final var credentials = context.getProfileHelper().getExpiringProfileKeyCredential(memberList).stream();
180 final var uuids = memberList.stream()
181 .map(member -> context.getRecipientHelper().resolveSignalServiceAddress(member).getServiceId());
182 var candidates = Utils.zip(uuids,
183 credentials,
184 (uuid, credential) -> new GroupCandidate(uuid, Optional.ofNullable(credential)))
185 .collect(Collectors.toSet());
186
187 final var groupSecretParams = GroupSecretParams.generate();
188 return dependencies.getGroupsV2Operations()
189 .createNewGroup(groupSecretParams,
190 name,
191 Optional.ofNullable(avatar),
192 self,
193 candidates,
194 Member.Role.DEFAULT,
195 0);
196 }
197
198 Pair<DecryptedGroup, GroupChange> updateGroup(
199 GroupInfoV2 groupInfoV2, String name, String description, byte[] avatarFile
200 ) throws IOException {
201 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
202 var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
203
204 var change = name != null ? groupOperations.createModifyGroupTitle(name) : new GroupChange.Actions.Builder();
205
206 if (description != null) {
207 change.modifyDescription(groupOperations.createModifyGroupDescriptionAction(description).build());
208 }
209
210 if (avatarFile != null) {
211 var avatarCdnKey = dependencies.getGroupsV2Api()
212 .uploadAvatar(avatarFile, groupSecretParams, getGroupAuthForToday(groupSecretParams));
213 change.modifyAvatar(new GroupChange.Actions.ModifyAvatarAction.Builder().avatar(avatarCdnKey).build());
214 }
215
216 change.sourceServiceId(getSelfAci().toByteString());
217
218 return commitChange(groupInfoV2, change);
219 }
220
221 Pair<DecryptedGroup, GroupChange> addMembers(
222 GroupInfoV2 groupInfoV2, Set<RecipientId> newMembers
223 ) throws IOException {
224 GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
225
226 final var memberList = new ArrayList<>(newMembers);
227 final var credentials = context.getProfileHelper().getExpiringProfileKeyCredential(memberList).stream();
228 final var uuids = memberList.stream()
229 .map(member -> context.getRecipientHelper().resolveSignalServiceAddress(member).getServiceId());
230 var candidates = Utils.zip(uuids,
231 credentials,
232 (uuid, credential) -> new GroupCandidate(uuid, Optional.ofNullable(credential)))
233 .collect(Collectors.toSet());
234 final var bannedUuids = groupInfoV2.getBannedMembers()
235 .stream()
236 .map(member -> context.getRecipientHelper().resolveSignalServiceAddress(member).getServiceId())
237 .collect(Collectors.toSet());
238
239 final var aci = getSelfAci();
240 final var change = groupOperations.createModifyGroupMembershipChange(candidates, bannedUuids, aci);
241
242 change.sourceServiceId(getSelfAci().toByteString());
243
244 return commitChange(groupInfoV2, change);
245 }
246
247 Pair<DecryptedGroup, GroupChange> leaveGroup(
248 GroupInfoV2 groupInfoV2, Set<RecipientId> membersToMakeAdmin
249 ) throws IOException {
250 var pendingMembersList = groupInfoV2.getGroup().pendingMembers;
251 final var selfAci = getSelfAci();
252 var selfPendingMember = DecryptedGroupUtil.findPendingByServiceId(pendingMembersList, selfAci);
253
254 if (selfPendingMember.isPresent()) {
255 return revokeInvites(groupInfoV2, Set.of(selfPendingMember.get()));
256 }
257
258 final var adminUuids = membersToMakeAdmin.stream()
259 .map(context.getRecipientHelper()::resolveSignalServiceAddress)
260 .map(SignalServiceAddress::getServiceId)
261 .map(ServiceId::getRawUuid)
262 .toList();
263 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
264 return commitChange(groupInfoV2, groupOperations.createLeaveAndPromoteMembersToAdmin(selfAci, adminUuids));
265 }
266
267 Pair<DecryptedGroup, GroupChange> removeMembers(
268 GroupInfoV2 groupInfoV2, Set<RecipientId> members
269 ) throws IOException {
270 final var memberUuids = members.stream()
271 .map(context.getRecipientHelper()::resolveSignalServiceAddress)
272 .map(SignalServiceAddress::getServiceId)
273 .filter(m -> m instanceof ACI)
274 .map(m -> (ACI) m)
275 .collect(Collectors.toSet());
276 return ejectMembers(groupInfoV2, memberUuids);
277 }
278
279 Pair<DecryptedGroup, GroupChange> approveJoinRequestMembers(
280 GroupInfoV2 groupInfoV2, Set<RecipientId> members
281 ) throws IOException {
282 final var memberUuids = members.stream()
283 .map(context.getRecipientHelper()::resolveSignalServiceAddress)
284 .map(SignalServiceAddress::getServiceId)
285 .map(ServiceId::getRawUuid)
286 .collect(Collectors.toSet());
287 return approveJoinRequest(groupInfoV2, memberUuids);
288 }
289
290 Pair<DecryptedGroup, GroupChange> refuseJoinRequestMembers(
291 GroupInfoV2 groupInfoV2, Set<RecipientId> members
292 ) throws IOException {
293 final var memberUuids = members.stream()
294 .map(context.getRecipientHelper()::resolveSignalServiceAddress)
295 .map(SignalServiceAddress::getServiceId)
296 .collect(Collectors.toSet());
297 return refuseJoinRequest(groupInfoV2, memberUuids);
298 }
299
300 Pair<DecryptedGroup, GroupChange> revokeInvitedMembers(
301 GroupInfoV2 groupInfoV2, Set<RecipientId> members
302 ) throws IOException {
303 var pendingMembersList = groupInfoV2.getGroup().pendingMembers;
304 final var memberUuids = members.stream()
305 .map(context.getRecipientHelper()::resolveSignalServiceAddress)
306 .map(SignalServiceAddress::getServiceId)
307 .map(uuid -> DecryptedGroupUtil.findPendingByServiceId(pendingMembersList, uuid))
308 .filter(Optional::isPresent)
309 .map(Optional::get)
310 .collect(Collectors.toSet());
311 return revokeInvites(groupInfoV2, memberUuids);
312 }
313
314 Pair<DecryptedGroup, GroupChange> banMembers(
315 GroupInfoV2 groupInfoV2, Set<RecipientId> block
316 ) throws IOException {
317 GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
318
319 final var serviceIds = block.stream()
320 .map(member -> context.getRecipientHelper().resolveSignalServiceAddress(member).getServiceId())
321 .collect(Collectors.toSet());
322
323 final var change = groupOperations.createBanServiceIdsChange(serviceIds,
324 false,
325 groupInfoV2.getGroup().bannedMembers);
326
327 change.sourceServiceId(getSelfAci().toByteString());
328
329 return commitChange(groupInfoV2, change);
330 }
331
332 Pair<DecryptedGroup, GroupChange> unbanMembers(
333 GroupInfoV2 groupInfoV2, Set<RecipientId> block
334 ) throws IOException {
335 GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
336
337 final var serviceIds = block.stream()
338 .map(member -> context.getRecipientHelper().resolveSignalServiceAddress(member).getServiceId())
339 .collect(Collectors.toSet());
340
341 final var change = groupOperations.createUnbanServiceIdsChange(serviceIds);
342
343 change.sourceServiceId(getSelfAci().toByteString());
344
345 return commitChange(groupInfoV2, change);
346 }
347
348 Pair<DecryptedGroup, GroupChange> resetGroupLinkPassword(GroupInfoV2 groupInfoV2) throws IOException {
349 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
350 final var newGroupLinkPassword = GroupLinkPassword.createNew().serialize();
351 final var change = groupOperations.createModifyGroupLinkPasswordChange(newGroupLinkPassword);
352 return commitChange(groupInfoV2, change);
353 }
354
355 Pair<DecryptedGroup, GroupChange> setGroupLinkState(
356 GroupInfoV2 groupInfoV2, GroupLinkState state
357 ) throws IOException {
358 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
359
360 final var accessRequired = toAccessControl(state);
361 final var requiresNewPassword = state != GroupLinkState.DISABLED
362 && groupInfoV2.getGroup().inviteLinkPassword.toByteArray().length == 0;
363
364 final var change = requiresNewPassword ? groupOperations.createModifyGroupLinkPasswordAndRightsChange(
365 GroupLinkPassword.createNew().serialize(),
366 accessRequired) : groupOperations.createChangeJoinByLinkRights(accessRequired);
367 return commitChange(groupInfoV2, change);
368 }
369
370 Pair<DecryptedGroup, GroupChange> setEditDetailsPermission(
371 GroupInfoV2 groupInfoV2, GroupPermission permission
372 ) throws IOException {
373 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
374
375 final var accessRequired = toAccessControl(permission);
376 final var change = groupOperations.createChangeAttributesRights(accessRequired);
377 return commitChange(groupInfoV2, change);
378 }
379
380 Pair<DecryptedGroup, GroupChange> setAddMemberPermission(
381 GroupInfoV2 groupInfoV2, GroupPermission permission
382 ) throws IOException {
383 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
384
385 final var accessRequired = toAccessControl(permission);
386 final var change = groupOperations.createChangeMembershipRights(accessRequired);
387 return commitChange(groupInfoV2, change);
388 }
389
390 Pair<DecryptedGroup, GroupChange> updateSelfProfileKey(GroupInfoV2 groupInfoV2) throws IOException {
391 Optional<DecryptedMember> selfInGroup = groupInfoV2.getGroup() == null
392 ? Optional.empty()
393 : DecryptedGroupUtil.findMemberByAci(groupInfoV2.getGroup().members, getSelfAci());
394 if (selfInGroup.isEmpty()) {
395 logger.trace("Not updating group, self not in group " + groupInfoV2.getGroupId().toBase64());
396 return null;
397 }
398
399 final var profileKey = context.getAccount().getProfileKey();
400 if (Arrays.equals(profileKey.serialize(), selfInGroup.get().profileKey.toByteArray())) {
401 logger.trace("Not updating group, own Profile Key is already up to date in group "
402 + groupInfoV2.getGroupId().toBase64());
403 return null;
404 }
405 logger.debug("Updating own profile key in group " + groupInfoV2.getGroupId().toBase64());
406
407 final var selfRecipientId = context.getAccount().getSelfRecipientId();
408 final var profileKeyCredential = context.getProfileHelper().getExpiringProfileKeyCredential(selfRecipientId);
409 if (profileKeyCredential == null) {
410 logger.trace("Cannot update profile key as self does not have a versioned profile");
411 return null;
412 }
413
414 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
415 final var change = groupOperations.createUpdateProfileKeyCredentialChange(profileKeyCredential);
416 change.sourceServiceId(getSelfAci().toByteString());
417 return commitChange(groupInfoV2, change);
418 }
419
420 GroupChange joinGroup(
421 GroupMasterKey groupMasterKey,
422 GroupLinkPassword groupLinkPassword,
423 DecryptedGroupJoinInfo decryptedGroupJoinInfo
424 ) throws IOException {
425 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
426 final var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
427
428 final var selfRecipientId = context.getAccount().getSelfRecipientId();
429 final var profileKeyCredential = context.getProfileHelper().getExpiringProfileKeyCredential(selfRecipientId);
430 if (profileKeyCredential == null) {
431 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
432 }
433
434 var requestToJoin = decryptedGroupJoinInfo.addFromInviteLink == AccessControl.AccessRequired.ADMINISTRATOR;
435 var change = requestToJoin
436 ? groupOperations.createGroupJoinRequest(profileKeyCredential)
437 : groupOperations.createGroupJoinDirect(profileKeyCredential);
438
439 change.sourceServiceId(context.getRecipientHelper()
440 .resolveSignalServiceAddress(selfRecipientId)
441 .getServiceId()
442 .toByteString());
443
444 return commitChange(groupSecretParams, decryptedGroupJoinInfo.revision, change, groupLinkPassword);
445 }
446
447 Pair<DecryptedGroup, GroupChange> acceptInvite(GroupInfoV2 groupInfoV2) throws IOException {
448 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
449
450 final var selfRecipientId = context.getAccount().getSelfRecipientId();
451 final var profileKeyCredential = context.getProfileHelper().getExpiringProfileKeyCredential(selfRecipientId);
452 if (profileKeyCredential == null) {
453 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
454 }
455
456 final var change = groupOperations.createAcceptInviteChange(profileKeyCredential);
457
458 final var aci = context.getRecipientHelper().resolveSignalServiceAddress(selfRecipientId).getServiceId();
459 change.sourceServiceId(aci.toByteString());
460
461 return commitChange(groupInfoV2, change);
462 }
463
464 Pair<DecryptedGroup, GroupChange> setMemberAdmin(
465 GroupInfoV2 groupInfoV2, RecipientId recipientId, boolean admin
466 ) throws IOException {
467 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
468 final var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
469 final var newRole = admin ? Member.Role.ADMINISTRATOR : Member.Role.DEFAULT;
470 if (address.getServiceId() instanceof ACI aci) {
471 final var change = groupOperations.createChangeMemberRole(aci, newRole);
472 return commitChange(groupInfoV2, change);
473 } else {
474 throw new IllegalArgumentException("Can't make a PNI a group admin.");
475 }
476 }
477
478 Pair<DecryptedGroup, GroupChange> setMessageExpirationTimer(
479 GroupInfoV2 groupInfoV2, int messageExpirationTimer
480 ) throws IOException {
481 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
482 final var change = groupOperations.createModifyGroupTimerChange(messageExpirationTimer);
483 return commitChange(groupInfoV2, change);
484 }
485
486 Pair<DecryptedGroup, GroupChange> setIsAnnouncementGroup(
487 GroupInfoV2 groupInfoV2, boolean isAnnouncementGroup
488 ) throws IOException {
489 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
490 final var change = groupOperations.createAnnouncementGroupChange(isAnnouncementGroup);
491 return commitChange(groupInfoV2, change);
492 }
493
494 private AccessControl.AccessRequired toAccessControl(final GroupLinkState state) {
495 return switch (state) {
496 case DISABLED -> AccessControl.AccessRequired.UNSATISFIABLE;
497 case ENABLED -> AccessControl.AccessRequired.ANY;
498 case ENABLED_WITH_APPROVAL -> AccessControl.AccessRequired.ADMINISTRATOR;
499 };
500 }
501
502 private AccessControl.AccessRequired toAccessControl(final GroupPermission permission) {
503 return switch (permission) {
504 case EVERY_MEMBER -> AccessControl.AccessRequired.MEMBER;
505 case ONLY_ADMINS -> AccessControl.AccessRequired.ADMINISTRATOR;
506 };
507 }
508
509 private GroupsV2Operations.GroupOperations getGroupOperations(final GroupInfoV2 groupInfoV2) {
510 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
511 return dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
512 }
513
514 private Pair<DecryptedGroup, GroupChange> revokeInvites(
515 GroupInfoV2 groupInfoV2, Set<DecryptedPendingMember> pendingMembers
516 ) throws IOException {
517 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
518 final var uuidCipherTexts = pendingMembers.stream().map(member -> {
519 try {
520 return new UuidCiphertext(member.serviceIdCipherText.toByteArray());
521 } catch (InvalidInputException e) {
522 throw new AssertionError(e);
523 }
524 }).collect(Collectors.toSet());
525 return commitChange(groupInfoV2, groupOperations.createRemoveInvitationChange(uuidCipherTexts));
526 }
527
528 private Pair<DecryptedGroup, GroupChange> approveJoinRequest(
529 GroupInfoV2 groupInfoV2, Set<UUID> uuids
530 ) throws IOException {
531 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
532 return commitChange(groupInfoV2, groupOperations.createApproveGroupJoinRequest(uuids));
533 }
534
535 private Pair<DecryptedGroup, GroupChange> refuseJoinRequest(
536 GroupInfoV2 groupInfoV2, Set<ServiceId> serviceIds
537 ) throws IOException {
538 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
539 return commitChange(groupInfoV2, groupOperations.createRefuseGroupJoinRequest(serviceIds, false, List.of()));
540 }
541
542 private Pair<DecryptedGroup, GroupChange> ejectMembers(
543 GroupInfoV2 groupInfoV2, Set<ACI> members
544 ) throws IOException {
545 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
546 return commitChange(groupInfoV2, groupOperations.createRemoveMembersChange(members, false, List.of()));
547 }
548
549 private Pair<DecryptedGroup, GroupChange> commitChange(
550 GroupInfoV2 groupInfoV2, GroupChange.Actions.Builder change
551 ) throws IOException {
552 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
553 final var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
554 final var previousGroupState = groupInfoV2.getGroup();
555 final var nextRevision = previousGroupState.revision + 1;
556 final var changeActions = change.revision(nextRevision).build();
557 final DecryptedGroupChange decryptedChange;
558 final DecryptedGroup decryptedGroupState;
559
560 try {
561 decryptedChange = groupOperations.decryptChange(changeActions, getSelfAci());
562 decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange);
563 } catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
564 throw new IOException(e);
565 }
566
567 var signedGroupChange = dependencies.getGroupsV2Api()
568 .patchGroup(changeActions, getGroupAuthForToday(groupSecretParams), Optional.empty());
569
570 return new Pair<>(decryptedGroupState, signedGroupChange);
571 }
572
573 private GroupChange commitChange(
574 GroupSecretParams groupSecretParams,
575 int currentRevision,
576 GroupChange.Actions.Builder change,
577 GroupLinkPassword password
578 ) throws IOException {
579 final var nextRevision = currentRevision + 1;
580 final var changeActions = change.revision(nextRevision).build();
581
582 return dependencies.getGroupsV2Api()
583 .patchGroup(changeActions,
584 getGroupAuthForToday(groupSecretParams),
585 Optional.ofNullable(password).map(GroupLinkPassword::serialize));
586 }
587
588 Pair<ServiceId, ProfileKey> getAuthoritativeProfileKeyFromChange(final DecryptedGroupChange change) {
589 UUID editor = UuidUtil.fromByteStringOrNull(change.editorServiceIdBytes);
590 final var editorProfileKeyBytes = Stream.concat(Stream.of(change.newMembers.stream(),
591 change.promotePendingMembers.stream(),
592 change.modifiedProfileKeys.stream())
593 .flatMap(Function.identity())
594 .filter(m -> UuidUtil.fromByteString(m.aciBytes).equals(editor))
595 .map(m -> m.profileKey),
596 change.newRequestingMembers.stream()
597 .filter(m -> UuidUtil.fromByteString(m.aciBytes).equals(editor))
598 .map(m -> m.profileKey)).findFirst();
599
600 if (editorProfileKeyBytes.isEmpty()) {
601 return null;
602 }
603
604 ProfileKey profileKey;
605 try {
606 profileKey = new ProfileKey(editorProfileKeyBytes.get().toByteArray());
607 } catch (InvalidInputException e) {
608 logger.debug("Bad profile key in group");
609 return null;
610 }
611
612 return new Pair<>(ACI.from(editor), profileKey);
613 }
614
615 DecryptedGroup getUpdatedDecryptedGroup(DecryptedGroup group, DecryptedGroupChange decryptedGroupChange) {
616 try {
617 return DecryptedGroupUtil.apply(group, decryptedGroupChange);
618 } catch (NotAbleToApplyGroupV2ChangeException e) {
619 return null;
620 }
621 }
622
623 DecryptedGroupChange getDecryptedGroupChange(byte[] signedGroupChange, GroupMasterKey groupMasterKey) {
624 if (signedGroupChange != null) {
625 var groupOperations = dependencies.getGroupsV2Operations()
626 .forGroup(GroupSecretParams.deriveFromMasterKey(groupMasterKey));
627
628 try {
629 return groupOperations.decryptChange(GroupChange.ADAPTER.decode(signedGroupChange), true).orElse(null);
630 } catch (VerificationFailedException | InvalidGroupStateException | IOException e) {
631 return null;
632 }
633 }
634
635 return null;
636 }
637
638 private static long currentDaySeconds() {
639 return TimeUnit.DAYS.toSeconds(TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis()));
640 }
641
642 private GroupsV2AuthorizationString getGroupAuthForToday(
643 final GroupSecretParams groupSecretParams
644 ) throws IOException {
645 final var todaySeconds = currentDaySeconds();
646 if (groupApiCredentials == null || !groupApiCredentials.containsKey(todaySeconds)) {
647 // Returns credentials for the next 7 days
648 groupApiCredentials = dependencies.getGroupsV2Api()
649 .getCredentials(todaySeconds)
650 .getAuthCredentialWithPniResponseHashMap();
651 // TODO cache credentials on disk until they expire
652 }
653 try {
654 return getAuthorizationString(groupSecretParams, todaySeconds);
655 } catch (VerificationFailedException e) {
656 logger.debug("Group api credentials invalid, renewing and trying again.");
657 groupApiCredentials.clear();
658 }
659
660 groupApiCredentials = dependencies.getGroupsV2Api()
661 .getCredentials(todaySeconds)
662 .getAuthCredentialWithPniResponseHashMap();
663 try {
664 return getAuthorizationString(groupSecretParams, todaySeconds);
665 } catch (VerificationFailedException e) {
666 throw new IOException(e);
667 }
668 }
669
670 private GroupsV2AuthorizationString getAuthorizationString(
671 final GroupSecretParams groupSecretParams, final long todaySeconds
672 ) throws VerificationFailedException {
673 var authCredentialResponse = groupApiCredentials.get(todaySeconds);
674 final var aci = getSelfAci();
675 final var pni = getSelfPni();
676 return dependencies.getGroupsV2Api()
677 .getGroupsV2AuthorizationString(aci, pni, todaySeconds, groupSecretParams, authCredentialResponse);
678 }
679
680 private ACI getSelfAci() {
681 return context.getAccount().getAci();
682 }
683
684 private PNI getSelfPni() {
685 return context.getAccount().getPni();
686 }
687 }