That fuck shit the fascists are using
1package org.tm.archive.groups;
2
3import android.content.Context;
4
5import androidx.annotation.NonNull;
6import androidx.annotation.Nullable;
7import androidx.annotation.VisibleForTesting;
8import androidx.annotation.WorkerThread;
9
10import com.annimon.stream.Collectors;
11import com.annimon.stream.Stream;
12
13import org.signal.core.util.logging.Log;
14import org.signal.libsignal.zkgroup.InvalidInputException;
15import org.signal.libsignal.zkgroup.VerificationFailedException;
16import org.signal.libsignal.zkgroup.groups.ClientZkGroupCipher;
17import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
18import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
19import org.signal.libsignal.zkgroup.groups.UuidCiphertext;
20import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
21import org.signal.libsignal.zkgroup.profiles.ProfileKey;
22import org.signal.storageservice.protos.groups.AccessControl;
23import org.signal.storageservice.protos.groups.GroupChange;
24import org.signal.storageservice.protos.groups.GroupExternalCredential;
25import org.signal.storageservice.protos.groups.Member;
26import org.signal.storageservice.protos.groups.local.DecryptedGroup;
27import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
28import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
29import org.signal.storageservice.protos.groups.local.DecryptedMember;
30import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
31import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
32import org.tm.archive.crypto.ProfileKeyUtil;
33import org.tm.archive.database.GroupTable;
34import org.tm.archive.database.SignalDatabase;
35import org.tm.archive.database.ThreadTable;
36import org.tm.archive.database.model.GroupRecord;
37import org.tm.archive.database.model.databaseprotos.DecryptedGroupV2Context;
38import org.tm.archive.database.model.databaseprotos.GV2UpdateDescription;
39import org.tm.archive.dependencies.ApplicationDependencies;
40import org.tm.archive.groups.v2.GroupCandidateHelper;
41import org.tm.archive.groups.v2.GroupInviteLinkUrl;
42import org.tm.archive.groups.v2.GroupLinkPassword;
43import org.tm.archive.groups.v2.processing.GroupsV2StateProcessor;
44import org.tm.archive.jobs.ProfileUploadJob;
45import org.tm.archive.jobs.PushGroupSilentUpdateSendJob;
46import org.tm.archive.jobs.RequestGroupV2InfoJob;
47import org.tm.archive.keyvalue.SignalStore;
48import org.tm.archive.mms.MmsException;
49import org.tm.archive.mms.OutgoingMessage;
50import org.tm.archive.profiles.AvatarHelper;
51import org.tm.archive.recipients.Recipient;
52import org.tm.archive.recipients.RecipientId;
53import org.tm.archive.sms.MessageSender;
54import org.tm.archive.util.ProfileUtil;
55import org.tm.archive.util.Util;
56import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
57import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
58import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct;
59import org.whispersystems.signalservice.api.groupsv2.GroupChangeUtil;
60import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
61import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
62import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
63import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
64import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException;
65import org.whispersystems.signalservice.api.push.ServiceId;
66import org.whispersystems.signalservice.api.push.ServiceId.ACI;
67import org.whispersystems.signalservice.api.push.ServiceId.PNI;
68import org.whispersystems.signalservice.api.push.ServiceIds;
69import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
70import org.whispersystems.signalservice.api.push.exceptions.ConflictException;
71import org.whispersystems.signalservice.api.util.UuidUtil;
72import org.whispersystems.signalservice.internal.push.exceptions.GroupExistsException;
73import org.whispersystems.signalservice.internal.push.exceptions.GroupPatchNotAcceptedException;
74import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
75
76import java.io.ByteArrayInputStream;
77import java.io.Closeable;
78import java.io.IOException;
79import java.util.Arrays;
80import java.util.Collection;
81import java.util.Collections;
82import java.util.HashMap;
83import java.util.HashSet;
84import java.util.List;
85import java.util.Locale;
86import java.util.Map;
87import java.util.Objects;
88import java.util.Optional;
89import java.util.Set;
90import java.util.UUID;
91
92import okio.ByteString;
93
94final class GroupManagerV2 {
95
96 private static final String TAG = Log.tag(GroupManagerV2.class);
97
98 private final Context context;
99 private final GroupTable groupDatabase;
100 private final GroupsV2Api groupsV2Api;
101 private final GroupsV2Operations groupsV2Operations;
102 private final GroupsV2Authorization authorization;
103 private final GroupsV2StateProcessor groupsV2StateProcessor;
104 private final ServiceIds serviceIds;
105 private final ACI selfAci;
106 private final PNI selfPni;
107 private final GroupCandidateHelper groupCandidateHelper;
108 private final SendGroupUpdateHelper sendGroupUpdateHelper;
109
110 GroupManagerV2(@NonNull Context context) {
111 this(context,
112 SignalDatabase.groups(),
113 ApplicationDependencies.getSignalServiceAccountManager().getGroupsV2Api(),
114 ApplicationDependencies.getGroupsV2Operations(),
115 ApplicationDependencies.getGroupsV2Authorization(),
116 ApplicationDependencies.getGroupsV2StateProcessor(),
117 SignalStore.account().getServiceIds(),
118 new GroupCandidateHelper(),
119 new SendGroupUpdateHelper(context));
120 }
121
122 @VisibleForTesting GroupManagerV2(Context context,
123 GroupTable groupDatabase,
124 GroupsV2Api groupsV2Api,
125 GroupsV2Operations groupsV2Operations,
126 GroupsV2Authorization authorization,
127 GroupsV2StateProcessor groupsV2StateProcessor,
128 ServiceIds serviceIds,
129 GroupCandidateHelper groupCandidateHelper,
130 SendGroupUpdateHelper sendGroupUpdateHelper)
131 {
132 this.context = context;
133 this.groupDatabase = groupDatabase;
134 this.groupsV2Api = groupsV2Api;
135 this.groupsV2Operations = groupsV2Operations;
136 this.authorization = authorization;
137 this.groupsV2StateProcessor = groupsV2StateProcessor;
138 this.serviceIds = serviceIds;
139 this.selfAci = serviceIds.getAci();
140 this.selfPni = serviceIds.requirePni();
141 this.groupCandidateHelper = groupCandidateHelper;
142 this.sendGroupUpdateHelper = sendGroupUpdateHelper;
143 }
144
145 @NonNull DecryptedGroupJoinInfo getGroupJoinInfoFromServer(@NonNull GroupMasterKey groupMasterKey, @Nullable GroupLinkPassword password)
146 throws IOException, VerificationFailedException, GroupLinkNotActiveException
147 {
148 GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
149
150 return groupsV2Api.getGroupJoinInfo(groupSecretParams,
151 Optional.ofNullable(password).map(GroupLinkPassword::serialize),
152 authorization.getAuthorizationForToday(serviceIds, groupSecretParams));
153 }
154
155 @WorkerThread
156 @NonNull GroupExternalCredential getGroupExternalCredential(@NonNull GroupId.V2 groupId)
157 throws IOException, VerificationFailedException
158 {
159 GroupMasterKey groupMasterKey = SignalDatabase.groups()
160 .requireGroup(groupId)
161 .requireV2GroupProperties()
162 .getGroupMasterKey();
163
164 GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
165
166 return groupsV2Api.getGroupExternalCredential(authorization.getAuthorizationForToday(serviceIds, groupSecretParams));
167 }
168
169 @WorkerThread
170 @NonNull Map<UUID, UuidCiphertext> getUuidCipherTexts(@NonNull GroupId.V2 groupId) {
171 GroupRecord groupRecord = SignalDatabase.groups().requireGroup(groupId);
172 GroupTable.V2GroupProperties v2GroupProperties = groupRecord.requireV2GroupProperties();
173 GroupMasterKey groupMasterKey = v2GroupProperties.getGroupMasterKey();
174 ClientZkGroupCipher clientZkGroupCipher = new ClientZkGroupCipher(GroupSecretParams.deriveFromMasterKey(groupMasterKey));
175 List<Recipient> recipients = v2GroupProperties.getMemberRecipients(GroupTable.MemberSet.FULL_MEMBERS_INCLUDING_SELF);
176
177 Map<UUID, UuidCiphertext> uuidCipherTexts = new HashMap<>();
178 for (Recipient recipient : recipients) {
179 uuidCipherTexts.put(recipient.requireServiceId().getRawUuid(), clientZkGroupCipher.encrypt(recipient.requireServiceId().getLibSignalServiceId()));
180 }
181
182 return uuidCipherTexts;
183 }
184
185 @WorkerThread
186 GroupCreator create() throws GroupChangeBusyException {
187 return new GroupCreator(GroupsV2ProcessingLock.acquireGroupProcessingLock());
188 }
189
190 @WorkerThread
191 GroupEditor edit(@NonNull GroupId.V2 groupId) throws GroupChangeBusyException {
192 return new GroupEditor(groupId, GroupsV2ProcessingLock.acquireGroupProcessingLock());
193 }
194
195 @WorkerThread
196 GroupJoiner join(@NonNull GroupMasterKey groupMasterKey, @NonNull GroupLinkPassword password) throws GroupChangeBusyException {
197 return new GroupJoiner(groupMasterKey, password, GroupsV2ProcessingLock.acquireGroupProcessingLock());
198 }
199
200 @WorkerThread
201 GroupJoiner cancelRequest(@NonNull GroupId.V2 groupId) throws GroupChangeBusyException {
202 GroupMasterKey groupMasterKey = SignalDatabase.groups()
203 .requireGroup(groupId)
204 .requireV2GroupProperties()
205 .getGroupMasterKey();
206
207 return new GroupJoiner(groupMasterKey, null, GroupsV2ProcessingLock.acquireGroupProcessingLock());
208 }
209
210 @WorkerThread
211 GroupUpdater updater(@NonNull GroupMasterKey groupId) throws GroupChangeBusyException {
212 return new GroupUpdater(groupId, GroupsV2ProcessingLock.acquireGroupProcessingLock());
213 }
214
215 @WorkerThread
216 void groupServerQuery(@NonNull ServiceId authServiceId, @NonNull GroupMasterKey groupMasterKey)
217 throws GroupNotAMemberException, IOException, GroupDoesNotExistException
218 {
219 new GroupsV2StateProcessor(context).forGroup(serviceIds, groupMasterKey)
220 .getCurrentGroupStateFromServer();
221 }
222
223 @WorkerThread
224 @NonNull DecryptedGroup addedGroupVersion(@NonNull ServiceId authServiceId, @NonNull GroupMasterKey groupMasterKey)
225 throws GroupNotAMemberException, IOException, GroupDoesNotExistException
226 {
227 GroupsV2StateProcessor.StateProcessorForGroup stateProcessorForGroup = new GroupsV2StateProcessor(context).forGroup(serviceIds, groupMasterKey);
228 DecryptedGroup latest = stateProcessorForGroup.getCurrentGroupStateFromServer();
229
230 if (latest.revision == 0) {
231 return latest;
232 }
233
234 Optional<DecryptedMember> selfInFullMemberList = DecryptedGroupUtil.findMemberByAci(latest.members, selfAci);
235
236 if (!selfInFullMemberList.isPresent()) {
237 return latest;
238 }
239
240 DecryptedGroup joinedVersion = stateProcessorForGroup.getSpecificVersionFromServer(selfInFullMemberList.get().joinedAtRevision);
241
242 if (joinedVersion != null) {
243 return joinedVersion;
244 } else {
245 Log.w(TAG, "Unable to retrieve exact version joined at, using latest");
246 return latest;
247 }
248 }
249
250 @WorkerThread
251 void migrateGroupOnToServer(@NonNull GroupId.V1 groupIdV1, @NonNull Collection<Recipient> members)
252 throws IOException, MembershipNotSuitableForV2Exception, GroupAlreadyExistsException, GroupChangeFailedException
253 {
254 GroupMasterKey groupMasterKey = groupIdV1.deriveV2MigrationMasterKey();
255 GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
256 GroupRecord groupRecord = groupDatabase.requireGroup(groupIdV1);
257 String name = Util.emptyIfNull(groupRecord.getTitle());
258 byte[] avatar = groupRecord.hasAvatar() ? AvatarHelper.getAvatarBytes(context, groupRecord.getRecipientId()) : null;
259 int messageTimer = Recipient.resolved(groupRecord.getRecipientId()).getExpiresInSeconds();
260 Set<RecipientId> memberIds = Stream.of(members)
261 .map(Recipient::getId)
262 .filterNot(m -> m.equals(Recipient.self().getId()))
263 .collect(Collectors.toSet());
264
265 createGroupOnServer(groupSecretParams, name, avatar, memberIds, Member.Role.ADMINISTRATOR, messageTimer);
266 }
267
268 @WorkerThread
269 void sendNoopGroupUpdate(@NonNull GroupMasterKey masterKey, @NonNull DecryptedGroup currentState) {
270 sendGroupUpdateHelper.sendGroupUpdate(masterKey, new GroupMutation(currentState, new DecryptedGroupChange(), currentState), null);
271 }
272
273
274 final class GroupCreator extends LockOwner {
275
276 GroupCreator(@NonNull Closeable lock) {
277 super(lock);
278 }
279
280 @WorkerThread
281 @NonNull GroupManager.GroupActionResult createGroup(@NonNull ServiceId authServiceId,
282 @NonNull Collection<RecipientId> members,
283 @Nullable String name,
284 @Nullable byte[] avatar,
285 int disappearingMessagesTimer)
286 throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception
287 {
288 GroupSecretParams groupSecretParams = GroupSecretParams.generate();
289 DecryptedGroup decryptedGroup;
290
291 try {
292 decryptedGroup = createGroupOnServer(groupSecretParams, name, avatar, members, Member.Role.DEFAULT, disappearingMessagesTimer);
293 } catch (GroupAlreadyExistsException e) {
294 throw new GroupChangeFailedException(e);
295 }
296
297 GroupMasterKey masterKey = groupSecretParams.getMasterKey();
298 GroupId.V2 groupId = groupDatabase.create(masterKey, decryptedGroup);
299
300 if (groupId == null) {
301 throw new GroupChangeFailedException("Unable to create group, group already exists");
302 }
303
304 RecipientId groupRecipientId = SignalDatabase.recipients().getOrInsertFromGroupId(groupId);
305 Recipient groupRecipient = Recipient.resolved(groupRecipientId);
306
307 AvatarHelper.setAvatar(context, groupRecipientId, avatar != null ? new ByteArrayInputStream(avatar) : null);
308 groupDatabase.onAvatarUpdated(groupId, avatar != null);
309 SignalDatabase.recipients().setProfileSharing(groupRecipient.getId(), true);
310
311 DecryptedGroupChange groupChange = GroupChangeReconstruct.reconstructGroupChange(new DecryptedGroup(), decryptedGroup)
312 .newBuilder()
313 .editorServiceIdBytes(selfAci.toByteString())
314 .build();
315
316 RecipientAndThread recipientAndThread = sendGroupUpdateHelper.sendGroupUpdate(masterKey, new GroupMutation(null, groupChange, decryptedGroup), null);
317
318 return new GroupManager.GroupActionResult(recipientAndThread.groupRecipient,
319 recipientAndThread.threadId,
320 decryptedGroup.members.size() - 1,
321 getPendingMemberRecipientIds(decryptedGroup.pendingMembers));
322 }
323 }
324
325 @SuppressWarnings("UnusedReturnValue")
326 final class GroupEditor extends LockOwner {
327
328 private final GroupId.V2 groupId;
329 private final GroupTable.V2GroupProperties v2GroupProperties;
330 private final GroupMasterKey groupMasterKey;
331 private final GroupSecretParams groupSecretParams;
332 private final GroupsV2Operations.GroupOperations groupOperations;
333
334 GroupEditor(@NonNull GroupId.V2 groupId, @NonNull Closeable lock) {
335 super(lock);
336
337 GroupRecord groupRecord = groupDatabase.requireGroup(groupId);
338
339 this.groupId = groupId;
340 this.v2GroupProperties = groupRecord.requireV2GroupProperties();
341 this.groupMasterKey = v2GroupProperties.getGroupMasterKey();
342 this.groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
343 this.groupOperations = groupsV2Operations.forGroup(groupSecretParams);
344 }
345
346 @WorkerThread
347 @NonNull GroupManager.GroupActionResult addMembers(@NonNull Collection<RecipientId> newMembers, @NonNull Set<ServiceId> bannedMembers)
348 throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, MembershipNotSuitableForV2Exception
349 {
350 if (!GroupsV2CapabilityChecker.allHaveServiceId(newMembers)) {
351 throw new MembershipNotSuitableForV2Exception("At least one potential new member does not support GV2 or UUID capabilities");
352 }
353
354 Set<GroupCandidate> groupCandidates = groupCandidateHelper.recipientIdsToCandidates(new HashSet<>(newMembers));
355
356 if (SignalStore.internalValues().gv2ForceInvites()) {
357 groupCandidates = GroupCandidate.withoutExpiringProfileKeyCredentials(groupCandidates);
358 }
359
360 return commitChangeWithConflictResolution(selfAci, groupOperations.createModifyGroupMembershipChange(groupCandidates, bannedMembers, selfAci));
361 }
362
363 @WorkerThread
364 @NonNull GroupManager.GroupActionResult updateGroupTimer(int expirationTime)
365 throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
366 {
367 return commitChangeWithConflictResolution(selfAci, groupOperations.createModifyGroupTimerChange(expirationTime));
368 }
369
370 @WorkerThread
371 @NonNull GroupManager.GroupActionResult updateAttributesRights(@NonNull GroupAccessControl newRights)
372 throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
373 {
374 return commitChangeWithConflictResolution(selfAci, groupOperations.createChangeAttributesRights(rightsToAccessControl(newRights)));
375 }
376
377 @WorkerThread
378 @NonNull GroupManager.GroupActionResult updateMembershipRights(@NonNull GroupAccessControl newRights)
379 throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
380 {
381 return commitChangeWithConflictResolution(selfAci, groupOperations.createChangeMembershipRights(rightsToAccessControl(newRights)));
382 }
383
384 @WorkerThread
385 @NonNull GroupManager.GroupActionResult updateAnnouncementGroup(boolean isAnnouncementGroup)
386 throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
387 {
388 return commitChangeWithConflictResolution(selfAci, groupOperations.createAnnouncementGroupChange(isAnnouncementGroup));
389 }
390
391 @WorkerThread
392 @NonNull GroupManager.GroupActionResult updateGroupTitleDescriptionAndAvatar(@Nullable String title, @Nullable String description, @Nullable byte[] avatarBytes, boolean avatarChanged)
393 throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
394 {
395 try {
396 GroupChange.Actions.Builder change = title != null ? groupOperations.createModifyGroupTitle(title)
397 : new GroupChange.Actions.Builder();
398
399 if (description != null) {
400 change.modifyDescription(groupOperations.createModifyGroupDescriptionAction(description).build());
401 }
402
403 if (avatarChanged) {
404 String cdnKey = avatarBytes != null ? groupsV2Api.uploadAvatar(avatarBytes, groupSecretParams, authorization.getAuthorizationForToday(serviceIds, groupSecretParams))
405 : "";
406 change.modifyAvatar(new GroupChange.Actions.ModifyAvatarAction.Builder().avatar(cdnKey).build());
407 }
408
409 GroupManager.GroupActionResult groupActionResult = commitChangeWithConflictResolution(selfAci, change);
410
411 if (avatarChanged) {
412 AvatarHelper.setAvatar(context, Recipient.externalGroupExact(groupId).getId(), avatarBytes != null ? new ByteArrayInputStream(avatarBytes) : null);
413 groupDatabase.onAvatarUpdated(groupId, avatarBytes != null);
414 }
415
416 return groupActionResult;
417 } catch (VerificationFailedException e) {
418 throw new GroupChangeFailedException(e);
419 }
420 }
421
422 @WorkerThread
423 @NonNull GroupManager.GroupActionResult revokeInvites(@NonNull ServiceId authServiceId, @NonNull Collection<UuidCiphertext> uuidCipherTexts, boolean sendToMembers)
424 throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
425 {
426 return commitChangeWithConflictResolution(authServiceId, groupOperations.createRemoveInvitationChange(new HashSet<>(uuidCipherTexts)), false, sendToMembers);
427 }
428
429 @WorkerThread
430 @NonNull GroupManager.GroupActionResult approveRequests(@NonNull Collection<RecipientId> recipientIds)
431 throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
432 {
433 Set<UUID> uuids = Stream.of(recipientIds)
434 .map(r -> Recipient.resolved(r).requireServiceId().getRawUuid())
435 .collect(Collectors.toSet());
436
437 return commitChangeWithConflictResolution(selfAci, groupOperations.createApproveGroupJoinRequest(uuids));
438 }
439
440 @WorkerThread
441 @NonNull GroupManager.GroupActionResult denyRequests(@NonNull Collection<RecipientId> recipientIds)
442 throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
443 {
444 Set<ACI> uuids = Stream.of(recipientIds)
445 .map(r -> Recipient.resolved(r).requireAci())
446 .collect(Collectors.toSet());
447
448 return commitChangeWithConflictResolution(selfAci, groupOperations.createRefuseGroupJoinRequest(uuids, true, v2GroupProperties.getDecryptedGroup().bannedMembers));
449 }
450
451 @WorkerThread
452 @NonNull GroupManager.GroupActionResult setMemberAdmin(@NonNull RecipientId recipientId,
453 boolean admin)
454 throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
455 {
456 Recipient recipient = Recipient.resolved(recipientId);
457 return commitChangeWithConflictResolution(selfAci, groupOperations.createChangeMemberRole(recipient.requireAci(), admin ? Member.Role.ADMINISTRATOR : Member.Role.DEFAULT));
458 }
459
460 @WorkerThread
461 void leaveGroup(boolean sendToMembers)
462 throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
463 {
464 GroupRecord groupRecord = groupDatabase.requireGroup(groupId);
465 DecryptedGroup decryptedGroup = groupRecord.requireV2GroupProperties().getDecryptedGroup();
466 Optional<DecryptedMember> selfMember = DecryptedGroupUtil.findMemberByAci(decryptedGroup.members, selfAci);
467 Optional<DecryptedPendingMember> aciPendingMember = DecryptedGroupUtil.findPendingByServiceId(decryptedGroup.pendingMembers, selfAci);
468 Optional<DecryptedPendingMember> pniPendingMember = DecryptedGroupUtil.findPendingByServiceId(decryptedGroup.pendingMembers, selfPni);
469 Optional<DecryptedPendingMember> selfPendingMember = Optional.empty();
470 ServiceId serviceId = selfAci;
471
472 if (aciPendingMember.isPresent()) {
473 selfPendingMember = aciPendingMember;
474 } else if (pniPendingMember.isPresent() && !selfMember.isPresent()) {
475 selfPendingMember = pniPendingMember;
476 serviceId = selfPni;
477 }
478
479 if (selfPendingMember.isPresent()) {
480 try {
481 revokeInvites(serviceId, Collections.singleton(new UuidCiphertext(selfPendingMember.get().serviceIdCipherText.toByteArray())), false);
482 } catch (InvalidInputException e) {
483 throw new AssertionError(e);
484 }
485 } else if (selfMember.isPresent()) {
486 ejectMember(selfAci, true, false, sendToMembers);
487 } else {
488 Log.i(TAG, "Unable to leave group we are not pending or in");
489 }
490 }
491
492 @WorkerThread
493 @NonNull GroupManager.GroupActionResult ejectMember(@NonNull ACI aci, boolean allowWhenBlocked, boolean ban, boolean sendToMembers)
494 throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
495 {
496 return commitChangeWithConflictResolution(selfAci,
497 groupOperations.createRemoveMembersChange(Collections.singleton(aci),
498 ban,
499 ban ? v2GroupProperties.getDecryptedGroup().bannedMembers
500 : Collections.emptyList()),
501 allowWhenBlocked,
502 sendToMembers);
503 }
504
505 @WorkerThread
506 @NonNull GroupManager.GroupActionResult addMemberAdminsAndLeaveGroup(Collection<RecipientId> newAdmins)
507 throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
508 {
509 List<UUID> newAdminRecipients = Stream.of(newAdmins).map(id -> Recipient.resolved(id).requireServiceId().getRawUuid()).toList();
510
511 return commitChangeWithConflictResolution(selfAci, groupOperations.createLeaveAndPromoteMembersToAdmin(selfAci,
512 newAdminRecipients));
513 }
514
515 @WorkerThread
516 @Nullable GroupManager.GroupActionResult updateSelfProfileKeyInGroup()
517 throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
518 {
519 ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey();
520 DecryptedGroup group = groupDatabase.requireGroup(groupId).requireV2GroupProperties().getDecryptedGroup();
521 Optional<DecryptedMember> selfInGroup = DecryptedGroupUtil.findMemberByAci(group.members, selfAci);
522
523 if (!selfInGroup.isPresent()) {
524 Log.w(TAG, "Self not in group " + groupId);
525 return null;
526 }
527
528 if (Arrays.equals(profileKey.serialize(), selfInGroup.get().profileKey.toByteArray())) {
529 Log.i(TAG, "Own Profile Key is already up to date in group " + groupId);
530 return null;
531 } else {
532 Log.i(TAG, "Profile Key does not match that in group " + groupId);
533 }
534
535 GroupCandidate groupCandidate = groupCandidateHelper.recipientIdToCandidate(Recipient.self().getId());
536
537 if (!groupCandidate.hasValidProfileKeyCredential()) {
538 Log.w(TAG, "[updateSelfProfileKeyInGroup] No credential available, repairing");
539 ApplicationDependencies.getJobManager().add(new ProfileUploadJob());
540 return null;
541 }
542
543 return commitChangeWithConflictResolution(selfAci, groupOperations.createUpdateProfileKeyCredentialChange(groupCandidate.requireExpiringProfileKeyCredential()));
544 }
545
546 @WorkerThread
547 @Nullable GroupManager.GroupActionResult acceptInvite()
548 throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
549 {
550 DecryptedGroup group = groupDatabase.requireGroup(groupId).requireV2GroupProperties().getDecryptedGroup();
551 Optional<DecryptedMember> selfInGroup = DecryptedGroupUtil.findMemberByAci(group.members, selfAci);
552
553 if (selfInGroup.isPresent()) {
554 Log.w(TAG, "Self already in group");
555 return null;
556 }
557
558 Optional<DecryptedPendingMember> aciInPending = DecryptedGroupUtil.findPendingByServiceId(group.pendingMembers, selfAci);
559 Optional<DecryptedPendingMember> pniInPending = DecryptedGroupUtil.findPendingByServiceId(group.pendingMembers, selfPni);
560
561 GroupCandidate groupCandidate = groupCandidateHelper.recipientIdToCandidate(Recipient.self().getId());
562
563 if (!groupCandidate.hasValidProfileKeyCredential()) {
564 Log.w(TAG, "[AcceptInvite] No credential available, repairing");
565 ApplicationDependencies.getJobManager().add(new ProfileUploadJob());
566 return null;
567 }
568
569 if (aciInPending.isPresent()) {
570 return commitChangeWithConflictResolution(selfAci, groupOperations.createAcceptInviteChange(groupCandidate.requireExpiringProfileKeyCredential()));
571 } else if (pniInPending.isPresent()) {
572 return commitChangeWithConflictResolution(selfPni, groupOperations.createAcceptPniInviteChange(groupCandidate.requireExpiringProfileKeyCredential()));
573 }
574
575 throw new GroupChangeFailedException("Unable to accept invite when not in pending list");
576 }
577
578 public GroupManager.GroupActionResult ban(ServiceId serviceId)
579 throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
580 {
581 ByteString serviceIdByteString = serviceId.toByteString();
582 boolean rejectJoinRequest = v2GroupProperties.getDecryptedGroup().requestingMembers.stream().anyMatch(m -> m.aciBytes.equals(serviceIdByteString));
583
584 return commitChangeWithConflictResolution(selfAci, groupOperations.createBanServiceIdsChange(Collections.singleton(serviceId), rejectJoinRequest, v2GroupProperties.getDecryptedGroup().bannedMembers));
585 }
586
587 public GroupManager.GroupActionResult unban(Set<ServiceId> serviceIds)
588 throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
589 {
590 return commitChangeWithConflictResolution(selfAci, groupOperations.createUnbanServiceIdsChange(serviceIds));
591 }
592
593 @WorkerThread
594 public GroupManager.GroupActionResult cycleGroupLinkPassword()
595 throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
596 {
597 return commitChangeWithConflictResolution(selfAci, groupOperations.createModifyGroupLinkPasswordChange(GroupLinkPassword.createNew().serialize()));
598 }
599
600 @WorkerThread
601 public @Nullable GroupInviteLinkUrl setJoinByGroupLinkState(@NonNull GroupManager.GroupLinkState state)
602 throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
603 {
604 AccessControl.AccessRequired access;
605
606 switch (state) {
607 case DISABLED : access = AccessControl.AccessRequired.UNSATISFIABLE; break;
608 case ENABLED : access = AccessControl.AccessRequired.ANY; break;
609 case ENABLED_WITH_APPROVAL: access = AccessControl.AccessRequired.ADMINISTRATOR; break;
610 default: throw new AssertionError();
611 }
612
613 GroupChange.Actions.Builder change = groupOperations.createChangeJoinByLinkRights(access);
614
615 if (state != GroupManager.GroupLinkState.DISABLED) {
616 DecryptedGroup group = groupDatabase.requireGroup(groupId).requireV2GroupProperties().getDecryptedGroup();
617
618 if (group.inviteLinkPassword.size() == 0) {
619 Log.d(TAG, "First time enabling group links for group and password empty, generating");
620 change = groupOperations.createModifyGroupLinkPasswordAndRightsChange(GroupLinkPassword.createNew().serialize(), access);
621 }
622 }
623
624 commitChangeWithConflictResolution(selfAci, change);
625
626 if (state != GroupManager.GroupLinkState.DISABLED) {
627 GroupTable.V2GroupProperties v2GroupProperties = groupDatabase.requireGroup(groupId).requireV2GroupProperties();
628 GroupMasterKey groupMasterKey = v2GroupProperties.getGroupMasterKey();
629 DecryptedGroup decryptedGroup = v2GroupProperties.getDecryptedGroup();
630
631 return GroupInviteLinkUrl.forGroup(groupMasterKey, decryptedGroup);
632 } else {
633 return null;
634 }
635 }
636
637 private @NonNull GroupManager.GroupActionResult commitChangeWithConflictResolution(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change)
638 throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
639 {
640 return commitChangeWithConflictResolution(authServiceId, change, false);
641 }
642
643 private @NonNull GroupManager.GroupActionResult commitChangeWithConflictResolution(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change, boolean allowWhenBlocked)
644 throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
645 {
646 return commitChangeWithConflictResolution(authServiceId, change, allowWhenBlocked, true);
647 }
648
649 private @NonNull GroupManager.GroupActionResult commitChangeWithConflictResolution(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change, boolean allowWhenBlocked, boolean sendToMembers)
650 throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
651 {
652 boolean refetchedAddMemberCredentials = false;
653 change.sourceServiceId(UuidUtil.toByteString(authServiceId.getRawUuid()));
654
655 for (int attempt = 0; attempt < 5; attempt++) {
656 try {
657 return commitChange(change, allowWhenBlocked, sendToMembers);
658 } catch (GroupPatchNotAcceptedException e) {
659 if (change.addMembers.size() > 0 && !refetchedAddMemberCredentials) {
660 refetchedAddMemberCredentials = true;
661 change = refetchAddMemberCredentials(change);
662 } else {
663 throw new GroupChangeFailedException(e);
664 }
665 } catch (ConflictException e) {
666 Log.w(TAG, "Invalid group patch or conflict", e);
667
668 change = resolveConflict(authServiceId, change);
669
670 if (GroupChangeUtil.changeIsEmpty(change.build())) {
671 Log.i(TAG, "Change is empty after conflict resolution");
672 Recipient groupRecipient = Recipient.externalGroupExact(groupId);
673 long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(groupRecipient);
674
675 return new GroupManager.GroupActionResult(groupRecipient, threadId, 0, Collections.emptyList());
676 }
677 }
678 }
679
680 throw new GroupChangeFailedException("Unable to apply change to group after conflicts");
681 }
682
683 private GroupChange.Actions.Builder resolveConflict(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change)
684 throws IOException, GroupNotAMemberException, GroupChangeFailedException
685 {
686 GroupsV2StateProcessor.GroupUpdateResult groupUpdateResult = groupsV2StateProcessor.forGroup(serviceIds, groupMasterKey)
687 .updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, System.currentTimeMillis(), null);
688
689 if (groupUpdateResult.getLatestServer() == null) {
690 Log.w(TAG, "Latest server state null.");
691 throw new GroupChangeFailedException();
692 }
693
694 if (groupUpdateResult.getGroupState() != GroupsV2StateProcessor.GroupState.GROUP_UPDATED) {
695 int serverRevision = groupUpdateResult.getLatestServer().revision;
696 int localRevision = groupDatabase.requireGroup(groupId).requireV2GroupProperties().getGroupRevision();
697 int revisionDelta = serverRevision - localRevision;
698 Log.w(TAG, String.format(Locale.US, "Server is ahead by %d revisions", revisionDelta));
699 throw new GroupChangeFailedException();
700 }
701
702 Log.w(TAG, "Group has been updated");
703 try {
704 GroupChange.Actions changeActions = change.build();
705
706 return GroupChangeUtil.resolveConflict(groupUpdateResult.getLatestServer(),
707 groupOperations.decryptChange(changeActions, authServiceId),
708 changeActions);
709 } catch (VerificationFailedException | InvalidGroupStateException ex) {
710 throw new GroupChangeFailedException(ex);
711 }
712 }
713
714 private GroupChange.Actions.Builder refetchAddMemberCredentials(@NonNull GroupChange.Actions.Builder change) {
715 try {
716 List<RecipientId> ids = groupOperations.decryptAddMembers(change.addMembers)
717 .stream()
718 .map(RecipientId::from)
719 .collect(java.util.stream.Collectors.toList());
720
721 for (RecipientId id : ids) {
722 ProfileUtil.updateExpiringProfileKeyCredential(Recipient.resolved(id));
723 }
724
725 List<GroupCandidate> groupCandidates = groupCandidateHelper.recipientIdsToCandidatesList(ids);
726
727 return groupOperations.replaceAddMembers(change, groupCandidates);
728 } catch (InvalidGroupStateException | InvalidInputException | VerificationFailedException | IOException e) {
729 Log.w(TAG, "Unable to refetch credentials for added members, failing change", e);
730 }
731
732 return change;
733 }
734
735 private GroupManager.GroupActionResult commitChange(@NonNull GroupChange.Actions.Builder change, boolean allowWhenBlocked, boolean sendToMembers)
736 throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException
737 {
738 final GroupRecord groupRecord = groupDatabase.requireGroup(groupId);
739 final GroupTable.V2GroupProperties v2GroupProperties = groupRecord.requireV2GroupProperties();
740 final int nextRevision = v2GroupProperties.getGroupRevision() + 1;
741 final GroupChange.Actions changeActions = change.revision(nextRevision).build();
742 final DecryptedGroupChange decryptedChange;
743 final DecryptedGroup decryptedGroupState;
744 final DecryptedGroup previousGroupState;
745
746 if (!allowWhenBlocked && Recipient.externalGroupExact(groupId).isBlocked()) {
747 throw new GroupChangeFailedException("Group is blocked.");
748 }
749
750 previousGroupState = v2GroupProperties.getDecryptedGroup();
751
752 GroupChange signedGroupChange = commitToServer(changeActions);
753 try {
754 //noinspection OptionalGetWithoutIsPresent
755 decryptedChange = groupOperations.decryptChange(signedGroupChange, false).get();
756 decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange);
757 } catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
758 Log.w(TAG, e);
759 throw new IOException(e);
760 }
761
762 groupDatabase.update(groupId, decryptedGroupState);
763
764 GroupMutation groupMutation = new GroupMutation(previousGroupState, decryptedChange, decryptedGroupState);
765 RecipientAndThread recipientAndThread = sendGroupUpdateHelper.sendGroupUpdate(groupMasterKey, groupMutation, signedGroupChange, sendToMembers);
766 int newMembersCount = decryptedChange.newMembers.size();
767 List<RecipientId> newPendingMembers = getPendingMemberRecipientIds(decryptedChange.newPendingMembers);
768
769 return new GroupManager.GroupActionResult(recipientAndThread.groupRecipient, recipientAndThread.threadId, newMembersCount, newPendingMembers);
770 }
771
772 private @NonNull GroupChange commitToServer(@NonNull GroupChange.Actions change)
773 throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException
774 {
775 try {
776 return groupsV2Api.patchGroup(change, authorization.getAuthorizationForToday(serviceIds, groupSecretParams), Optional.empty());
777 } catch (NotInGroupException e) {
778 Log.w(TAG, e);
779 throw new GroupNotAMemberException(e);
780 } catch (AuthorizationFailedException e) {
781 Log.w(TAG, e);
782 throw new GroupInsufficientRightsException(e);
783 } catch (VerificationFailedException e) {
784 Log.w(TAG, e);
785 throw new GroupChangeFailedException(e);
786 }
787 }
788 }
789
790 final class GroupUpdater extends LockOwner {
791
792 private final GroupMasterKey groupMasterKey;
793
794 GroupUpdater(@NonNull GroupMasterKey groupMasterKey, @NonNull Closeable lock) {
795 super(lock);
796
797 this.groupMasterKey = groupMasterKey;
798 }
799
800 @WorkerThread
801 GroupsV2StateProcessor.GroupUpdateResult updateLocalToServerRevision(int revision, long timestamp, @Nullable GroupSecretParams groupSecretParams, @Nullable byte[] signedGroupChange)
802 throws IOException, GroupNotAMemberException
803 {
804 return new GroupsV2StateProcessor(context).forGroup(serviceIds, groupMasterKey, groupSecretParams)
805 .updateLocalGroupToRevision(revision, timestamp, getDecryptedGroupChange(signedGroupChange));
806 }
807
808 @WorkerThread
809 GroupsV2StateProcessor.GroupUpdateResult updateLocalToServerRevision(int revision,
810 long timestamp,
811 @NonNull Optional<GroupRecord> localRecord,
812 @Nullable GroupSecretParams groupSecretParams,
813 @Nullable byte[] signedGroupChange,
814 @Nullable String serverGuid)
815 throws IOException, GroupNotAMemberException
816 {
817 return new GroupsV2StateProcessor(context).forGroup(serviceIds, groupMasterKey, groupSecretParams)
818 .updateLocalGroupToRevision(revision, timestamp, localRecord, getDecryptedGroupChange(signedGroupChange), serverGuid);
819 }
820
821 @WorkerThread
822 void forceSanityUpdateFromServer(long timestamp)
823 throws IOException, GroupNotAMemberException
824 {
825 new GroupsV2StateProcessor(context).forGroup(serviceIds, groupMasterKey)
826 .forceSanityUpdateFromServer(timestamp);
827 }
828
829 private DecryptedGroupChange getDecryptedGroupChange(@Nullable byte[] signedGroupChange) {
830 if (signedGroupChange != null && signedGroupChange.length > 0) {
831 GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(GroupSecretParams.deriveFromMasterKey(groupMasterKey));
832
833 try {
834 return groupOperations.decryptChange(GroupChange.ADAPTER.decode(signedGroupChange), true)
835 .orElse(null);
836 } catch (VerificationFailedException | InvalidGroupStateException | IOException e) {
837 Log.w(TAG, "Unable to verify supplied group change", e);
838 }
839 }
840
841 return null;
842 }
843 }
844
845 @WorkerThread
846 private @NonNull DecryptedGroup createGroupOnServer(@NonNull GroupSecretParams groupSecretParams,
847 @Nullable String name,
848 @Nullable byte[] avatar,
849 @NonNull Collection<RecipientId> members,
850 @NonNull Member.Role memberRole,
851 int disappearingMessageTimerSeconds)
852 throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception, GroupAlreadyExistsException
853 {
854 if (!GroupsV2CapabilityChecker.allAndSelfHaveServiceId(members)) {
855 throw new MembershipNotSuitableForV2Exception("At least one potential new member does not support GV2 capability or we don't have their UUID");
856 }
857
858 GroupCandidate self = groupCandidateHelper.recipientIdToCandidate(Recipient.self().getId());
859 Set<GroupCandidate> candidates = new HashSet<>(groupCandidateHelper.recipientIdsToCandidates(members));
860
861 if (SignalStore.internalValues().gv2ForceInvites()) {
862 Log.w(TAG, "Forcing GV2 invites due to internal setting");
863 candidates = GroupCandidate.withoutExpiringProfileKeyCredentials(candidates);
864 }
865
866 if (!self.hasValidProfileKeyCredential()) {
867 Log.w(TAG, "Cannot create a V2 group as self does not have a versioned profile");
868 throw new MembershipNotSuitableForV2Exception("Cannot create a V2 group as self does not have a versioned profile");
869 }
870
871 GroupsV2Operations.NewGroup newGroup = groupsV2Operations.createNewGroup(groupSecretParams,
872 name,
873 Optional.ofNullable(avatar),
874 self,
875 candidates,
876 memberRole,
877 disappearingMessageTimerSeconds);
878
879 try {
880 groupsV2Api.putNewGroup(newGroup, authorization.getAuthorizationForToday(serviceIds, groupSecretParams));
881
882 DecryptedGroup decryptedGroup = groupsV2Api.getGroup(groupSecretParams, ApplicationDependencies.getGroupsV2Authorization().getAuthorizationForToday(serviceIds, groupSecretParams));
883 if (decryptedGroup == null) {
884 throw new GroupChangeFailedException();
885 }
886
887 return decryptedGroup;
888 } catch (VerificationFailedException | InvalidGroupStateException e) {
889 throw new GroupChangeFailedException(e);
890 } catch (GroupExistsException e) {
891 throw new GroupAlreadyExistsException(e);
892 }
893 }
894
895 final class GroupJoiner extends LockOwner {
896 private final GroupId.V2 groupId;
897 private final GroupLinkPassword password;
898 private final GroupSecretParams groupSecretParams;
899 private final GroupsV2Operations.GroupOperations groupOperations;
900 private final GroupMasterKey groupMasterKey;
901
902 public GroupJoiner(@NonNull GroupMasterKey groupMasterKey,
903 @Nullable GroupLinkPassword password,
904 @NonNull Closeable lock)
905 {
906 super(lock);
907
908 this.groupId = GroupId.v2(groupMasterKey);
909 this.password = password;
910 this.groupMasterKey = groupMasterKey;
911 this.groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
912 this.groupOperations = groupsV2Operations.forGroup(groupSecretParams);
913 }
914
915 @WorkerThread
916 public GroupManager.GroupActionResult joinGroup(@NonNull DecryptedGroupJoinInfo joinInfo,
917 @Nullable byte[] avatar)
918 throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception, GroupLinkNotActiveException
919 {
920 boolean requestToJoin = joinInfo.addFromInviteLink == AccessControl.AccessRequired.ADMINISTRATOR;
921 boolean alreadyAMember = false;
922
923 if (requestToJoin) {
924 Log.i(TAG, "Requesting to join " + groupId);
925 } else {
926 Log.i(TAG, "Joining " + groupId);
927 }
928
929 GroupChange signedGroupChange = null;
930 DecryptedGroupChange decryptedChange = null;
931 try {
932 signedGroupChange = joinGroupOnServer(requestToJoin, joinInfo.revision);
933
934 if (requestToJoin) {
935 Log.i(TAG, String.format("Successfully requested to join %s on server", groupId));
936 } else {
937 Log.i(TAG, String.format("Successfully added self to %s on server", groupId));
938 }
939
940 decryptedChange = decryptChange(signedGroupChange);
941 } catch (GroupJoinAlreadyAMemberException e) {
942 Log.i(TAG, "Server reports that we are already a member of " + groupId);
943 alreadyAMember = true;
944 }
945
946 DecryptedGroup decryptedGroup = createPlaceholderGroup(joinInfo, requestToJoin);
947
948 Optional<GroupRecord> group = groupDatabase.getGroup(groupId);
949
950 if (group.isPresent()) {
951 Log.i(TAG, "Group already present locally");
952 if (decryptedChange != null) {
953 try {
954 groupsV2StateProcessor.forGroup(SignalStore.account().getServiceIds(), groupMasterKey)
955 .updateLocalGroupToRevision(decryptedChange.revision, System.currentTimeMillis(), decryptedChange);
956 } catch (GroupNotAMemberException e) {
957 Log.w(TAG, "Unable to apply join change to existing group", e);
958 }
959 }
960 } else {
961 GroupId.V2 groupId = groupDatabase.create(groupMasterKey, decryptedGroup);
962 if (groupId != null) {
963 Log.i(TAG, "Created local group with placeholder");
964 } else {
965 Log.i(TAG, "Create placeholder failed, group suddenly present locally, attempting to apply change");
966 if (decryptedChange != null) {
967 try {
968 groupsV2StateProcessor.forGroup(SignalStore.account().getServiceIds(), groupMasterKey)
969 .updateLocalGroupToRevision(decryptedChange.revision, System.currentTimeMillis(), decryptedChange);
970 } catch (GroupNotAMemberException e) {
971 Log.w(TAG, "Unable to apply join change to existing group", e);
972 }
973 }
974 }
975 }
976
977 RecipientId groupRecipientId = SignalDatabase.recipients().getOrInsertFromGroupId(groupId);
978 Recipient groupRecipient = Recipient.resolved(groupRecipientId);
979
980 AvatarHelper.setAvatar(context, groupRecipientId, avatar != null ? new ByteArrayInputStream(avatar) : null);
981 groupDatabase.onAvatarUpdated(groupId, avatar != null);
982 SignalDatabase.recipients().setProfileSharing(groupRecipientId, true);
983
984 if (alreadyAMember) {
985 Log.i(TAG, "Already a member of the group");
986
987 ThreadTable threadTable = SignalDatabase.threads();
988 long threadId = threadTable.getOrCreateValidThreadId(groupRecipient, -1);
989
990 return new GroupManager.GroupActionResult(groupRecipient,
991 threadId,
992 0,
993 Collections.emptyList());
994 } else if (requestToJoin) {
995 Log.i(TAG, "Requested to join, cannot send update");
996
997 RecipientAndThread recipientAndThread = sendGroupUpdateHelper.sendGroupUpdate(groupMasterKey, new GroupMutation(null, decryptedChange, decryptedGroup), signedGroupChange, false);
998
999 return new GroupManager.GroupActionResult(groupRecipient,
1000 recipientAndThread.threadId,
1001 0,
1002 Collections.emptyList());
1003 } else {
1004 Log.i(TAG, "Joined group on server, fetching group state and sending update");
1005
1006 return fetchGroupStateAndSendUpdate(groupRecipient, decryptedGroup, decryptedChange, signedGroupChange);
1007 }
1008 }
1009
1010 private GroupManager.GroupActionResult fetchGroupStateAndSendUpdate(@NonNull Recipient groupRecipient,
1011 @NonNull DecryptedGroup decryptedGroup,
1012 @NonNull DecryptedGroupChange decryptedChange,
1013 @NonNull GroupChange signedGroupChange)
1014 throws GroupChangeFailedException, IOException
1015 {
1016 try {
1017 new GroupsV2StateProcessor(context).forGroup(serviceIds, groupMasterKey)
1018 .updateLocalGroupToRevision(decryptedChange.revision,
1019 System.currentTimeMillis(),
1020 decryptedChange);
1021
1022 RecipientAndThread recipientAndThread = sendGroupUpdateHelper.sendGroupUpdate(groupMasterKey, new GroupMutation(null, decryptedChange, decryptedGroup), signedGroupChange);
1023
1024 return new GroupManager.GroupActionResult(groupRecipient,
1025 recipientAndThread.threadId,
1026 1,
1027 Collections.emptyList());
1028 } catch (GroupNotAMemberException e) {
1029 Log.w(TAG, "Despite adding self to group, server says we are not a member, scheduling refresh of group info " + groupId, e);
1030
1031 ApplicationDependencies.getJobManager()
1032 .add(new RequestGroupV2InfoJob(groupId));
1033
1034 throw new GroupChangeFailedException(e);
1035 } catch (IOException e) {
1036 Log.w(TAG, "Group data fetch failed, scheduling refresh of group info " + groupId, e);
1037
1038 ApplicationDependencies.getJobManager()
1039 .add(new RequestGroupV2InfoJob(groupId));
1040
1041 throw e;
1042 }
1043 }
1044
1045 private @NonNull DecryptedGroupChange decryptChange(@NonNull GroupChange signedGroupChange)
1046 throws GroupChangeFailedException
1047 {
1048 try {
1049 //noinspection OptionalGetWithoutIsPresent
1050 return groupOperations.decryptChange(signedGroupChange, false).get();
1051 } catch (VerificationFailedException | InvalidGroupStateException | IOException e) {
1052 Log.w(TAG, e);
1053 throw new GroupChangeFailedException(e);
1054 }
1055 }
1056
1057 /**
1058 * Creates a local group from what we know before joining.
1059 * <p>
1060 * Creates as a {@link GroupsV2StateProcessor#PLACEHOLDER_REVISION} so that we know not do do a
1061 * full diff against this group once we learn more about this group as that would create a large
1062 * update message.
1063 */
1064 private DecryptedGroup createPlaceholderGroup(@NonNull DecryptedGroupJoinInfo joinInfo, boolean requestToJoin) {
1065 DecryptedGroup.Builder group = new DecryptedGroup.Builder()
1066 .title(joinInfo.title)
1067 .avatar(joinInfo.avatar)
1068 .revision(GroupsV2StateProcessor.PLACEHOLDER_REVISION);
1069
1070 Recipient self = Recipient.self();
1071 ByteString selfAciBytes = selfAci.toByteString();
1072 ByteString profileKey = ByteString.of(Objects.requireNonNull(self.getProfileKey()));
1073
1074 if (requestToJoin) {
1075 group.requestingMembers(Collections.singletonList(new DecryptedRequestingMember.Builder()
1076 .aciBytes(selfAciBytes)
1077 .profileKey(profileKey)
1078 .build()));
1079 } else {
1080 group.members(Collections.singletonList(new DecryptedMember.Builder()
1081 .aciBytes(selfAciBytes)
1082 .profileKey(profileKey)
1083 .build()));
1084 }
1085
1086 return group.build();
1087 }
1088
1089 private @NonNull GroupChange joinGroupOnServer(boolean requestToJoin, int currentRevision)
1090 throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception, GroupLinkNotActiveException, GroupJoinAlreadyAMemberException
1091 {
1092 if (!GroupsV2CapabilityChecker.allAndSelfHaveServiceId(Collections.singleton(Recipient.self().getId()))) {
1093 throw new MembershipNotSuitableForV2Exception("Self does not support GV2 or UUID capabilities");
1094 }
1095
1096 GroupCandidate self = groupCandidateHelper.recipientIdToCandidate(Recipient.self().getId());
1097
1098 if (!self.hasValidProfileKeyCredential()) {
1099 throw new MembershipNotSuitableForV2Exception("No profile key credential for self");
1100 }
1101
1102 ExpiringProfileKeyCredential expiringProfileKeyCredential = self.requireExpiringProfileKeyCredential();
1103
1104 GroupChange.Actions.Builder change = requestToJoin ? groupOperations.createGroupJoinRequest(expiringProfileKeyCredential)
1105 : groupOperations.createGroupJoinDirect(expiringProfileKeyCredential);
1106
1107 change.sourceServiceId(selfAci.toByteString());
1108
1109 return commitJoinChangeWithConflictResolution(currentRevision, change);
1110 }
1111
1112 private @NonNull GroupChange commitJoinChangeWithConflictResolution(int currentRevision, @NonNull GroupChange.Actions.Builder change)
1113 throws GroupChangeFailedException, IOException, GroupLinkNotActiveException, GroupJoinAlreadyAMemberException
1114 {
1115 for (int attempt = 0; attempt < 5; attempt++) {
1116 try {
1117 GroupChange.Actions changeActions = change.revision(currentRevision + 1)
1118 .build();
1119
1120 Log.i(TAG, "Trying to join group at V" + changeActions.revision);
1121 GroupChange signedGroupChange = commitJoinToServer(changeActions);
1122
1123 Log.i(TAG, "Successfully joined group at V" + changeActions.revision);
1124 return signedGroupChange;
1125 } catch (GroupPatchNotAcceptedException e) {
1126 Log.w(TAG, "Patch not accepted", e);
1127
1128 try {
1129 if (alreadyPendingAdminApproval() || testGroupMembership()) {
1130 throw new GroupJoinAlreadyAMemberException(e);
1131 } else {
1132 throw new GroupChangeFailedException(e);
1133 }
1134 } catch (VerificationFailedException | InvalidGroupStateException ex) {
1135 throw new GroupChangeFailedException(ex);
1136 }
1137 } catch (ConflictException e) {
1138 Log.w(TAG, "Revision conflict", e);
1139
1140 currentRevision = getCurrentGroupRevisionFromServer();
1141 }
1142 }
1143
1144 throw new GroupChangeFailedException("Unable to join group after conflicts");
1145 }
1146
1147 private @NonNull GroupChange commitJoinToServer(@NonNull GroupChange.Actions change)
1148 throws GroupChangeFailedException, IOException, GroupLinkNotActiveException
1149 {
1150 try {
1151 return groupsV2Api.patchGroup(change, authorization.getAuthorizationForToday(serviceIds, groupSecretParams), Optional.ofNullable(password).map(GroupLinkPassword::serialize));
1152 } catch (NotInGroupException | VerificationFailedException e) {
1153 Log.w(TAG, e);
1154 throw new GroupChangeFailedException(e);
1155 } catch (AuthorizationFailedException e) {
1156 Log.w(TAG, e);
1157 throw new GroupLinkNotActiveException(e, Optional.empty());
1158 }
1159 }
1160
1161 private int getCurrentGroupRevisionFromServer()
1162 throws IOException, GroupLinkNotActiveException, GroupChangeFailedException
1163 {
1164 try {
1165 int currentRevision = getGroupJoinInfoFromServer(groupMasterKey, password).revision;
1166
1167 Log.i(TAG, "Server now on V" + currentRevision);
1168
1169 return currentRevision;
1170 } catch (VerificationFailedException ex) {
1171 throw new GroupChangeFailedException(ex);
1172 }
1173 }
1174
1175 private boolean alreadyPendingAdminApproval()
1176 throws IOException, GroupLinkNotActiveException, GroupChangeFailedException
1177 {
1178 try {
1179 boolean pendingAdminApproval = getGroupJoinInfoFromServer(groupMasterKey, password).pendingAdminApproval;
1180
1181 if (pendingAdminApproval) {
1182 Log.i(TAG, "User is already pending admin approval");
1183 }
1184
1185 return pendingAdminApproval;
1186 } catch (VerificationFailedException ex) {
1187 throw new GroupChangeFailedException(ex);
1188 }
1189 }
1190
1191 private boolean testGroupMembership()
1192 throws IOException, VerificationFailedException, InvalidGroupStateException
1193 {
1194 try {
1195 groupsV2Api.getGroup(groupSecretParams, authorization.getAuthorizationForToday(serviceIds, groupSecretParams));
1196 return true;
1197 } catch (NotInGroupException ex) {
1198 return false;
1199 }
1200 }
1201
1202 @WorkerThread
1203 void cancelJoinRequest()
1204 throws GroupChangeFailedException, IOException
1205 {
1206 Set<ACI> uuids = Collections.singleton(selfAci);
1207
1208 GroupChange signedGroupChange;
1209 try {
1210 signedGroupChange = commitCancelChangeWithConflictResolution(groupOperations.createRefuseGroupJoinRequest(uuids, false, Collections.emptyList()));
1211 } catch (GroupLinkNotActiveException e) {
1212 Log.d(TAG, "Unexpected unable to leave group due to group link off");
1213 throw new GroupChangeFailedException(e);
1214 }
1215
1216 DecryptedGroup decryptedGroup = groupDatabase.requireGroup(groupId).requireV2GroupProperties().getDecryptedGroup();
1217
1218 try {
1219 //noinspection OptionalGetWithoutIsPresent
1220 DecryptedGroupChange decryptedChange = groupOperations.decryptChange(signedGroupChange, false).get();
1221 DecryptedGroup newGroup = DecryptedGroupUtil.applyWithoutRevisionCheck(decryptedGroup, decryptedChange);
1222
1223 groupDatabase.update(groupId, resetRevision(newGroup, decryptedGroup.revision));
1224
1225 sendGroupUpdateHelper.sendGroupUpdate(groupMasterKey, new GroupMutation(decryptedGroup, decryptedChange, newGroup), signedGroupChange, false);
1226 } catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
1227 throw new GroupChangeFailedException(e);
1228 }
1229 }
1230
1231 private DecryptedGroup resetRevision(DecryptedGroup newGroup, int revision) {
1232 return newGroup.newBuilder()
1233 .revision(revision)
1234 .build();
1235 }
1236
1237 private @NonNull GroupChange commitCancelChangeWithConflictResolution(@NonNull GroupChange.Actions.Builder change)
1238 throws GroupChangeFailedException, IOException, GroupLinkNotActiveException
1239 {
1240 int currentRevision = getCurrentGroupRevisionFromServer();
1241
1242 for (int attempt = 0; attempt < 5; attempt++) {
1243 try {
1244 GroupChange.Actions changeActions = change.revision(currentRevision + 1)
1245 .build();
1246
1247 Log.i(TAG, "Trying to cancel request group at V" + changeActions.revision);
1248 GroupChange signedGroupChange = commitJoinToServer(changeActions);
1249
1250 Log.i(TAG, "Successfully cancelled group join at V" + changeActions.revision);
1251 return signedGroupChange;
1252 } catch (GroupPatchNotAcceptedException e) {
1253 throw new GroupChangeFailedException(e);
1254 } catch (ConflictException e) {
1255 Log.w(TAG, "Revision conflict", e);
1256
1257 currentRevision = getCurrentGroupRevisionFromServer();
1258 }
1259 }
1260
1261 throw new GroupChangeFailedException("Unable to cancel group join request after conflicts");
1262 }
1263}
1264
1265 private abstract static class LockOwner implements Closeable {
1266 final Closeable lock;
1267
1268 LockOwner(@NonNull Closeable lock) {
1269 this.lock = lock;
1270 }
1271
1272 @Override
1273 public void close() throws IOException {
1274 lock.close();
1275 }
1276 }
1277
1278 @VisibleForTesting
1279 static class SendGroupUpdateHelper {
1280
1281 private final Context context;
1282
1283 SendGroupUpdateHelper(Context context) {
1284 this.context = context;
1285 }
1286
1287 @NonNull RecipientAndThread sendGroupUpdate(@NonNull GroupMasterKey masterKey,
1288 @NonNull GroupMutation groupMutation,
1289 @Nullable GroupChange signedGroupChange)
1290 {
1291 return sendGroupUpdate(masterKey, groupMutation, signedGroupChange, true);
1292 }
1293
1294 @NonNull RecipientAndThread sendGroupUpdate(@NonNull GroupMasterKey masterKey,
1295 @NonNull GroupMutation groupMutation,
1296 @Nullable GroupChange signedGroupChange,
1297 boolean sendToMembers)
1298 {
1299 GroupId.V2 groupId = GroupId.v2(masterKey);
1300 Recipient groupRecipient = Recipient.externalGroupExact(groupId);
1301 GV2UpdateDescription updateDescription = GroupProtoUtil.createOutgoingGroupV2UpdateDescription(masterKey, groupMutation, signedGroupChange);
1302 OutgoingMessage outgoingMessage = OutgoingMessage.groupUpdateMessage(groupRecipient, updateDescription, System.currentTimeMillis());
1303
1304
1305 DecryptedGroupChange plainGroupChange = groupMutation.getGroupChange();
1306
1307 if (plainGroupChange != null && DecryptedGroupUtil.changeIsSilent(plainGroupChange)) {
1308 if (sendToMembers) {
1309 ApplicationDependencies.getJobManager().add(PushGroupSilentUpdateSendJob.create(context, groupId, groupMutation.getNewGroupState(), outgoingMessage));
1310 }
1311
1312 return new RecipientAndThread(groupRecipient, -1);
1313 } else {
1314 //noinspection IfStatementWithIdenticalBranches
1315 if (sendToMembers) {
1316 long threadId = MessageSender.send(context, outgoingMessage, -1, MessageSender.SendType.SIGNAL, null, null);
1317 return new RecipientAndThread(groupRecipient, threadId);
1318 } else {
1319 long threadId = SignalDatabase.threads().getOrCreateValidThreadId(outgoingMessage.getThreadRecipient(), -1, outgoingMessage.getDistributionType());
1320 try {
1321 long messageId = SignalDatabase.messages().insertMessageOutbox(outgoingMessage, threadId, false, null);
1322 SignalDatabase.messages().markAsSent(messageId, true);
1323 SignalDatabase.threads().update(threadId, true);
1324 } catch (MmsException e) {
1325 throw new AssertionError(e);
1326 }
1327 return new RecipientAndThread(groupRecipient, threadId);
1328 }
1329 }
1330 }
1331 }
1332
1333 private static @NonNull List<RecipientId> getPendingMemberRecipientIds(@NonNull List<DecryptedPendingMember> newPendingMembersList) {
1334 return Stream.of(DecryptedGroupUtil.pendingToServiceIdList(newPendingMembersList))
1335 .map(serviceId -> RecipientId.from(serviceId))
1336 .toList();
1337 }
1338
1339 private static @NonNull AccessControl.AccessRequired rightsToAccessControl(@NonNull GroupAccessControl rights) {
1340 switch (rights){
1341 case ALL_MEMBERS:
1342 return AccessControl.AccessRequired.MEMBER;
1343 case ONLY_ADMINS:
1344 return AccessControl.AccessRequired.ADMINISTRATOR;
1345 case NO_ONE:
1346 return AccessControl.AccessRequired.UNSATISFIABLE;
1347 default:
1348 throw new AssertionError();
1349 }
1350 }
1351
1352 static class RecipientAndThread {
1353 private final Recipient groupRecipient;
1354 private final long threadId;
1355
1356 RecipientAndThread(@NonNull Recipient groupRecipient, long threadId) {
1357 this.groupRecipient = groupRecipient;
1358 this.threadId = threadId;
1359 }
1360 }
1361}