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
;
48 import java
.io
.IOException
;
49 import java
.util
.ArrayList
;
50 import java
.util
.Arrays
;
51 import java
.util
.List
;
53 import java
.util
.Optional
;
55 import java
.util
.UUID
;
56 import java
.util
.concurrent
.TimeUnit
;
57 import java
.util
.function
.Function
;
58 import java
.util
.stream
.Collectors
;
59 import java
.util
.stream
.Stream
;
61 import okio
.ByteString
;
65 private static final Logger logger
= LoggerFactory
.getLogger(GroupV2Helper
.class);
67 private final SignalDependencies dependencies
;
68 private final Context context
;
70 private Map
<Long
, AuthCredentialWithPniResponse
> groupApiCredentials
;
72 GroupV2Helper(final Context context
) {
73 this.dependencies
= context
.getDependencies();
74 this.context
= context
;
77 void clearAuthCredentialCache() {
78 groupApiCredentials
= null;
81 DecryptedGroupResponse
getDecryptedGroup(final GroupSecretParams groupSecretParams
) throws NotAGroupMemberException
{
83 final var groupsV2AuthorizationString
= getGroupAuthForToday(groupSecretParams
);
84 return dependencies
.getGroupsV2Api().getGroup(groupSecretParams
, groupsV2AuthorizationString
);
85 } catch (NonSuccessfulResponseCodeException e
) {
87 throw new NotAGroupMemberException(GroupUtils
.getGroupIdV2(groupSecretParams
), null);
89 logger
.warn("Failed to retrieve Group V2 info, ignoring: {}", e
.getMessage());
91 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException
| InvalidInputException e
) {
92 logger
.warn("Failed to retrieve Group V2 info, ignoring: {}", e
.getMessage());
97 DecryptedGroupJoinInfo
getDecryptedGroupJoinInfo(
98 GroupMasterKey groupMasterKey
,
99 GroupLinkPassword password
100 ) throws IOException
, GroupLinkNotActiveException
{
101 var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
103 return dependencies
.getGroupsV2Api()
104 .getGroupJoinInfo(groupSecretParams
,
105 Optional
.ofNullable(password
).map(GroupLinkPassword
::serialize
),
106 getGroupAuthForToday(groupSecretParams
));
109 GroupHistoryPage
getDecryptedGroupHistoryPage(
110 final GroupSecretParams groupSecretParams
,
112 long sendEndorsementsExpirationMs
113 ) throws NotAGroupMemberException
{
115 final var groupsV2AuthorizationString
= getGroupAuthForToday(groupSecretParams
);
116 return dependencies
.getGroupsV2Api()
117 .getGroupHistoryPage(groupSecretParams
,
119 groupsV2AuthorizationString
,
121 sendEndorsementsExpirationMs
);
122 } catch (NonSuccessfulResponseCodeException e
) {
124 throw new NotAGroupMemberException(GroupUtils
.getGroupIdV2(groupSecretParams
), null);
126 logger
.warn("Failed to retrieve Group V2 history, ignoring: {}", e
.getMessage());
128 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException
| InvalidInputException e
) {
129 logger
.warn("Failed to retrieve Group V2 history, ignoring: {}", e
.getMessage());
134 int findRevisionWeWereAdded(DecryptedGroup partialDecryptedGroup
) {
135 ByteString aciBytes
= getSelfAci().toByteString();
136 ByteString pniBytes
= getSelfPni().toByteString();
137 for (DecryptedMember decryptedMember
: partialDecryptedGroup
.members
) {
138 if (decryptedMember
.aciBytes
.equals(aciBytes
) || decryptedMember
.pniBytes
.equals(pniBytes
)) {
139 return decryptedMember
.joinedAtRevision
;
142 return partialDecryptedGroup
.revision
;
145 Pair
<GroupInfoV2
, DecryptedGroupResponse
> createGroup(String name
, Set
<RecipientId
> members
, byte[] avatarFile
) {
146 final var newGroup
= buildNewGroup(name
, members
, avatarFile
);
147 if (newGroup
== null) {
151 final var groupSecretParams
= newGroup
.getGroupSecretParams();
153 final GroupsV2AuthorizationString groupAuthForToday
;
154 final DecryptedGroupResponse response
;
156 groupAuthForToday
= getGroupAuthForToday(groupSecretParams
);
157 dependencies
.getGroupsV2Api().putNewGroup(newGroup
, groupAuthForToday
);
158 response
= dependencies
.getGroupsV2Api().getGroup(groupSecretParams
, groupAuthForToday
);
159 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException
| InvalidInputException e
) {
160 logger
.warn("Failed to create V2 group: {}", e
.getMessage());
163 if (response
== null) {
164 logger
.warn("Failed to create V2 group, unknown error!");
168 final var groupId
= GroupUtils
.getGroupIdV2(groupSecretParams
);
169 final var masterKey
= groupSecretParams
.getMasterKey();
170 var g
= new GroupInfoV2(groupId
, masterKey
, context
.getAccount().getRecipientResolver());
172 return new Pair
<>(g
, response
);
175 private GroupsV2Operations
.NewGroup
buildNewGroup(String name
, Set
<RecipientId
> members
, byte[] avatar
) {
176 final var profileKeyCredential
= context
.getProfileHelper()
177 .getExpiringProfileKeyCredential(context
.getAccount().getSelfRecipientId());
178 if (profileKeyCredential
== null) {
179 logger
.warn("Cannot create a V2 group as self does not have a versioned profile");
183 final var self
= new GroupCandidate(getSelfAci(), Optional
.of(profileKeyCredential
));
184 final var memberList
= new ArrayList
<>(members
);
185 final var credentials
= context
.getProfileHelper().getExpiringProfileKeyCredential(memberList
).stream();
186 final var uuids
= memberList
.stream()
187 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId());
188 var candidates
= Utils
.zip(uuids
,
190 (uuid
, credential
) -> new GroupCandidate(uuid
, Optional
.ofNullable(credential
)))
191 .collect(Collectors
.toSet());
193 final var groupSecretParams
= GroupSecretParams
.generate();
194 return dependencies
.getGroupsV2Operations()
195 .createNewGroup(groupSecretParams
,
197 Optional
.ofNullable(avatar
),
204 Pair
<DecryptedGroup
, GroupChangeResponse
> updateGroup(
205 GroupInfoV2 groupInfoV2
,
209 ) throws IOException
{
210 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
211 var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
213 var change
= name
!= null ? groupOperations
.createModifyGroupTitle(name
) : new GroupChange
.Actions
.Builder();
215 if (description
!= null) {
216 change
.modifyDescription(groupOperations
.createModifyGroupDescriptionAction(description
).build());
219 if (avatarFile
!= null) {
220 var avatarCdnKey
= dependencies
.getGroupsV2Api()
221 .uploadAvatar(avatarFile
, groupSecretParams
, getGroupAuthForToday(groupSecretParams
));
222 change
.modifyAvatar(new GroupChange
.Actions
.ModifyAvatarAction
.Builder().avatar(avatarCdnKey
).build());
225 change
.sourceServiceId(getSelfAci().toByteString());
227 return commitChange(groupInfoV2
, change
);
230 Pair
<DecryptedGroup
, GroupChangeResponse
> addMembers(
231 GroupInfoV2 groupInfoV2
,
232 Set
<RecipientId
> newMembers
233 ) throws IOException
{
234 GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
236 final var memberList
= new ArrayList
<>(newMembers
);
237 final var credentials
= context
.getProfileHelper().getExpiringProfileKeyCredential(memberList
).stream();
238 final var uuids
= memberList
.stream()
239 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId());
240 var candidates
= Utils
.zip(uuids
,
242 (uuid
, credential
) -> new GroupCandidate(uuid
, Optional
.ofNullable(credential
)))
243 .collect(Collectors
.toSet());
244 final var bannedUuids
= groupInfoV2
.getBannedMembers()
246 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId())
247 .collect(Collectors
.toSet());
249 final var aci
= getSelfAci();
250 final var change
= groupOperations
.createModifyGroupMembershipChange(candidates
, bannedUuids
, aci
);
252 change
.sourceServiceId(getSelfAci().toByteString());
254 return commitChange(groupInfoV2
, change
);
257 Pair
<DecryptedGroup
, GroupChangeResponse
> leaveGroup(
258 GroupInfoV2 groupInfoV2
,
259 Set
<RecipientId
> membersToMakeAdmin
260 ) throws IOException
{
261 var pendingMembersList
= groupInfoV2
.getGroup().pendingMembers
;
262 final var selfAci
= getSelfAci();
263 var selfPendingMember
= DecryptedGroupUtil
.findPendingByServiceId(pendingMembersList
, selfAci
);
265 if (selfPendingMember
.isPresent()) {
266 return revokeInvites(groupInfoV2
, Set
.of(selfPendingMember
.get()));
269 final var adminUuids
= membersToMakeAdmin
.stream()
270 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
271 .map(SignalServiceAddress
::getServiceId
)
272 .map(ServiceId
::getRawUuid
)
274 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
275 return commitChange(groupInfoV2
, groupOperations
.createLeaveAndPromoteMembersToAdmin(selfAci
, adminUuids
));
278 Pair
<DecryptedGroup
, GroupChangeResponse
> removeMembers(
279 GroupInfoV2 groupInfoV2
,
280 Set
<RecipientId
> members
281 ) throws IOException
{
282 final var memberUuids
= members
.stream()
283 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
284 .map(SignalServiceAddress
::getServiceId
)
285 .filter(m
-> m
instanceof ACI
)
287 .collect(Collectors
.toSet());
288 return ejectMembers(groupInfoV2
, memberUuids
);
291 Pair
<DecryptedGroup
, GroupChangeResponse
> approveJoinRequestMembers(
292 GroupInfoV2 groupInfoV2
,
293 Set
<RecipientId
> members
294 ) throws IOException
{
295 final var memberUuids
= members
.stream()
296 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
297 .map(SignalServiceAddress
::getServiceId
)
298 .map(ServiceId
::getRawUuid
)
299 .collect(Collectors
.toSet());
300 return approveJoinRequest(groupInfoV2
, memberUuids
);
303 Pair
<DecryptedGroup
, GroupChangeResponse
> refuseJoinRequestMembers(
304 GroupInfoV2 groupInfoV2
,
305 Set
<RecipientId
> members
306 ) throws IOException
{
307 final var memberUuids
= members
.stream()
308 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
309 .map(SignalServiceAddress
::getServiceId
)
310 .collect(Collectors
.toSet());
311 return refuseJoinRequest(groupInfoV2
, memberUuids
);
314 Pair
<DecryptedGroup
, GroupChangeResponse
> revokeInvitedMembers(
315 GroupInfoV2 groupInfoV2
,
316 Set
<RecipientId
> members
317 ) throws IOException
{
318 var pendingMembersList
= groupInfoV2
.getGroup().pendingMembers
;
319 final var memberUuids
= members
.stream()
320 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
321 .map(SignalServiceAddress
::getServiceId
)
322 .map(uuid
-> DecryptedGroupUtil
.findPendingByServiceId(pendingMembersList
, uuid
))
323 .filter(Optional
::isPresent
)
325 .collect(Collectors
.toSet());
326 return revokeInvites(groupInfoV2
, memberUuids
);
329 Pair
<DecryptedGroup
, GroupChangeResponse
> banMembers(
330 GroupInfoV2 groupInfoV2
,
331 Set
<RecipientId
> block
332 ) throws IOException
{
333 GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
335 final var serviceIds
= block
.stream()
336 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId())
337 .collect(Collectors
.toSet());
339 final var change
= groupOperations
.createBanServiceIdsChange(serviceIds
,
341 groupInfoV2
.getGroup().bannedMembers
);
343 change
.sourceServiceId(getSelfAci().toByteString());
345 return commitChange(groupInfoV2
, change
);
348 Pair
<DecryptedGroup
, GroupChangeResponse
> unbanMembers(
349 GroupInfoV2 groupInfoV2
,
350 Set
<RecipientId
> block
351 ) throws IOException
{
352 GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
354 final var serviceIds
= block
.stream()
355 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId())
356 .collect(Collectors
.toSet());
358 final var change
= groupOperations
.createUnbanServiceIdsChange(serviceIds
);
360 change
.sourceServiceId(getSelfAci().toByteString());
362 return commitChange(groupInfoV2
, change
);
365 Pair
<DecryptedGroup
, GroupChangeResponse
> resetGroupLinkPassword(GroupInfoV2 groupInfoV2
) throws IOException
{
366 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
367 final var newGroupLinkPassword
= GroupLinkPassword
.createNew().serialize();
368 final var change
= groupOperations
.createModifyGroupLinkPasswordChange(newGroupLinkPassword
);
369 return commitChange(groupInfoV2
, change
);
372 Pair
<DecryptedGroup
, GroupChangeResponse
> setGroupLinkState(
373 GroupInfoV2 groupInfoV2
,
375 ) throws IOException
{
376 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
378 final var accessRequired
= toAccessControl(state
);
379 final var requiresNewPassword
= state
!= GroupLinkState
.DISABLED
380 && groupInfoV2
.getGroup().inviteLinkPassword
.toByteArray().length
== 0;
382 final var change
= requiresNewPassword ? groupOperations
.createModifyGroupLinkPasswordAndRightsChange(
383 GroupLinkPassword
.createNew().serialize(),
384 accessRequired
) : groupOperations
.createChangeJoinByLinkRights(accessRequired
);
385 return commitChange(groupInfoV2
, change
);
388 Pair
<DecryptedGroup
, GroupChangeResponse
> setEditDetailsPermission(
389 GroupInfoV2 groupInfoV2
,
390 GroupPermission permission
391 ) throws IOException
{
392 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
394 final var accessRequired
= toAccessControl(permission
);
395 final var change
= groupOperations
.createChangeAttributesRights(accessRequired
);
396 return commitChange(groupInfoV2
, change
);
399 Pair
<DecryptedGroup
, GroupChangeResponse
> setAddMemberPermission(
400 GroupInfoV2 groupInfoV2
,
401 GroupPermission permission
402 ) throws IOException
{
403 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
405 final var accessRequired
= toAccessControl(permission
);
406 final var change
= groupOperations
.createChangeMembershipRights(accessRequired
);
407 return commitChange(groupInfoV2
, change
);
410 Pair
<DecryptedGroup
, GroupChangeResponse
> updateSelfProfileKey(GroupInfoV2 groupInfoV2
) throws IOException
{
411 Optional
<DecryptedMember
> selfInGroup
= groupInfoV2
.getGroup() == null
413 : DecryptedGroupUtil
.findMemberByAci(groupInfoV2
.getGroup().members
, getSelfAci());
414 if (selfInGroup
.isEmpty()) {
415 logger
.trace("Not updating group, self not in group " + groupInfoV2
.getGroupId().toBase64());
419 final var profileKey
= context
.getAccount().getProfileKey();
420 if (Arrays
.equals(profileKey
.serialize(), selfInGroup
.get().profileKey
.toByteArray())) {
421 logger
.trace("Not updating group, own Profile Key is already up to date in group "
422 + groupInfoV2
.getGroupId().toBase64());
425 logger
.debug("Updating own profile key in group " + groupInfoV2
.getGroupId().toBase64());
427 final var selfRecipientId
= context
.getAccount().getSelfRecipientId();
428 final var profileKeyCredential
= context
.getProfileHelper().getExpiringProfileKeyCredential(selfRecipientId
);
429 if (profileKeyCredential
== null) {
430 logger
.trace("Cannot update profile key as self does not have a versioned profile");
434 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
435 final var change
= groupOperations
.createUpdateProfileKeyCredentialChange(profileKeyCredential
);
436 change
.sourceServiceId(getSelfAci().toByteString());
437 return commitChange(groupInfoV2
, change
);
440 GroupChangeResponse
joinGroup(
441 GroupMasterKey groupMasterKey
,
442 GroupLinkPassword groupLinkPassword
,
443 DecryptedGroupJoinInfo decryptedGroupJoinInfo
444 ) throws IOException
{
445 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
446 final var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
448 final var selfRecipientId
= context
.getAccount().getSelfRecipientId();
449 final var profileKeyCredential
= context
.getProfileHelper().getExpiringProfileKeyCredential(selfRecipientId
);
450 if (profileKeyCredential
== null) {
451 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
454 var requestToJoin
= decryptedGroupJoinInfo
.addFromInviteLink
== AccessControl
.AccessRequired
.ADMINISTRATOR
;
455 var change
= requestToJoin
456 ? groupOperations
.createGroupJoinRequest(profileKeyCredential
)
457 : groupOperations
.createGroupJoinDirect(profileKeyCredential
);
459 change
.sourceServiceId(context
.getRecipientHelper()
460 .resolveSignalServiceAddress(selfRecipientId
)
464 return commitChange(groupSecretParams
, decryptedGroupJoinInfo
.revision
, change
, groupLinkPassword
);
467 Pair
<DecryptedGroup
, GroupChangeResponse
> acceptInvite(GroupInfoV2 groupInfoV2
) throws IOException
{
468 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
470 final var selfRecipientId
= context
.getAccount().getSelfRecipientId();
471 final var profileKeyCredential
= context
.getProfileHelper().getExpiringProfileKeyCredential(selfRecipientId
);
472 if (profileKeyCredential
== null) {
473 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
476 final var change
= groupOperations
.createAcceptInviteChange(profileKeyCredential
);
478 final var aci
= context
.getRecipientHelper().resolveSignalServiceAddress(selfRecipientId
).getServiceId();
479 change
.sourceServiceId(aci
.toByteString());
481 return commitChange(groupInfoV2
, change
);
484 Pair
<DecryptedGroup
, GroupChangeResponse
> setMemberAdmin(
485 GroupInfoV2 groupInfoV2
,
486 RecipientId recipientId
,
488 ) throws IOException
{
489 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
490 final var address
= context
.getRecipientHelper().resolveSignalServiceAddress(recipientId
);
491 final var newRole
= admin ? Member
.Role
.ADMINISTRATOR
: Member
.Role
.DEFAULT
;
492 if (address
.getServiceId() instanceof ACI aci
) {
493 final var change
= groupOperations
.createChangeMemberRole(aci
, newRole
);
494 return commitChange(groupInfoV2
, change
);
496 throw new IllegalArgumentException("Can't make a PNI a group admin.");
500 Pair
<DecryptedGroup
, GroupChangeResponse
> setMessageExpirationTimer(
501 GroupInfoV2 groupInfoV2
,
502 int messageExpirationTimer
503 ) throws IOException
{
504 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
505 final var change
= groupOperations
.createModifyGroupTimerChange(messageExpirationTimer
);
506 return commitChange(groupInfoV2
, change
);
509 Pair
<DecryptedGroup
, GroupChangeResponse
> setIsAnnouncementGroup(
510 GroupInfoV2 groupInfoV2
,
511 boolean isAnnouncementGroup
512 ) throws IOException
{
513 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
514 final var change
= groupOperations
.createAnnouncementGroupChange(isAnnouncementGroup
);
515 return commitChange(groupInfoV2
, change
);
518 private AccessControl
.AccessRequired
toAccessControl(final GroupLinkState state
) {
519 return switch (state
) {
520 case DISABLED
-> AccessControl
.AccessRequired
.UNSATISFIABLE
;
521 case ENABLED
-> AccessControl
.AccessRequired
.ANY
;
522 case ENABLED_WITH_APPROVAL
-> AccessControl
.AccessRequired
.ADMINISTRATOR
;
526 private AccessControl
.AccessRequired
toAccessControl(final GroupPermission permission
) {
527 return switch (permission
) {
528 case EVERY_MEMBER
-> AccessControl
.AccessRequired
.MEMBER
;
529 case ONLY_ADMINS
-> AccessControl
.AccessRequired
.ADMINISTRATOR
;
533 private GroupsV2Operations
.GroupOperations
getGroupOperations(final GroupInfoV2 groupInfoV2
) {
534 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
535 return dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
538 private Pair
<DecryptedGroup
, GroupChangeResponse
> revokeInvites(
539 GroupInfoV2 groupInfoV2
,
540 Set
<DecryptedPendingMember
> pendingMembers
541 ) throws IOException
{
542 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
543 final var uuidCipherTexts
= pendingMembers
.stream().map(member
-> {
545 return new UuidCiphertext(member
.serviceIdCipherText
.toByteArray());
546 } catch (InvalidInputException e
) {
547 throw new AssertionError(e
);
549 }).collect(Collectors
.toSet());
550 return commitChange(groupInfoV2
, groupOperations
.createRemoveInvitationChange(uuidCipherTexts
));
553 private Pair
<DecryptedGroup
, GroupChangeResponse
> approveJoinRequest(
554 GroupInfoV2 groupInfoV2
,
556 ) throws IOException
{
557 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
558 return commitChange(groupInfoV2
, groupOperations
.createApproveGroupJoinRequest(uuids
));
561 private Pair
<DecryptedGroup
, GroupChangeResponse
> refuseJoinRequest(
562 GroupInfoV2 groupInfoV2
,
563 Set
<ServiceId
> serviceIds
564 ) throws IOException
{
565 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
566 return commitChange(groupInfoV2
, groupOperations
.createRefuseGroupJoinRequest(serviceIds
, false, List
.of()));
569 private Pair
<DecryptedGroup
, GroupChangeResponse
> ejectMembers(
570 GroupInfoV2 groupInfoV2
,
572 ) throws IOException
{
573 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
574 return commitChange(groupInfoV2
, groupOperations
.createRemoveMembersChange(members
, false, List
.of()));
577 private Pair
<DecryptedGroup
, GroupChangeResponse
> commitChange(
578 GroupInfoV2 groupInfoV2
,
579 GroupChange
.Actions
.Builder change
580 ) throws IOException
{
581 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
582 final var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
583 final var previousGroupState
= groupInfoV2
.getGroup();
584 final var nextRevision
= previousGroupState
.revision
+ 1;
585 final var changeActions
= change
.revision(nextRevision
).build();
586 final DecryptedGroupChange decryptedChange
;
587 final DecryptedGroup decryptedGroupState
;
590 decryptedChange
= groupOperations
.decryptChange(changeActions
, getSelfAci());
591 decryptedGroupState
= DecryptedGroupUtil
.apply(previousGroupState
, decryptedChange
);
592 } catch (VerificationFailedException
| InvalidGroupStateException
| NotAbleToApplyGroupV2ChangeException e
) {
593 throw new IOException(e
);
596 var signedGroupChange
= dependencies
.getGroupsV2Api()
597 .patchGroup(changeActions
, getGroupAuthForToday(groupSecretParams
), Optional
.empty());
599 groupInfoV2
.setGroup(decryptedGroupState
);
601 return new Pair
<>(decryptedGroupState
, signedGroupChange
);
604 private GroupChangeResponse
commitChange(
605 GroupSecretParams groupSecretParams
,
607 GroupChange
.Actions
.Builder change
,
608 GroupLinkPassword password
609 ) throws IOException
{
610 final var nextRevision
= currentRevision
+ 1;
611 final var changeActions
= change
.revision(nextRevision
).build();
613 return dependencies
.getGroupsV2Api()
614 .patchGroup(changeActions
,
615 getGroupAuthForToday(groupSecretParams
),
616 Optional
.ofNullable(password
).map(GroupLinkPassword
::serialize
));
619 Pair
<ServiceId
, ProfileKey
> getAuthoritativeProfileKeyFromChange(final DecryptedGroupChange change
) {
620 UUID editor
= UuidUtil
.fromByteStringOrNull(change
.editorServiceIdBytes
);
621 final var editorProfileKeyBytes
= Stream
.concat(Stream
.of(change
.newMembers
.stream(),
622 change
.promotePendingMembers
.stream(),
623 change
.modifiedProfileKeys
.stream())
624 .flatMap(Function
.identity())
625 .filter(m
-> UuidUtil
.fromByteString(m
.aciBytes
).equals(editor
))
626 .map(m
-> m
.profileKey
),
627 change
.newRequestingMembers
.stream()
628 .filter(m
-> UuidUtil
.fromByteString(m
.aciBytes
).equals(editor
))
629 .map(m
-> m
.profileKey
)).findFirst();
631 if (editorProfileKeyBytes
.isEmpty()) {
635 ProfileKey profileKey
;
637 profileKey
= new ProfileKey(editorProfileKeyBytes
.get().toByteArray());
638 } catch (InvalidInputException e
) {
639 logger
.debug("Bad profile key in group");
643 return new Pair
<>(ACI
.from(editor
), profileKey
);
646 DecryptedGroup
getUpdatedDecryptedGroup(DecryptedGroup group
, DecryptedGroupChange decryptedGroupChange
) {
648 return DecryptedGroupUtil
.apply(group
, decryptedGroupChange
);
649 } catch (NotAbleToApplyGroupV2ChangeException e
) {
654 DecryptedGroupChange
getDecryptedGroupChange(byte[] signedGroupChange
, GroupMasterKey groupMasterKey
) {
655 if (signedGroupChange
!= null) {
656 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
657 final var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
658 final var groupId
= groupSecretParams
.getPublicParams().getGroupIdentifier();
661 return groupOperations
.decryptChange(GroupChange
.ADAPTER
.decode(signedGroupChange
),
662 DecryptChangeVerificationMode
.verify(groupId
)).orElse(null);
663 } catch (VerificationFailedException
| InvalidGroupStateException
| IOException e
) {
671 private static long currentDaySeconds() {
672 return TimeUnit
.DAYS
.toSeconds(TimeUnit
.MILLISECONDS
.toDays(System
.currentTimeMillis()));
675 private GroupsV2AuthorizationString
getGroupAuthForToday(
676 final GroupSecretParams groupSecretParams
677 ) throws IOException
{
678 final var todaySeconds
= currentDaySeconds();
679 if (groupApiCredentials
== null || !groupApiCredentials
.containsKey(todaySeconds
)) {
680 // Returns credentials for the next 7 days
681 groupApiCredentials
= dependencies
.getGroupsV2Api()
682 .getCredentials(todaySeconds
)
683 .getAuthCredentialWithPniResponseHashMap();
684 // TODO cache credentials on disk until they expire
687 return getAuthorizationString(groupSecretParams
, todaySeconds
);
688 } catch (VerificationFailedException e
) {
689 logger
.debug("Group api credentials invalid, renewing and trying again.");
690 groupApiCredentials
.clear();
693 groupApiCredentials
= dependencies
.getGroupsV2Api()
694 .getCredentials(todaySeconds
)
695 .getAuthCredentialWithPniResponseHashMap();
697 return getAuthorizationString(groupSecretParams
, todaySeconds
);
698 } catch (VerificationFailedException e
) {
699 throw new IOException(e
);
703 private GroupsV2AuthorizationString
getAuthorizationString(
704 final GroupSecretParams groupSecretParams
,
705 final long todaySeconds
706 ) throws VerificationFailedException
{
707 var authCredentialResponse
= groupApiCredentials
.get(todaySeconds
);
708 final var aci
= getSelfAci();
709 final var pni
= getSelfPni();
710 return dependencies
.getGroupsV2Api()
711 .getGroupsV2AuthorizationString(aci
, pni
, todaySeconds
, groupSecretParams
, authCredentialResponse
);
714 private ACI
getSelfAci() {
715 return context
.getAccount().getAci();
718 private PNI
getSelfPni() {
719 return context
.getAccount().getPni();