1 package org
.asamk
.signal
.manager
.helper
;
3 import org
.asamk
.signal
.manager
.api
.GroupLinkState
;
4 import org
.asamk
.signal
.manager
.api
.GroupPermission
;
5 import org
.asamk
.signal
.manager
.api
.NotAGroupMemberException
;
6 import org
.asamk
.signal
.manager
.api
.Pair
;
7 import org
.asamk
.signal
.manager
.groups
.GroupLinkPassword
;
8 import org
.asamk
.signal
.manager
.groups
.GroupUtils
;
9 import org
.asamk
.signal
.manager
.internal
.SignalDependencies
;
10 import org
.asamk
.signal
.manager
.storage
.groups
.GroupInfoV2
;
11 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientId
;
12 import org
.asamk
.signal
.manager
.util
.Utils
;
13 import org
.signal
.libsignal
.zkgroup
.InvalidInputException
;
14 import org
.signal
.libsignal
.zkgroup
.VerificationFailedException
;
15 import org
.signal
.libsignal
.zkgroup
.auth
.AuthCredentialWithPniResponse
;
16 import org
.signal
.libsignal
.zkgroup
.groups
.GroupMasterKey
;
17 import org
.signal
.libsignal
.zkgroup
.groups
.GroupSecretParams
;
18 import org
.signal
.libsignal
.zkgroup
.groups
.UuidCiphertext
;
19 import org
.signal
.libsignal
.zkgroup
.profiles
.ProfileKey
;
20 import org
.signal
.storageservice
.protos
.groups
.AccessControl
;
21 import org
.signal
.storageservice
.protos
.groups
.GroupChange
;
22 import org
.signal
.storageservice
.protos
.groups
.GroupChangeResponse
;
23 import org
.signal
.storageservice
.protos
.groups
.Member
;
24 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroup
;
25 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroupChange
;
26 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroupJoinInfo
;
27 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedMember
;
28 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedPendingMember
;
29 import org
.slf4j
.Logger
;
30 import org
.slf4j
.LoggerFactory
;
31 import org
.whispersystems
.signalservice
.api
.groupsv2
.DecryptChangeVerificationMode
;
32 import org
.whispersystems
.signalservice
.api
.groupsv2
.DecryptedGroupResponse
;
33 import org
.whispersystems
.signalservice
.api
.groupsv2
.DecryptedGroupUtil
;
34 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupCandidate
;
35 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupHistoryPage
;
36 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupLinkNotActiveException
;
37 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2AuthorizationString
;
38 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2Operations
;
39 import org
.whispersystems
.signalservice
.api
.groupsv2
.InvalidGroupStateException
;
40 import org
.whispersystems
.signalservice
.api
.groupsv2
.NotAbleToApplyGroupV2ChangeException
;
41 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
;
42 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
.ACI
;
43 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
.PNI
;
44 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
45 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.NonSuccessfulResponseCodeException
;
46 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
47 import org
.whispersystems
.signalservice
.internal
.push
.exceptions
.NotInGroupException
;
49 import java
.io
.IOException
;
50 import java
.util
.ArrayList
;
51 import java
.util
.Arrays
;
52 import java
.util
.List
;
54 import java
.util
.Optional
;
56 import java
.util
.UUID
;
57 import java
.util
.concurrent
.TimeUnit
;
58 import java
.util
.function
.Function
;
59 import java
.util
.stream
.Collectors
;
60 import java
.util
.stream
.Stream
;
62 import okio
.ByteString
;
66 private static final Logger logger
= LoggerFactory
.getLogger(GroupV2Helper
.class);
68 private final SignalDependencies dependencies
;
69 private final Context context
;
71 private Map
<Long
, AuthCredentialWithPniResponse
> groupApiCredentials
;
73 GroupV2Helper(final Context context
) {
74 this.dependencies
= context
.getDependencies();
75 this.context
= context
;
78 void clearAuthCredentialCache() {
79 groupApiCredentials
= null;
82 DecryptedGroupResponse
getDecryptedGroup(final GroupSecretParams groupSecretParams
) throws NotAGroupMemberException
{
84 final var groupsV2AuthorizationString
= getGroupAuthForToday(groupSecretParams
);
85 return dependencies
.getGroupsV2Api().getGroup(groupSecretParams
, groupsV2AuthorizationString
);
86 } catch (NonSuccessfulResponseCodeException e
) {
88 throw new NotAGroupMemberException(GroupUtils
.getGroupIdV2(groupSecretParams
), null);
90 logger
.warn("Failed to retrieve Group V2 info, ignoring: {}", e
.getMessage());
92 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException
| InvalidInputException e
) {
93 logger
.warn("Failed to retrieve Group V2 info, ignoring: {}", e
.getMessage());
98 DecryptedGroupJoinInfo
getDecryptedGroupJoinInfo(
99 GroupMasterKey groupMasterKey
,
100 GroupLinkPassword password
101 ) throws IOException
, GroupLinkNotActiveException
{
102 var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
104 return dependencies
.getGroupsV2Api()
105 .getGroupJoinInfo(groupSecretParams
,
106 Optional
.ofNullable(password
).map(GroupLinkPassword
::serialize
),
107 getGroupAuthForToday(groupSecretParams
));
110 GroupHistoryPage
getDecryptedGroupHistoryPage(
111 final GroupSecretParams groupSecretParams
,
113 long sendEndorsementsExpirationMs
114 ) throws NotAGroupMemberException
{
116 final var groupsV2AuthorizationString
= getGroupAuthForToday(groupSecretParams
);
117 return dependencies
.getGroupsV2Api()
118 .getGroupHistoryPage(groupSecretParams
,
120 groupsV2AuthorizationString
,
122 sendEndorsementsExpirationMs
);
123 } catch (NotInGroupException e
) {
124 throw new NotAGroupMemberException(GroupUtils
.getGroupIdV2(groupSecretParams
), null);
125 } catch (NonSuccessfulResponseCodeException e
) {
127 throw new NotAGroupMemberException(GroupUtils
.getGroupIdV2(groupSecretParams
), null);
129 logger
.warn("Failed to retrieve Group V2 history, ignoring: {}", e
.getMessage());
131 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException
| InvalidInputException e
) {
132 logger
.warn("Failed to retrieve Group V2 history, ignoring: {}", e
.getMessage());
137 int findRevisionWeWereAdded(DecryptedGroup partialDecryptedGroup
) {
138 ByteString aciBytes
= getSelfAci().toByteString();
139 ByteString pniBytes
= getSelfPni().toByteString();
140 for (DecryptedMember decryptedMember
: partialDecryptedGroup
.members
) {
141 if (decryptedMember
.aciBytes
.equals(aciBytes
) || decryptedMember
.pniBytes
.equals(pniBytes
)) {
142 return decryptedMember
.joinedAtRevision
;
145 return partialDecryptedGroup
.revision
;
148 Pair
<GroupInfoV2
, DecryptedGroupResponse
> createGroup(String name
, Set
<RecipientId
> members
, byte[] avatarFile
) {
149 final var newGroup
= buildNewGroup(name
, members
, avatarFile
);
150 if (newGroup
== null) {
154 final var groupSecretParams
= newGroup
.getGroupSecretParams();
156 final GroupsV2AuthorizationString groupAuthForToday
;
157 final DecryptedGroupResponse response
;
159 groupAuthForToday
= getGroupAuthForToday(groupSecretParams
);
160 dependencies
.getGroupsV2Api().putNewGroup(newGroup
, groupAuthForToday
);
161 response
= dependencies
.getGroupsV2Api().getGroup(groupSecretParams
, groupAuthForToday
);
162 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException
| InvalidInputException e
) {
163 logger
.warn("Failed to create V2 group: {}", e
.getMessage());
166 if (response
== null) {
167 logger
.warn("Failed to create V2 group, unknown error!");
171 final var groupId
= GroupUtils
.getGroupIdV2(groupSecretParams
);
172 final var masterKey
= groupSecretParams
.getMasterKey();
173 var g
= new GroupInfoV2(groupId
, masterKey
, context
.getAccount().getRecipientResolver());
175 return new Pair
<>(g
, response
);
178 private GroupsV2Operations
.NewGroup
buildNewGroup(String name
, Set
<RecipientId
> members
, byte[] avatar
) {
179 final var profileKeyCredential
= context
.getProfileHelper()
180 .getExpiringProfileKeyCredential(context
.getAccount().getSelfRecipientId());
181 if (profileKeyCredential
== null) {
182 logger
.warn("Cannot create a V2 group as self does not have a versioned profile");
186 final var self
= new GroupCandidate(getSelfAci(), Optional
.of(profileKeyCredential
));
187 final var memberList
= new ArrayList
<>(members
);
188 final var credentials
= context
.getProfileHelper().getExpiringProfileKeyCredential(memberList
).stream();
189 final var uuids
= memberList
.stream()
190 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId());
191 var candidates
= Utils
.zip(uuids
,
193 (uuid
, credential
) -> new GroupCandidate(uuid
, Optional
.ofNullable(credential
)))
194 .collect(Collectors
.toSet());
196 final var groupSecretParams
= GroupSecretParams
.generate();
197 return dependencies
.getGroupsV2Operations()
198 .createNewGroup(groupSecretParams
,
200 Optional
.ofNullable(avatar
),
207 Pair
<DecryptedGroup
, GroupChangeResponse
> updateGroup(
208 GroupInfoV2 groupInfoV2
,
212 ) throws IOException
{
213 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
214 var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
216 var change
= name
!= null ? groupOperations
.createModifyGroupTitle(name
) : new GroupChange
.Actions
.Builder();
218 if (description
!= null) {
219 change
.modifyDescription(groupOperations
.createModifyGroupDescriptionAction(description
).build());
222 if (avatarFile
!= null) {
223 var avatarCdnKey
= dependencies
.getGroupsV2Api()
224 .uploadAvatar(avatarFile
, groupSecretParams
, getGroupAuthForToday(groupSecretParams
));
225 change
.modifyAvatar(new GroupChange
.Actions
.ModifyAvatarAction
.Builder().avatar(avatarCdnKey
).build());
228 change
.sourceServiceId(getSelfAci().toByteString());
230 return commitChange(groupInfoV2
, change
);
233 Pair
<DecryptedGroup
, GroupChangeResponse
> addMembers(
234 GroupInfoV2 groupInfoV2
,
235 Set
<RecipientId
> newMembers
236 ) throws IOException
{
237 GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
239 final var memberList
= new ArrayList
<>(newMembers
);
240 final var credentials
= context
.getProfileHelper().getExpiringProfileKeyCredential(memberList
).stream();
241 final var uuids
= memberList
.stream()
242 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId());
243 var candidates
= Utils
.zip(uuids
,
245 (uuid
, credential
) -> new GroupCandidate(uuid
, Optional
.ofNullable(credential
)))
246 .collect(Collectors
.toSet());
247 final var bannedUuids
= groupInfoV2
.getBannedMembers()
249 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId())
250 .collect(Collectors
.toSet());
252 final var aci
= getSelfAci();
253 final var change
= groupOperations
.createModifyGroupMembershipChange(candidates
, bannedUuids
, aci
);
255 change
.sourceServiceId(getSelfAci().toByteString());
257 return commitChange(groupInfoV2
, change
);
260 Pair
<DecryptedGroup
, GroupChangeResponse
> leaveGroup(
261 GroupInfoV2 groupInfoV2
,
262 Set
<RecipientId
> membersToMakeAdmin
263 ) throws IOException
{
264 var pendingMembersList
= groupInfoV2
.getGroup().pendingMembers
;
265 final var selfAci
= getSelfAci();
266 var selfPendingMember
= DecryptedGroupUtil
.findPendingByServiceId(pendingMembersList
, selfAci
);
268 if (selfPendingMember
.isPresent()) {
269 return revokeInvites(groupInfoV2
, Set
.of(selfPendingMember
.get()));
272 final var adminUuids
= membersToMakeAdmin
.stream()
273 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
274 .map(SignalServiceAddress
::getServiceId
)
275 .map(ServiceId
::getRawUuid
)
277 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
278 return commitChange(groupInfoV2
, groupOperations
.createLeaveAndPromoteMembersToAdmin(selfAci
, adminUuids
));
281 Pair
<DecryptedGroup
, GroupChangeResponse
> removeMembers(
282 GroupInfoV2 groupInfoV2
,
283 Set
<RecipientId
> members
284 ) throws IOException
{
285 final var memberUuids
= members
.stream()
286 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
287 .map(SignalServiceAddress
::getServiceId
)
288 .filter(m
-> m
instanceof ACI
)
290 .collect(Collectors
.toSet());
291 return ejectMembers(groupInfoV2
, memberUuids
);
294 Pair
<DecryptedGroup
, GroupChangeResponse
> approveJoinRequestMembers(
295 GroupInfoV2 groupInfoV2
,
296 Set
<RecipientId
> members
297 ) throws IOException
{
298 final var memberUuids
= members
.stream()
299 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
300 .map(SignalServiceAddress
::getServiceId
)
301 .map(ServiceId
::getRawUuid
)
302 .collect(Collectors
.toSet());
303 return approveJoinRequest(groupInfoV2
, memberUuids
);
306 Pair
<DecryptedGroup
, GroupChangeResponse
> refuseJoinRequestMembers(
307 GroupInfoV2 groupInfoV2
,
308 Set
<RecipientId
> members
309 ) throws IOException
{
310 final var memberUuids
= members
.stream()
311 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
312 .map(SignalServiceAddress
::getServiceId
)
313 .collect(Collectors
.toSet());
314 return refuseJoinRequest(groupInfoV2
, memberUuids
);
317 Pair
<DecryptedGroup
, GroupChangeResponse
> revokeInvitedMembers(
318 GroupInfoV2 groupInfoV2
,
319 Set
<RecipientId
> members
320 ) throws IOException
{
321 var pendingMembersList
= groupInfoV2
.getGroup().pendingMembers
;
322 final var memberUuids
= members
.stream()
323 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
324 .map(SignalServiceAddress
::getServiceId
)
325 .map(uuid
-> DecryptedGroupUtil
.findPendingByServiceId(pendingMembersList
, uuid
))
326 .filter(Optional
::isPresent
)
328 .collect(Collectors
.toSet());
329 return revokeInvites(groupInfoV2
, memberUuids
);
332 Pair
<DecryptedGroup
, GroupChangeResponse
> banMembers(
333 GroupInfoV2 groupInfoV2
,
334 Set
<RecipientId
> block
335 ) throws IOException
{
336 GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
338 final var serviceIds
= block
.stream()
339 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId())
340 .collect(Collectors
.toSet());
342 final var change
= groupOperations
.createBanServiceIdsChange(serviceIds
,
344 groupInfoV2
.getGroup().bannedMembers
);
346 change
.sourceServiceId(getSelfAci().toByteString());
348 return commitChange(groupInfoV2
, change
);
351 Pair
<DecryptedGroup
, GroupChangeResponse
> unbanMembers(
352 GroupInfoV2 groupInfoV2
,
353 Set
<RecipientId
> block
354 ) throws IOException
{
355 GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
357 final var serviceIds
= block
.stream()
358 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId())
359 .collect(Collectors
.toSet());
361 final var change
= groupOperations
.createUnbanServiceIdsChange(serviceIds
);
363 change
.sourceServiceId(getSelfAci().toByteString());
365 return commitChange(groupInfoV2
, change
);
368 Pair
<DecryptedGroup
, GroupChangeResponse
> resetGroupLinkPassword(GroupInfoV2 groupInfoV2
) throws IOException
{
369 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
370 final var newGroupLinkPassword
= GroupLinkPassword
.createNew().serialize();
371 final var change
= groupOperations
.createModifyGroupLinkPasswordChange(newGroupLinkPassword
);
372 return commitChange(groupInfoV2
, change
);
375 Pair
<DecryptedGroup
, GroupChangeResponse
> setGroupLinkState(
376 GroupInfoV2 groupInfoV2
,
378 ) throws IOException
{
379 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
381 final var accessRequired
= toAccessControl(state
);
382 final var requiresNewPassword
= state
!= GroupLinkState
.DISABLED
383 && groupInfoV2
.getGroup().inviteLinkPassword
.toByteArray().length
== 0;
385 final var change
= requiresNewPassword ? groupOperations
.createModifyGroupLinkPasswordAndRightsChange(
386 GroupLinkPassword
.createNew().serialize(),
387 accessRequired
) : groupOperations
.createChangeJoinByLinkRights(accessRequired
);
388 return commitChange(groupInfoV2
, change
);
391 Pair
<DecryptedGroup
, GroupChangeResponse
> setEditDetailsPermission(
392 GroupInfoV2 groupInfoV2
,
393 GroupPermission permission
394 ) throws IOException
{
395 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
397 final var accessRequired
= toAccessControl(permission
);
398 final var change
= groupOperations
.createChangeAttributesRights(accessRequired
);
399 return commitChange(groupInfoV2
, change
);
402 Pair
<DecryptedGroup
, GroupChangeResponse
> setAddMemberPermission(
403 GroupInfoV2 groupInfoV2
,
404 GroupPermission permission
405 ) throws IOException
{
406 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
408 final var accessRequired
= toAccessControl(permission
);
409 final var change
= groupOperations
.createChangeMembershipRights(accessRequired
);
410 return commitChange(groupInfoV2
, change
);
413 Pair
<DecryptedGroup
, GroupChangeResponse
> updateSelfProfileKey(GroupInfoV2 groupInfoV2
) throws IOException
{
414 Optional
<DecryptedMember
> selfInGroup
= groupInfoV2
.getGroup() == null
416 : DecryptedGroupUtil
.findMemberByAci(groupInfoV2
.getGroup().members
, getSelfAci());
417 if (selfInGroup
.isEmpty()) {
418 logger
.trace("Not updating group, self not in group " + groupInfoV2
.getGroupId().toBase64());
422 final var profileKey
= context
.getAccount().getProfileKey();
423 if (Arrays
.equals(profileKey
.serialize(), selfInGroup
.get().profileKey
.toByteArray())) {
424 logger
.trace("Not updating group, own Profile Key is already up to date in group "
425 + groupInfoV2
.getGroupId().toBase64());
428 logger
.debug("Updating own profile key in group " + groupInfoV2
.getGroupId().toBase64());
430 final var selfRecipientId
= context
.getAccount().getSelfRecipientId();
431 final var profileKeyCredential
= context
.getProfileHelper().getExpiringProfileKeyCredential(selfRecipientId
);
432 if (profileKeyCredential
== null) {
433 logger
.trace("Cannot update profile key as self does not have a versioned profile");
437 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
438 final var change
= groupOperations
.createUpdateProfileKeyCredentialChange(profileKeyCredential
);
439 change
.sourceServiceId(getSelfAci().toByteString());
440 return commitChange(groupInfoV2
, change
);
443 GroupChangeResponse
joinGroup(
444 GroupMasterKey groupMasterKey
,
445 GroupLinkPassword groupLinkPassword
,
446 DecryptedGroupJoinInfo decryptedGroupJoinInfo
447 ) throws IOException
{
448 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
449 final var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
451 final var selfRecipientId
= context
.getAccount().getSelfRecipientId();
452 final var profileKeyCredential
= context
.getProfileHelper().getExpiringProfileKeyCredential(selfRecipientId
);
453 if (profileKeyCredential
== null) {
454 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
457 var requestToJoin
= decryptedGroupJoinInfo
.addFromInviteLink
== AccessControl
.AccessRequired
.ADMINISTRATOR
;
458 var change
= requestToJoin
459 ? groupOperations
.createGroupJoinRequest(profileKeyCredential
)
460 : groupOperations
.createGroupJoinDirect(profileKeyCredential
);
462 change
.sourceServiceId(context
.getRecipientHelper()
463 .resolveSignalServiceAddress(selfRecipientId
)
467 return commitChange(groupSecretParams
, decryptedGroupJoinInfo
.revision
, change
, groupLinkPassword
);
470 Pair
<DecryptedGroup
, GroupChangeResponse
> acceptInvite(GroupInfoV2 groupInfoV2
) throws IOException
{
471 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
473 final var selfRecipientId
= context
.getAccount().getSelfRecipientId();
474 final var profileKeyCredential
= context
.getProfileHelper().getExpiringProfileKeyCredential(selfRecipientId
);
475 if (profileKeyCredential
== null) {
476 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
479 final var change
= groupOperations
.createAcceptInviteChange(profileKeyCredential
);
481 final var aci
= context
.getRecipientHelper().resolveSignalServiceAddress(selfRecipientId
).getServiceId();
482 change
.sourceServiceId(aci
.toByteString());
484 return commitChange(groupInfoV2
, change
);
487 Pair
<DecryptedGroup
, GroupChangeResponse
> setMemberAdmin(
488 GroupInfoV2 groupInfoV2
,
489 RecipientId recipientId
,
491 ) throws IOException
{
492 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
493 final var address
= context
.getRecipientHelper().resolveSignalServiceAddress(recipientId
);
494 final var newRole
= admin ? Member
.Role
.ADMINISTRATOR
: Member
.Role
.DEFAULT
;
495 if (address
.getServiceId() instanceof ACI aci
) {
496 final var change
= groupOperations
.createChangeMemberRole(aci
, newRole
);
497 return commitChange(groupInfoV2
, change
);
499 throw new IllegalArgumentException("Can't make a PNI a group admin.");
503 Pair
<DecryptedGroup
, GroupChangeResponse
> setMessageExpirationTimer(
504 GroupInfoV2 groupInfoV2
,
505 int messageExpirationTimer
506 ) throws IOException
{
507 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
508 final var change
= groupOperations
.createModifyGroupTimerChange(messageExpirationTimer
);
509 return commitChange(groupInfoV2
, change
);
512 Pair
<DecryptedGroup
, GroupChangeResponse
> setIsAnnouncementGroup(
513 GroupInfoV2 groupInfoV2
,
514 boolean isAnnouncementGroup
515 ) throws IOException
{
516 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
517 final var change
= groupOperations
.createAnnouncementGroupChange(isAnnouncementGroup
);
518 return commitChange(groupInfoV2
, change
);
521 private AccessControl
.AccessRequired
toAccessControl(final GroupLinkState state
) {
522 return switch (state
) {
523 case DISABLED
-> AccessControl
.AccessRequired
.UNSATISFIABLE
;
524 case ENABLED
-> AccessControl
.AccessRequired
.ANY
;
525 case ENABLED_WITH_APPROVAL
-> AccessControl
.AccessRequired
.ADMINISTRATOR
;
529 private AccessControl
.AccessRequired
toAccessControl(final GroupPermission permission
) {
530 return switch (permission
) {
531 case EVERY_MEMBER
-> AccessControl
.AccessRequired
.MEMBER
;
532 case ONLY_ADMINS
-> AccessControl
.AccessRequired
.ADMINISTRATOR
;
536 private GroupsV2Operations
.GroupOperations
getGroupOperations(final GroupInfoV2 groupInfoV2
) {
537 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
538 return dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
541 private Pair
<DecryptedGroup
, GroupChangeResponse
> revokeInvites(
542 GroupInfoV2 groupInfoV2
,
543 Set
<DecryptedPendingMember
> pendingMembers
544 ) throws IOException
{
545 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
546 final var uuidCipherTexts
= pendingMembers
.stream().map(member
-> {
548 return new UuidCiphertext(member
.serviceIdCipherText
.toByteArray());
549 } catch (InvalidInputException e
) {
550 throw new AssertionError(e
);
552 }).collect(Collectors
.toSet());
553 return commitChange(groupInfoV2
, groupOperations
.createRemoveInvitationChange(uuidCipherTexts
));
556 private Pair
<DecryptedGroup
, GroupChangeResponse
> approveJoinRequest(
557 GroupInfoV2 groupInfoV2
,
559 ) throws IOException
{
560 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
561 return commitChange(groupInfoV2
, groupOperations
.createApproveGroupJoinRequest(uuids
));
564 private Pair
<DecryptedGroup
, GroupChangeResponse
> refuseJoinRequest(
565 GroupInfoV2 groupInfoV2
,
566 Set
<ServiceId
> serviceIds
567 ) throws IOException
{
568 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
569 return commitChange(groupInfoV2
, groupOperations
.createRefuseGroupJoinRequest(serviceIds
, false, List
.of()));
572 private Pair
<DecryptedGroup
, GroupChangeResponse
> ejectMembers(
573 GroupInfoV2 groupInfoV2
,
575 ) throws IOException
{
576 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
577 return commitChange(groupInfoV2
, groupOperations
.createRemoveMembersChange(members
, false, List
.of()));
580 private Pair
<DecryptedGroup
, GroupChangeResponse
> commitChange(
581 GroupInfoV2 groupInfoV2
,
582 GroupChange
.Actions
.Builder change
583 ) throws IOException
{
584 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
585 final var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
586 final var previousGroupState
= groupInfoV2
.getGroup();
587 final var nextRevision
= previousGroupState
.revision
+ 1;
588 final var changeActions
= change
.revision(nextRevision
).build();
589 final DecryptedGroupChange decryptedChange
;
590 final DecryptedGroup decryptedGroupState
;
593 decryptedChange
= groupOperations
.decryptChange(changeActions
, getSelfAci());
594 decryptedGroupState
= DecryptedGroupUtil
.apply(previousGroupState
, decryptedChange
);
595 } catch (VerificationFailedException
| InvalidGroupStateException
| NotAbleToApplyGroupV2ChangeException e
) {
596 throw new IOException(e
);
599 var signedGroupChange
= dependencies
.getGroupsV2Api()
600 .patchGroup(changeActions
, getGroupAuthForToday(groupSecretParams
), Optional
.empty());
602 groupInfoV2
.setGroup(decryptedGroupState
);
604 return new Pair
<>(decryptedGroupState
, signedGroupChange
);
607 private GroupChangeResponse
commitChange(
608 GroupSecretParams groupSecretParams
,
610 GroupChange
.Actions
.Builder change
,
611 GroupLinkPassword password
612 ) throws IOException
{
613 final var nextRevision
= currentRevision
+ 1;
614 final var changeActions
= change
.revision(nextRevision
).build();
616 return dependencies
.getGroupsV2Api()
617 .patchGroup(changeActions
,
618 getGroupAuthForToday(groupSecretParams
),
619 Optional
.ofNullable(password
).map(GroupLinkPassword
::serialize
));
622 Pair
<ServiceId
, ProfileKey
> getAuthoritativeProfileKeyFromChange(final DecryptedGroupChange change
) {
623 UUID editor
= UuidUtil
.fromByteStringOrNull(change
.editorServiceIdBytes
);
624 final var editorProfileKeyBytes
= Stream
.concat(Stream
.of(change
.newMembers
.stream(),
625 change
.promotePendingMembers
.stream(),
626 change
.modifiedProfileKeys
.stream())
627 .flatMap(Function
.identity())
628 .filter(m
-> UuidUtil
.fromByteString(m
.aciBytes
).equals(editor
))
629 .map(m
-> m
.profileKey
),
630 change
.newRequestingMembers
.stream()
631 .filter(m
-> UuidUtil
.fromByteString(m
.aciBytes
).equals(editor
))
632 .map(m
-> m
.profileKey
)).findFirst();
634 if (editorProfileKeyBytes
.isEmpty()) {
638 ProfileKey profileKey
;
640 profileKey
= new ProfileKey(editorProfileKeyBytes
.get().toByteArray());
641 } catch (InvalidInputException e
) {
642 logger
.debug("Bad profile key in group");
646 return new Pair
<>(ACI
.from(editor
), profileKey
);
649 DecryptedGroup
getUpdatedDecryptedGroup(DecryptedGroup group
, DecryptedGroupChange decryptedGroupChange
) {
651 return DecryptedGroupUtil
.apply(group
, decryptedGroupChange
);
652 } catch (NotAbleToApplyGroupV2ChangeException e
) {
657 DecryptedGroupChange
getDecryptedGroupChange(byte[] signedGroupChange
, GroupMasterKey groupMasterKey
) {
658 if (signedGroupChange
!= null) {
659 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
660 final var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
661 final var groupId
= groupSecretParams
.getPublicParams().getGroupIdentifier();
664 return groupOperations
.decryptChange(GroupChange
.ADAPTER
.decode(signedGroupChange
),
665 DecryptChangeVerificationMode
.verify(groupId
)).orElse(null);
666 } catch (VerificationFailedException
| InvalidGroupStateException
| IOException e
) {
674 private static long currentDaySeconds() {
675 return TimeUnit
.DAYS
.toSeconds(TimeUnit
.MILLISECONDS
.toDays(System
.currentTimeMillis()));
678 private GroupsV2AuthorizationString
getGroupAuthForToday(
679 final GroupSecretParams groupSecretParams
680 ) throws IOException
{
681 final var todaySeconds
= currentDaySeconds();
682 if (groupApiCredentials
== null || !groupApiCredentials
.containsKey(todaySeconds
)) {
683 // Returns credentials for the next 7 days
684 groupApiCredentials
= dependencies
.getGroupsV2Api()
685 .getCredentials(todaySeconds
)
686 .getAuthCredentialWithPniResponseHashMap();
687 // TODO cache credentials on disk until they expire
690 return getAuthorizationString(groupSecretParams
, todaySeconds
);
691 } catch (VerificationFailedException e
) {
692 logger
.debug("Group api credentials invalid, renewing and trying again.");
693 groupApiCredentials
.clear();
696 groupApiCredentials
= dependencies
.getGroupsV2Api()
697 .getCredentials(todaySeconds
)
698 .getAuthCredentialWithPniResponseHashMap();
700 return getAuthorizationString(groupSecretParams
, todaySeconds
);
701 } catch (VerificationFailedException e
) {
702 throw new IOException(e
);
706 private GroupsV2AuthorizationString
getAuthorizationString(
707 final GroupSecretParams groupSecretParams
,
708 final long todaySeconds
709 ) throws VerificationFailedException
{
710 var authCredentialResponse
= groupApiCredentials
.get(todaySeconds
);
711 final var aci
= getSelfAci();
712 final var pni
= getSelfPni();
713 return dependencies
.getGroupsV2Api()
714 .getGroupsV2AuthorizationString(aci
, pni
, todaySeconds
, groupSecretParams
, authCredentialResponse
);
717 private ACI
getSelfAci() {
718 return context
.getAccount().getAci();
721 private PNI
getSelfPni() {
722 return context
.getAccount().getPni();