That fuck shit the fascists are using
at master 1361 lines 68 kB view raw
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}