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
.Member
;
23 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroup
;
24 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroupChange
;
25 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroupJoinInfo
;
26 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedMember
;
27 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedPendingMember
;
28 import org
.slf4j
.Logger
;
29 import org
.slf4j
.LoggerFactory
;
30 import org
.whispersystems
.signalservice
.api
.groupsv2
.DecryptedGroupUtil
;
31 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupCandidate
;
32 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupHistoryPage
;
33 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupLinkNotActiveException
;
34 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2AuthorizationString
;
35 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2Operations
;
36 import org
.whispersystems
.signalservice
.api
.groupsv2
.InvalidGroupStateException
;
37 import org
.whispersystems
.signalservice
.api
.groupsv2
.NotAbleToApplyGroupV2ChangeException
;
38 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
;
39 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
.ACI
;
40 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
.PNI
;
41 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
42 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.NonSuccessfulResponseCodeException
;
43 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
45 import java
.io
.IOException
;
46 import java
.util
.ArrayList
;
47 import java
.util
.Arrays
;
48 import java
.util
.List
;
50 import java
.util
.Optional
;
52 import java
.util
.UUID
;
53 import java
.util
.concurrent
.TimeUnit
;
54 import java
.util
.function
.Function
;
55 import java
.util
.stream
.Collectors
;
56 import java
.util
.stream
.Stream
;
58 import okio
.ByteString
;
62 private final static Logger logger
= LoggerFactory
.getLogger(GroupV2Helper
.class);
64 private final SignalDependencies dependencies
;
65 private final Context context
;
67 private Map
<Long
, AuthCredentialWithPniResponse
> groupApiCredentials
;
69 GroupV2Helper(final Context context
) {
70 this.dependencies
= context
.getDependencies();
71 this.context
= context
;
74 void clearAuthCredentialCache() {
75 groupApiCredentials
= null;
78 DecryptedGroup
getDecryptedGroup(final GroupSecretParams groupSecretParams
) throws NotAGroupMemberException
{
80 final var groupsV2AuthorizationString
= getGroupAuthForToday(groupSecretParams
);
81 return dependencies
.getGroupsV2Api().getGroup(groupSecretParams
, groupsV2AuthorizationString
);
82 } catch (NonSuccessfulResponseCodeException e
) {
83 if (e
.getCode() == 403) {
84 throw new NotAGroupMemberException(GroupUtils
.getGroupIdV2(groupSecretParams
), null);
86 logger
.warn("Failed to retrieve Group V2 info, ignoring: {}", e
.getMessage());
88 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
89 logger
.warn("Failed to retrieve Group V2 info, ignoring: {}", e
.getMessage());
94 DecryptedGroupJoinInfo
getDecryptedGroupJoinInfo(
95 GroupMasterKey groupMasterKey
, GroupLinkPassword password
96 ) throws IOException
, GroupLinkNotActiveException
{
97 var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
99 return dependencies
.getGroupsV2Api()
100 .getGroupJoinInfo(groupSecretParams
,
101 Optional
.ofNullable(password
).map(GroupLinkPassword
::serialize
),
102 getGroupAuthForToday(groupSecretParams
));
105 GroupHistoryPage
getDecryptedGroupHistoryPage(
106 final GroupSecretParams groupSecretParams
, int fromRevision
107 ) throws NotAGroupMemberException
{
109 final var groupsV2AuthorizationString
= getGroupAuthForToday(groupSecretParams
);
110 return dependencies
.getGroupsV2Api()
111 .getGroupHistoryPage(groupSecretParams
, fromRevision
, groupsV2AuthorizationString
, false);
112 } catch (NonSuccessfulResponseCodeException e
) {
113 if (e
.getCode() == 403) {
114 throw new NotAGroupMemberException(GroupUtils
.getGroupIdV2(groupSecretParams
), null);
116 logger
.warn("Failed to retrieve Group V2 history, ignoring: {}", e
.getMessage());
118 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
119 logger
.warn("Failed to retrieve Group V2 history, ignoring: {}", e
.getMessage());
124 int findRevisionWeWereAdded(DecryptedGroup partialDecryptedGroup
) {
125 ByteString aciBytes
= getSelfAci().toByteString();
126 ByteString pniBytes
= getSelfPni().toByteString();
127 for (DecryptedMember decryptedMember
: partialDecryptedGroup
.members
) {
128 if (decryptedMember
.aciBytes
.equals(aciBytes
) || decryptedMember
.pniBytes
.equals(pniBytes
)) {
129 return decryptedMember
.joinedAtRevision
;
132 return partialDecryptedGroup
.revision
;
135 Pair
<GroupInfoV2
, DecryptedGroup
> createGroup(
136 String name
, Set
<RecipientId
> members
, byte[] avatarFile
138 final var newGroup
= buildNewGroup(name
, members
, avatarFile
);
139 if (newGroup
== null) {
143 final var groupSecretParams
= newGroup
.getGroupSecretParams();
145 final GroupsV2AuthorizationString groupAuthForToday
;
146 final DecryptedGroup decryptedGroup
;
148 groupAuthForToday
= getGroupAuthForToday(groupSecretParams
);
149 dependencies
.getGroupsV2Api().putNewGroup(newGroup
, groupAuthForToday
);
150 decryptedGroup
= dependencies
.getGroupsV2Api().getGroup(groupSecretParams
, groupAuthForToday
);
151 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
152 logger
.warn("Failed to create V2 group: {}", e
.getMessage());
155 if (decryptedGroup
== null) {
156 logger
.warn("Failed to create V2 group, unknown error!");
160 final var groupId
= GroupUtils
.getGroupIdV2(groupSecretParams
);
161 final var masterKey
= groupSecretParams
.getMasterKey();
162 var g
= new GroupInfoV2(groupId
, masterKey
, context
.getAccount().getRecipientResolver());
164 return new Pair
<>(g
, decryptedGroup
);
167 private GroupsV2Operations
.NewGroup
buildNewGroup(
168 String name
, Set
<RecipientId
> members
, byte[] avatar
170 final var profileKeyCredential
= context
.getProfileHelper()
171 .getExpiringProfileKeyCredential(context
.getAccount().getSelfRecipientId());
172 if (profileKeyCredential
== null) {
173 logger
.warn("Cannot create a V2 group as self does not have a versioned profile");
177 final var self
= new GroupCandidate(getSelfAci(), Optional
.of(profileKeyCredential
));
178 final var memberList
= new ArrayList
<>(members
);
179 final var credentials
= context
.getProfileHelper().getExpiringProfileKeyCredential(memberList
).stream();
180 final var uuids
= memberList
.stream()
181 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId());
182 var candidates
= Utils
.zip(uuids
,
184 (uuid
, credential
) -> new GroupCandidate(uuid
, Optional
.ofNullable(credential
)))
185 .collect(Collectors
.toSet());
187 final var groupSecretParams
= GroupSecretParams
.generate();
188 return dependencies
.getGroupsV2Operations()
189 .createNewGroup(groupSecretParams
,
191 Optional
.ofNullable(avatar
),
198 Pair
<DecryptedGroup
, GroupChange
> updateGroup(
199 GroupInfoV2 groupInfoV2
, String name
, String description
, byte[] avatarFile
200 ) throws IOException
{
201 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
202 var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
204 var change
= name
!= null ? groupOperations
.createModifyGroupTitle(name
) : new GroupChange
.Actions
.Builder();
206 if (description
!= null) {
207 change
.modifyDescription(groupOperations
.createModifyGroupDescriptionAction(description
).build());
210 if (avatarFile
!= null) {
211 var avatarCdnKey
= dependencies
.getGroupsV2Api()
212 .uploadAvatar(avatarFile
, groupSecretParams
, getGroupAuthForToday(groupSecretParams
));
213 change
.modifyAvatar(new GroupChange
.Actions
.ModifyAvatarAction
.Builder().avatar(avatarCdnKey
).build());
216 change
.sourceServiceId(getSelfAci().toByteString());
218 return commitChange(groupInfoV2
, change
);
221 Pair
<DecryptedGroup
, GroupChange
> addMembers(
222 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> newMembers
223 ) throws IOException
{
224 GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
226 final var memberList
= new ArrayList
<>(newMembers
);
227 final var credentials
= context
.getProfileHelper().getExpiringProfileKeyCredential(memberList
).stream();
228 final var uuids
= memberList
.stream()
229 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId());
230 var candidates
= Utils
.zip(uuids
,
232 (uuid
, credential
) -> new GroupCandidate(uuid
, Optional
.ofNullable(credential
)))
233 .collect(Collectors
.toSet());
234 final var bannedUuids
= groupInfoV2
.getBannedMembers()
236 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId())
237 .collect(Collectors
.toSet());
239 final var aci
= getSelfAci();
240 final var change
= groupOperations
.createModifyGroupMembershipChange(candidates
, bannedUuids
, aci
);
242 change
.sourceServiceId(getSelfAci().toByteString());
244 return commitChange(groupInfoV2
, change
);
247 Pair
<DecryptedGroup
, GroupChange
> leaveGroup(
248 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> membersToMakeAdmin
249 ) throws IOException
{
250 var pendingMembersList
= groupInfoV2
.getGroup().pendingMembers
;
251 final var selfAci
= getSelfAci();
252 var selfPendingMember
= DecryptedGroupUtil
.findPendingByServiceId(pendingMembersList
, selfAci
);
254 if (selfPendingMember
.isPresent()) {
255 return revokeInvites(groupInfoV2
, Set
.of(selfPendingMember
.get()));
258 final var adminUuids
= membersToMakeAdmin
.stream()
259 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
260 .map(SignalServiceAddress
::getServiceId
)
261 .map(ServiceId
::getRawUuid
)
263 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
264 return commitChange(groupInfoV2
, groupOperations
.createLeaveAndPromoteMembersToAdmin(selfAci
, adminUuids
));
267 Pair
<DecryptedGroup
, GroupChange
> removeMembers(
268 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> members
269 ) throws IOException
{
270 final var memberUuids
= members
.stream()
271 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
272 .map(SignalServiceAddress
::getServiceId
)
273 .filter(m
-> m
instanceof ACI
)
275 .collect(Collectors
.toSet());
276 return ejectMembers(groupInfoV2
, memberUuids
);
279 Pair
<DecryptedGroup
, GroupChange
> approveJoinRequestMembers(
280 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> members
281 ) throws IOException
{
282 final var memberUuids
= members
.stream()
283 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
284 .map(SignalServiceAddress
::getServiceId
)
285 .map(ServiceId
::getRawUuid
)
286 .collect(Collectors
.toSet());
287 return approveJoinRequest(groupInfoV2
, memberUuids
);
290 Pair
<DecryptedGroup
, GroupChange
> refuseJoinRequestMembers(
291 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> members
292 ) throws IOException
{
293 final var memberUuids
= members
.stream()
294 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
295 .map(SignalServiceAddress
::getServiceId
)
296 .collect(Collectors
.toSet());
297 return refuseJoinRequest(groupInfoV2
, memberUuids
);
300 Pair
<DecryptedGroup
, GroupChange
> revokeInvitedMembers(
301 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> members
302 ) throws IOException
{
303 var pendingMembersList
= groupInfoV2
.getGroup().pendingMembers
;
304 final var memberUuids
= members
.stream()
305 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
306 .map(SignalServiceAddress
::getServiceId
)
307 .map(uuid
-> DecryptedGroupUtil
.findPendingByServiceId(pendingMembersList
, uuid
))
308 .filter(Optional
::isPresent
)
310 .collect(Collectors
.toSet());
311 return revokeInvites(groupInfoV2
, memberUuids
);
314 Pair
<DecryptedGroup
, GroupChange
> banMembers(
315 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> block
316 ) throws IOException
{
317 GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
319 final var serviceIds
= block
.stream()
320 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId())
321 .collect(Collectors
.toSet());
323 final var change
= groupOperations
.createBanServiceIdsChange(serviceIds
,
325 groupInfoV2
.getGroup().bannedMembers
);
327 change
.sourceServiceId(getSelfAci().toByteString());
329 return commitChange(groupInfoV2
, change
);
332 Pair
<DecryptedGroup
, GroupChange
> unbanMembers(
333 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> block
334 ) throws IOException
{
335 GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
337 final var serviceIds
= block
.stream()
338 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId())
339 .collect(Collectors
.toSet());
341 final var change
= groupOperations
.createUnbanServiceIdsChange(serviceIds
);
343 change
.sourceServiceId(getSelfAci().toByteString());
345 return commitChange(groupInfoV2
, change
);
348 Pair
<DecryptedGroup
, GroupChange
> resetGroupLinkPassword(GroupInfoV2 groupInfoV2
) throws IOException
{
349 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
350 final var newGroupLinkPassword
= GroupLinkPassword
.createNew().serialize();
351 final var change
= groupOperations
.createModifyGroupLinkPasswordChange(newGroupLinkPassword
);
352 return commitChange(groupInfoV2
, change
);
355 Pair
<DecryptedGroup
, GroupChange
> setGroupLinkState(
356 GroupInfoV2 groupInfoV2
, GroupLinkState state
357 ) throws IOException
{
358 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
360 final var accessRequired
= toAccessControl(state
);
361 final var requiresNewPassword
= state
!= GroupLinkState
.DISABLED
362 && groupInfoV2
.getGroup().inviteLinkPassword
.toByteArray().length
== 0;
364 final var change
= requiresNewPassword ? groupOperations
.createModifyGroupLinkPasswordAndRightsChange(
365 GroupLinkPassword
.createNew().serialize(),
366 accessRequired
) : groupOperations
.createChangeJoinByLinkRights(accessRequired
);
367 return commitChange(groupInfoV2
, change
);
370 Pair
<DecryptedGroup
, GroupChange
> setEditDetailsPermission(
371 GroupInfoV2 groupInfoV2
, GroupPermission permission
372 ) throws IOException
{
373 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
375 final var accessRequired
= toAccessControl(permission
);
376 final var change
= groupOperations
.createChangeAttributesRights(accessRequired
);
377 return commitChange(groupInfoV2
, change
);
380 Pair
<DecryptedGroup
, GroupChange
> setAddMemberPermission(
381 GroupInfoV2 groupInfoV2
, GroupPermission permission
382 ) throws IOException
{
383 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
385 final var accessRequired
= toAccessControl(permission
);
386 final var change
= groupOperations
.createChangeMembershipRights(accessRequired
);
387 return commitChange(groupInfoV2
, change
);
390 Pair
<DecryptedGroup
, GroupChange
> updateSelfProfileKey(GroupInfoV2 groupInfoV2
) throws IOException
{
391 Optional
<DecryptedMember
> selfInGroup
= groupInfoV2
.getGroup() == null
393 : DecryptedGroupUtil
.findMemberByAci(groupInfoV2
.getGroup().members
, getSelfAci());
394 if (selfInGroup
.isEmpty()) {
395 logger
.trace("Not updating group, self not in group " + groupInfoV2
.getGroupId().toBase64());
399 final var profileKey
= context
.getAccount().getProfileKey();
400 if (Arrays
.equals(profileKey
.serialize(), selfInGroup
.get().profileKey
.toByteArray())) {
401 logger
.trace("Not updating group, own Profile Key is already up to date in group "
402 + groupInfoV2
.getGroupId().toBase64());
405 logger
.debug("Updating own profile key in group " + groupInfoV2
.getGroupId().toBase64());
407 final var selfRecipientId
= context
.getAccount().getSelfRecipientId();
408 final var profileKeyCredential
= context
.getProfileHelper().getExpiringProfileKeyCredential(selfRecipientId
);
409 if (profileKeyCredential
== null) {
410 logger
.trace("Cannot update profile key as self does not have a versioned profile");
414 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
415 final var change
= groupOperations
.createUpdateProfileKeyCredentialChange(profileKeyCredential
);
416 change
.sourceServiceId(getSelfAci().toByteString());
417 return commitChange(groupInfoV2
, change
);
420 GroupChange
joinGroup(
421 GroupMasterKey groupMasterKey
,
422 GroupLinkPassword groupLinkPassword
,
423 DecryptedGroupJoinInfo decryptedGroupJoinInfo
424 ) throws IOException
{
425 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
426 final var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
428 final var selfRecipientId
= context
.getAccount().getSelfRecipientId();
429 final var profileKeyCredential
= context
.getProfileHelper().getExpiringProfileKeyCredential(selfRecipientId
);
430 if (profileKeyCredential
== null) {
431 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
434 var requestToJoin
= decryptedGroupJoinInfo
.addFromInviteLink
== AccessControl
.AccessRequired
.ADMINISTRATOR
;
435 var change
= requestToJoin
436 ? groupOperations
.createGroupJoinRequest(profileKeyCredential
)
437 : groupOperations
.createGroupJoinDirect(profileKeyCredential
);
439 change
.sourceServiceId(context
.getRecipientHelper()
440 .resolveSignalServiceAddress(selfRecipientId
)
444 return commitChange(groupSecretParams
, decryptedGroupJoinInfo
.revision
, change
, groupLinkPassword
);
447 Pair
<DecryptedGroup
, GroupChange
> acceptInvite(GroupInfoV2 groupInfoV2
) throws IOException
{
448 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
450 final var selfRecipientId
= context
.getAccount().getSelfRecipientId();
451 final var profileKeyCredential
= context
.getProfileHelper().getExpiringProfileKeyCredential(selfRecipientId
);
452 if (profileKeyCredential
== null) {
453 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
456 final var change
= groupOperations
.createAcceptInviteChange(profileKeyCredential
);
458 final var aci
= context
.getRecipientHelper().resolveSignalServiceAddress(selfRecipientId
).getServiceId();
459 change
.sourceServiceId(aci
.toByteString());
461 return commitChange(groupInfoV2
, change
);
464 Pair
<DecryptedGroup
, GroupChange
> setMemberAdmin(
465 GroupInfoV2 groupInfoV2
, RecipientId recipientId
, boolean admin
466 ) throws IOException
{
467 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
468 final var address
= context
.getRecipientHelper().resolveSignalServiceAddress(recipientId
);
469 final var newRole
= admin ? Member
.Role
.ADMINISTRATOR
: Member
.Role
.DEFAULT
;
470 if (address
.getServiceId() instanceof ACI aci
) {
471 final var change
= groupOperations
.createChangeMemberRole(aci
, newRole
);
472 return commitChange(groupInfoV2
, change
);
474 throw new IllegalArgumentException("Can't make a PNI a group admin.");
478 Pair
<DecryptedGroup
, GroupChange
> setMessageExpirationTimer(
479 GroupInfoV2 groupInfoV2
, int messageExpirationTimer
480 ) throws IOException
{
481 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
482 final var change
= groupOperations
.createModifyGroupTimerChange(messageExpirationTimer
);
483 return commitChange(groupInfoV2
, change
);
486 Pair
<DecryptedGroup
, GroupChange
> setIsAnnouncementGroup(
487 GroupInfoV2 groupInfoV2
, boolean isAnnouncementGroup
488 ) throws IOException
{
489 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
490 final var change
= groupOperations
.createAnnouncementGroupChange(isAnnouncementGroup
);
491 return commitChange(groupInfoV2
, change
);
494 private AccessControl
.AccessRequired
toAccessControl(final GroupLinkState state
) {
495 return switch (state
) {
496 case DISABLED
-> AccessControl
.AccessRequired
.UNSATISFIABLE
;
497 case ENABLED
-> AccessControl
.AccessRequired
.ANY
;
498 case ENABLED_WITH_APPROVAL
-> AccessControl
.AccessRequired
.ADMINISTRATOR
;
502 private AccessControl
.AccessRequired
toAccessControl(final GroupPermission permission
) {
503 return switch (permission
) {
504 case EVERY_MEMBER
-> AccessControl
.AccessRequired
.MEMBER
;
505 case ONLY_ADMINS
-> AccessControl
.AccessRequired
.ADMINISTRATOR
;
509 private GroupsV2Operations
.GroupOperations
getGroupOperations(final GroupInfoV2 groupInfoV2
) {
510 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
511 return dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
514 private Pair
<DecryptedGroup
, GroupChange
> revokeInvites(
515 GroupInfoV2 groupInfoV2
, Set
<DecryptedPendingMember
> pendingMembers
516 ) throws IOException
{
517 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
518 final var uuidCipherTexts
= pendingMembers
.stream().map(member
-> {
520 return new UuidCiphertext(member
.serviceIdCipherText
.toByteArray());
521 } catch (InvalidInputException e
) {
522 throw new AssertionError(e
);
524 }).collect(Collectors
.toSet());
525 return commitChange(groupInfoV2
, groupOperations
.createRemoveInvitationChange(uuidCipherTexts
));
528 private Pair
<DecryptedGroup
, GroupChange
> approveJoinRequest(
529 GroupInfoV2 groupInfoV2
, Set
<UUID
> uuids
530 ) throws IOException
{
531 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
532 return commitChange(groupInfoV2
, groupOperations
.createApproveGroupJoinRequest(uuids
));
535 private Pair
<DecryptedGroup
, GroupChange
> refuseJoinRequest(
536 GroupInfoV2 groupInfoV2
, Set
<ServiceId
> serviceIds
537 ) throws IOException
{
538 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
539 return commitChange(groupInfoV2
, groupOperations
.createRefuseGroupJoinRequest(serviceIds
, false, List
.of()));
542 private Pair
<DecryptedGroup
, GroupChange
> ejectMembers(
543 GroupInfoV2 groupInfoV2
, Set
<ACI
> members
544 ) throws IOException
{
545 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
546 return commitChange(groupInfoV2
, groupOperations
.createRemoveMembersChange(members
, false, List
.of()));
549 private Pair
<DecryptedGroup
, GroupChange
> commitChange(
550 GroupInfoV2 groupInfoV2
, GroupChange
.Actions
.Builder change
551 ) throws IOException
{
552 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
553 final var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
554 final var previousGroupState
= groupInfoV2
.getGroup();
555 final var nextRevision
= previousGroupState
.revision
+ 1;
556 final var changeActions
= change
.revision(nextRevision
).build();
557 final DecryptedGroupChange decryptedChange
;
558 final DecryptedGroup decryptedGroupState
;
561 decryptedChange
= groupOperations
.decryptChange(changeActions
, getSelfAci());
562 decryptedGroupState
= DecryptedGroupUtil
.apply(previousGroupState
, decryptedChange
);
563 } catch (VerificationFailedException
| InvalidGroupStateException
| NotAbleToApplyGroupV2ChangeException e
) {
564 throw new IOException(e
);
567 var signedGroupChange
= dependencies
.getGroupsV2Api()
568 .patchGroup(changeActions
, getGroupAuthForToday(groupSecretParams
), Optional
.empty());
570 return new Pair
<>(decryptedGroupState
, signedGroupChange
);
573 private GroupChange
commitChange(
574 GroupSecretParams groupSecretParams
,
576 GroupChange
.Actions
.Builder change
,
577 GroupLinkPassword password
578 ) throws IOException
{
579 final var nextRevision
= currentRevision
+ 1;
580 final var changeActions
= change
.revision(nextRevision
).build();
582 return dependencies
.getGroupsV2Api()
583 .patchGroup(changeActions
,
584 getGroupAuthForToday(groupSecretParams
),
585 Optional
.ofNullable(password
).map(GroupLinkPassword
::serialize
));
588 Pair
<ServiceId
, ProfileKey
> getAuthoritativeProfileKeyFromChange(final DecryptedGroupChange change
) {
589 UUID editor
= UuidUtil
.fromByteStringOrNull(change
.editorServiceIdBytes
);
590 final var editorProfileKeyBytes
= Stream
.concat(Stream
.of(change
.newMembers
.stream(),
591 change
.promotePendingMembers
.stream(),
592 change
.modifiedProfileKeys
.stream())
593 .flatMap(Function
.identity())
594 .filter(m
-> UuidUtil
.fromByteString(m
.aciBytes
).equals(editor
))
595 .map(m
-> m
.profileKey
),
596 change
.newRequestingMembers
.stream()
597 .filter(m
-> UuidUtil
.fromByteString(m
.aciBytes
).equals(editor
))
598 .map(m
-> m
.profileKey
)).findFirst();
600 if (editorProfileKeyBytes
.isEmpty()) {
604 ProfileKey profileKey
;
606 profileKey
= new ProfileKey(editorProfileKeyBytes
.get().toByteArray());
607 } catch (InvalidInputException e
) {
608 logger
.debug("Bad profile key in group");
612 return new Pair
<>(ACI
.from(editor
), profileKey
);
615 DecryptedGroup
getUpdatedDecryptedGroup(DecryptedGroup group
, DecryptedGroupChange decryptedGroupChange
) {
617 return DecryptedGroupUtil
.apply(group
, decryptedGroupChange
);
618 } catch (NotAbleToApplyGroupV2ChangeException e
) {
623 DecryptedGroupChange
getDecryptedGroupChange(byte[] signedGroupChange
, GroupMasterKey groupMasterKey
) {
624 if (signedGroupChange
!= null) {
625 var groupOperations
= dependencies
.getGroupsV2Operations()
626 .forGroup(GroupSecretParams
.deriveFromMasterKey(groupMasterKey
));
629 return groupOperations
.decryptChange(GroupChange
.ADAPTER
.decode(signedGroupChange
), true).orElse(null);
630 } catch (VerificationFailedException
| InvalidGroupStateException
| IOException e
) {
638 private static long currentDaySeconds() {
639 return TimeUnit
.DAYS
.toSeconds(TimeUnit
.MILLISECONDS
.toDays(System
.currentTimeMillis()));
642 private GroupsV2AuthorizationString
getGroupAuthForToday(
643 final GroupSecretParams groupSecretParams
644 ) throws IOException
{
645 final var todaySeconds
= currentDaySeconds();
646 if (groupApiCredentials
== null || !groupApiCredentials
.containsKey(todaySeconds
)) {
647 // Returns credentials for the next 7 days
648 groupApiCredentials
= dependencies
.getGroupsV2Api()
649 .getCredentials(todaySeconds
)
650 .getAuthCredentialWithPniResponseHashMap();
651 // TODO cache credentials on disk until they expire
654 return getAuthorizationString(groupSecretParams
, todaySeconds
);
655 } catch (VerificationFailedException e
) {
656 logger
.debug("Group api credentials invalid, renewing and trying again.");
657 groupApiCredentials
.clear();
660 groupApiCredentials
= dependencies
.getGroupsV2Api()
661 .getCredentials(todaySeconds
)
662 .getAuthCredentialWithPniResponseHashMap();
664 return getAuthorizationString(groupSecretParams
, todaySeconds
);
665 } catch (VerificationFailedException e
) {
666 throw new IOException(e
);
670 private GroupsV2AuthorizationString
getAuthorizationString(
671 final GroupSecretParams groupSecretParams
, final long todaySeconds
672 ) throws VerificationFailedException
{
673 var authCredentialResponse
= groupApiCredentials
.get(todaySeconds
);
674 final var aci
= getSelfAci();
675 final var pni
= getSelfPni();
676 return dependencies
.getGroupsV2Api()
677 .getGroupsV2AuthorizationString(aci
, pni
, todaySeconds
, groupSecretParams
, authCredentialResponse
);
680 private ACI
getSelfAci() {
681 return context
.getAccount().getAci();
684 private PNI
getSelfPni() {
685 return context
.getAccount().getPni();