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
.DecryptedGroupResponse
;
32 import org
.whispersystems
.signalservice
.api
.groupsv2
.DecryptedGroupUtil
;
33 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupCandidate
;
34 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupHistoryPage
;
35 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupLinkNotActiveException
;
36 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2AuthorizationString
;
37 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2Operations
;
38 import org
.whispersystems
.signalservice
.api
.groupsv2
.InvalidGroupStateException
;
39 import org
.whispersystems
.signalservice
.api
.groupsv2
.NotAbleToApplyGroupV2ChangeException
;
40 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
;
41 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
.ACI
;
42 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
.PNI
;
43 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
44 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.NonSuccessfulResponseCodeException
;
45 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
47 import java
.io
.IOException
;
48 import java
.util
.ArrayList
;
49 import java
.util
.Arrays
;
50 import java
.util
.List
;
52 import java
.util
.Optional
;
54 import java
.util
.UUID
;
55 import java
.util
.concurrent
.TimeUnit
;
56 import java
.util
.function
.Function
;
57 import java
.util
.stream
.Collectors
;
58 import java
.util
.stream
.Stream
;
60 import okio
.ByteString
;
64 private static final Logger logger
= LoggerFactory
.getLogger(GroupV2Helper
.class);
66 private final SignalDependencies dependencies
;
67 private final Context context
;
69 private Map
<Long
, AuthCredentialWithPniResponse
> groupApiCredentials
;
71 GroupV2Helper(final Context context
) {
72 this.dependencies
= context
.getDependencies();
73 this.context
= context
;
76 void clearAuthCredentialCache() {
77 groupApiCredentials
= null;
80 DecryptedGroupResponse
getDecryptedGroup(final GroupSecretParams groupSecretParams
) throws NotAGroupMemberException
{
82 final var groupsV2AuthorizationString
= getGroupAuthForToday(groupSecretParams
);
83 return dependencies
.getGroupsV2Api().getGroup(groupSecretParams
, groupsV2AuthorizationString
);
84 } catch (NonSuccessfulResponseCodeException e
) {
85 if (e
.getCode() == 403) {
86 throw new NotAGroupMemberException(GroupUtils
.getGroupIdV2(groupSecretParams
), null);
88 logger
.warn("Failed to retrieve Group V2 info, ignoring: {}", e
.getMessage());
90 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException
| InvalidInputException e
) {
91 logger
.warn("Failed to retrieve Group V2 info, ignoring: {}", e
.getMessage());
96 DecryptedGroupJoinInfo
getDecryptedGroupJoinInfo(
97 GroupMasterKey groupMasterKey
,
98 GroupLinkPassword password
99 ) throws IOException
, GroupLinkNotActiveException
{
100 var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
102 return dependencies
.getGroupsV2Api()
103 .getGroupJoinInfo(groupSecretParams
,
104 Optional
.ofNullable(password
).map(GroupLinkPassword
::serialize
),
105 getGroupAuthForToday(groupSecretParams
));
108 GroupHistoryPage
getDecryptedGroupHistoryPage(
109 final GroupSecretParams groupSecretParams
,
111 long sendEndorsementsExpirationMs
112 ) throws NotAGroupMemberException
{
114 final var groupsV2AuthorizationString
= getGroupAuthForToday(groupSecretParams
);
115 return dependencies
.getGroupsV2Api()
116 .getGroupHistoryPage(groupSecretParams
,
118 groupsV2AuthorizationString
,
120 sendEndorsementsExpirationMs
);
121 } catch (NonSuccessfulResponseCodeException e
) {
122 if (e
.getCode() == 403) {
123 throw new NotAGroupMemberException(GroupUtils
.getGroupIdV2(groupSecretParams
), null);
125 logger
.warn("Failed to retrieve Group V2 history, ignoring: {}", e
.getMessage());
127 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException
| InvalidInputException e
) {
128 logger
.warn("Failed to retrieve Group V2 history, ignoring: {}", e
.getMessage());
133 int findRevisionWeWereAdded(DecryptedGroup partialDecryptedGroup
) {
134 ByteString aciBytes
= getSelfAci().toByteString();
135 ByteString pniBytes
= getSelfPni().toByteString();
136 for (DecryptedMember decryptedMember
: partialDecryptedGroup
.members
) {
137 if (decryptedMember
.aciBytes
.equals(aciBytes
) || decryptedMember
.pniBytes
.equals(pniBytes
)) {
138 return decryptedMember
.joinedAtRevision
;
141 return partialDecryptedGroup
.revision
;
144 Pair
<GroupInfoV2
, DecryptedGroupResponse
> createGroup(String name
, Set
<RecipientId
> members
, byte[] avatarFile
) {
145 final var newGroup
= buildNewGroup(name
, members
, avatarFile
);
146 if (newGroup
== null) {
150 final var groupSecretParams
= newGroup
.getGroupSecretParams();
152 final GroupsV2AuthorizationString groupAuthForToday
;
153 final DecryptedGroupResponse response
;
155 groupAuthForToday
= getGroupAuthForToday(groupSecretParams
);
156 dependencies
.getGroupsV2Api().putNewGroup(newGroup
, groupAuthForToday
);
157 response
= dependencies
.getGroupsV2Api().getGroup(groupSecretParams
, groupAuthForToday
);
158 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException
| InvalidInputException e
) {
159 logger
.warn("Failed to create V2 group: {}", e
.getMessage());
162 if (response
== null) {
163 logger
.warn("Failed to create V2 group, unknown error!");
167 final var groupId
= GroupUtils
.getGroupIdV2(groupSecretParams
);
168 final var masterKey
= groupSecretParams
.getMasterKey();
169 var g
= new GroupInfoV2(groupId
, masterKey
, context
.getAccount().getRecipientResolver());
171 return new Pair
<>(g
, response
);
174 private GroupsV2Operations
.NewGroup
buildNewGroup(String name
, Set
<RecipientId
> members
, byte[] avatar
) {
175 final var profileKeyCredential
= context
.getProfileHelper()
176 .getExpiringProfileKeyCredential(context
.getAccount().getSelfRecipientId());
177 if (profileKeyCredential
== null) {
178 logger
.warn("Cannot create a V2 group as self does not have a versioned profile");
182 final var self
= new GroupCandidate(getSelfAci(), Optional
.of(profileKeyCredential
));
183 final var memberList
= new ArrayList
<>(members
);
184 final var credentials
= context
.getProfileHelper().getExpiringProfileKeyCredential(memberList
).stream();
185 final var uuids
= memberList
.stream()
186 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId());
187 var candidates
= Utils
.zip(uuids
,
189 (uuid
, credential
) -> new GroupCandidate(uuid
, Optional
.ofNullable(credential
)))
190 .collect(Collectors
.toSet());
192 final var groupSecretParams
= GroupSecretParams
.generate();
193 return dependencies
.getGroupsV2Operations()
194 .createNewGroup(groupSecretParams
,
196 Optional
.ofNullable(avatar
),
203 Pair
<DecryptedGroup
, GroupChangeResponse
> updateGroup(
204 GroupInfoV2 groupInfoV2
,
208 ) throws IOException
{
209 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
210 var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
212 var change
= name
!= null ? groupOperations
.createModifyGroupTitle(name
) : new GroupChange
.Actions
.Builder();
214 if (description
!= null) {
215 change
.modifyDescription(groupOperations
.createModifyGroupDescriptionAction(description
).build());
218 if (avatarFile
!= null) {
219 var avatarCdnKey
= dependencies
.getGroupsV2Api()
220 .uploadAvatar(avatarFile
, groupSecretParams
, getGroupAuthForToday(groupSecretParams
));
221 change
.modifyAvatar(new GroupChange
.Actions
.ModifyAvatarAction
.Builder().avatar(avatarCdnKey
).build());
224 change
.sourceServiceId(getSelfAci().toByteString());
226 return commitChange(groupInfoV2
, change
);
229 Pair
<DecryptedGroup
, GroupChangeResponse
> addMembers(
230 GroupInfoV2 groupInfoV2
,
231 Set
<RecipientId
> newMembers
232 ) throws IOException
{
233 GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
235 final var memberList
= new ArrayList
<>(newMembers
);
236 final var credentials
= context
.getProfileHelper().getExpiringProfileKeyCredential(memberList
).stream();
237 final var uuids
= memberList
.stream()
238 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId());
239 var candidates
= Utils
.zip(uuids
,
241 (uuid
, credential
) -> new GroupCandidate(uuid
, Optional
.ofNullable(credential
)))
242 .collect(Collectors
.toSet());
243 final var bannedUuids
= groupInfoV2
.getBannedMembers()
245 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId())
246 .collect(Collectors
.toSet());
248 final var aci
= getSelfAci();
249 final var change
= groupOperations
.createModifyGroupMembershipChange(candidates
, bannedUuids
, aci
);
251 change
.sourceServiceId(getSelfAci().toByteString());
253 return commitChange(groupInfoV2
, change
);
256 Pair
<DecryptedGroup
, GroupChangeResponse
> leaveGroup(
257 GroupInfoV2 groupInfoV2
,
258 Set
<RecipientId
> membersToMakeAdmin
259 ) throws IOException
{
260 var pendingMembersList
= groupInfoV2
.getGroup().pendingMembers
;
261 final var selfAci
= getSelfAci();
262 var selfPendingMember
= DecryptedGroupUtil
.findPendingByServiceId(pendingMembersList
, selfAci
);
264 if (selfPendingMember
.isPresent()) {
265 return revokeInvites(groupInfoV2
, Set
.of(selfPendingMember
.get()));
268 final var adminUuids
= membersToMakeAdmin
.stream()
269 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
270 .map(SignalServiceAddress
::getServiceId
)
271 .map(ServiceId
::getRawUuid
)
273 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
274 return commitChange(groupInfoV2
, groupOperations
.createLeaveAndPromoteMembersToAdmin(selfAci
, adminUuids
));
277 Pair
<DecryptedGroup
, GroupChangeResponse
> removeMembers(
278 GroupInfoV2 groupInfoV2
,
279 Set
<RecipientId
> members
280 ) throws IOException
{
281 final var memberUuids
= members
.stream()
282 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
283 .map(SignalServiceAddress
::getServiceId
)
284 .filter(m
-> m
instanceof ACI
)
286 .collect(Collectors
.toSet());
287 return ejectMembers(groupInfoV2
, memberUuids
);
290 Pair
<DecryptedGroup
, GroupChangeResponse
> approveJoinRequestMembers(
291 GroupInfoV2 groupInfoV2
,
292 Set
<RecipientId
> members
293 ) throws IOException
{
294 final var memberUuids
= members
.stream()
295 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
296 .map(SignalServiceAddress
::getServiceId
)
297 .map(ServiceId
::getRawUuid
)
298 .collect(Collectors
.toSet());
299 return approveJoinRequest(groupInfoV2
, memberUuids
);
302 Pair
<DecryptedGroup
, GroupChangeResponse
> refuseJoinRequestMembers(
303 GroupInfoV2 groupInfoV2
,
304 Set
<RecipientId
> members
305 ) throws IOException
{
306 final var memberUuids
= members
.stream()
307 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
308 .map(SignalServiceAddress
::getServiceId
)
309 .collect(Collectors
.toSet());
310 return refuseJoinRequest(groupInfoV2
, memberUuids
);
313 Pair
<DecryptedGroup
, GroupChangeResponse
> revokeInvitedMembers(
314 GroupInfoV2 groupInfoV2
,
315 Set
<RecipientId
> members
316 ) throws IOException
{
317 var pendingMembersList
= groupInfoV2
.getGroup().pendingMembers
;
318 final var memberUuids
= members
.stream()
319 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
320 .map(SignalServiceAddress
::getServiceId
)
321 .map(uuid
-> DecryptedGroupUtil
.findPendingByServiceId(pendingMembersList
, uuid
))
322 .filter(Optional
::isPresent
)
324 .collect(Collectors
.toSet());
325 return revokeInvites(groupInfoV2
, memberUuids
);
328 Pair
<DecryptedGroup
, GroupChangeResponse
> banMembers(
329 GroupInfoV2 groupInfoV2
,
330 Set
<RecipientId
> block
331 ) throws IOException
{
332 GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
334 final var serviceIds
= block
.stream()
335 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId())
336 .collect(Collectors
.toSet());
338 final var change
= groupOperations
.createBanServiceIdsChange(serviceIds
,
340 groupInfoV2
.getGroup().bannedMembers
);
342 change
.sourceServiceId(getSelfAci().toByteString());
344 return commitChange(groupInfoV2
, change
);
347 Pair
<DecryptedGroup
, GroupChangeResponse
> unbanMembers(
348 GroupInfoV2 groupInfoV2
,
349 Set
<RecipientId
> block
350 ) throws IOException
{
351 GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
353 final var serviceIds
= block
.stream()
354 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId())
355 .collect(Collectors
.toSet());
357 final var change
= groupOperations
.createUnbanServiceIdsChange(serviceIds
);
359 change
.sourceServiceId(getSelfAci().toByteString());
361 return commitChange(groupInfoV2
, change
);
364 Pair
<DecryptedGroup
, GroupChangeResponse
> resetGroupLinkPassword(GroupInfoV2 groupInfoV2
) throws IOException
{
365 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
366 final var newGroupLinkPassword
= GroupLinkPassword
.createNew().serialize();
367 final var change
= groupOperations
.createModifyGroupLinkPasswordChange(newGroupLinkPassword
);
368 return commitChange(groupInfoV2
, change
);
371 Pair
<DecryptedGroup
, GroupChangeResponse
> setGroupLinkState(
372 GroupInfoV2 groupInfoV2
,
374 ) throws IOException
{
375 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
377 final var accessRequired
= toAccessControl(state
);
378 final var requiresNewPassword
= state
!= GroupLinkState
.DISABLED
379 && groupInfoV2
.getGroup().inviteLinkPassword
.toByteArray().length
== 0;
381 final var change
= requiresNewPassword ? groupOperations
.createModifyGroupLinkPasswordAndRightsChange(
382 GroupLinkPassword
.createNew().serialize(),
383 accessRequired
) : groupOperations
.createChangeJoinByLinkRights(accessRequired
);
384 return commitChange(groupInfoV2
, change
);
387 Pair
<DecryptedGroup
, GroupChangeResponse
> setEditDetailsPermission(
388 GroupInfoV2 groupInfoV2
,
389 GroupPermission permission
390 ) throws IOException
{
391 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
393 final var accessRequired
= toAccessControl(permission
);
394 final var change
= groupOperations
.createChangeAttributesRights(accessRequired
);
395 return commitChange(groupInfoV2
, change
);
398 Pair
<DecryptedGroup
, GroupChangeResponse
> setAddMemberPermission(
399 GroupInfoV2 groupInfoV2
,
400 GroupPermission permission
401 ) throws IOException
{
402 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
404 final var accessRequired
= toAccessControl(permission
);
405 final var change
= groupOperations
.createChangeMembershipRights(accessRequired
);
406 return commitChange(groupInfoV2
, change
);
409 Pair
<DecryptedGroup
, GroupChangeResponse
> updateSelfProfileKey(GroupInfoV2 groupInfoV2
) throws IOException
{
410 Optional
<DecryptedMember
> selfInGroup
= groupInfoV2
.getGroup() == null
412 : DecryptedGroupUtil
.findMemberByAci(groupInfoV2
.getGroup().members
, getSelfAci());
413 if (selfInGroup
.isEmpty()) {
414 logger
.trace("Not updating group, self not in group " + groupInfoV2
.getGroupId().toBase64());
418 final var profileKey
= context
.getAccount().getProfileKey();
419 if (Arrays
.equals(profileKey
.serialize(), selfInGroup
.get().profileKey
.toByteArray())) {
420 logger
.trace("Not updating group, own Profile Key is already up to date in group "
421 + groupInfoV2
.getGroupId().toBase64());
424 logger
.debug("Updating own profile key in group " + groupInfoV2
.getGroupId().toBase64());
426 final var selfRecipientId
= context
.getAccount().getSelfRecipientId();
427 final var profileKeyCredential
= context
.getProfileHelper().getExpiringProfileKeyCredential(selfRecipientId
);
428 if (profileKeyCredential
== null) {
429 logger
.trace("Cannot update profile key as self does not have a versioned profile");
433 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
434 final var change
= groupOperations
.createUpdateProfileKeyCredentialChange(profileKeyCredential
);
435 change
.sourceServiceId(getSelfAci().toByteString());
436 return commitChange(groupInfoV2
, change
);
439 GroupChangeResponse
joinGroup(
440 GroupMasterKey groupMasterKey
,
441 GroupLinkPassword groupLinkPassword
,
442 DecryptedGroupJoinInfo decryptedGroupJoinInfo
443 ) throws IOException
{
444 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
445 final var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
447 final var selfRecipientId
= context
.getAccount().getSelfRecipientId();
448 final var profileKeyCredential
= context
.getProfileHelper().getExpiringProfileKeyCredential(selfRecipientId
);
449 if (profileKeyCredential
== null) {
450 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
453 var requestToJoin
= decryptedGroupJoinInfo
.addFromInviteLink
== AccessControl
.AccessRequired
.ADMINISTRATOR
;
454 var change
= requestToJoin
455 ? groupOperations
.createGroupJoinRequest(profileKeyCredential
)
456 : groupOperations
.createGroupJoinDirect(profileKeyCredential
);
458 change
.sourceServiceId(context
.getRecipientHelper()
459 .resolveSignalServiceAddress(selfRecipientId
)
463 return commitChange(groupSecretParams
, decryptedGroupJoinInfo
.revision
, change
, groupLinkPassword
);
466 Pair
<DecryptedGroup
, GroupChangeResponse
> acceptInvite(GroupInfoV2 groupInfoV2
) throws IOException
{
467 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
469 final var selfRecipientId
= context
.getAccount().getSelfRecipientId();
470 final var profileKeyCredential
= context
.getProfileHelper().getExpiringProfileKeyCredential(selfRecipientId
);
471 if (profileKeyCredential
== null) {
472 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
475 final var change
= groupOperations
.createAcceptInviteChange(profileKeyCredential
);
477 final var aci
= context
.getRecipientHelper().resolveSignalServiceAddress(selfRecipientId
).getServiceId();
478 change
.sourceServiceId(aci
.toByteString());
480 return commitChange(groupInfoV2
, change
);
483 Pair
<DecryptedGroup
, GroupChangeResponse
> setMemberAdmin(
484 GroupInfoV2 groupInfoV2
,
485 RecipientId recipientId
,
487 ) throws IOException
{
488 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
489 final var address
= context
.getRecipientHelper().resolveSignalServiceAddress(recipientId
);
490 final var newRole
= admin ? Member
.Role
.ADMINISTRATOR
: Member
.Role
.DEFAULT
;
491 if (address
.getServiceId() instanceof ACI aci
) {
492 final var change
= groupOperations
.createChangeMemberRole(aci
, newRole
);
493 return commitChange(groupInfoV2
, change
);
495 throw new IllegalArgumentException("Can't make a PNI a group admin.");
499 Pair
<DecryptedGroup
, GroupChangeResponse
> setMessageExpirationTimer(
500 GroupInfoV2 groupInfoV2
,
501 int messageExpirationTimer
502 ) throws IOException
{
503 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
504 final var change
= groupOperations
.createModifyGroupTimerChange(messageExpirationTimer
);
505 return commitChange(groupInfoV2
, change
);
508 Pair
<DecryptedGroup
, GroupChangeResponse
> setIsAnnouncementGroup(
509 GroupInfoV2 groupInfoV2
,
510 boolean isAnnouncementGroup
511 ) throws IOException
{
512 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
513 final var change
= groupOperations
.createAnnouncementGroupChange(isAnnouncementGroup
);
514 return commitChange(groupInfoV2
, change
);
517 private AccessControl
.AccessRequired
toAccessControl(final GroupLinkState state
) {
518 return switch (state
) {
519 case DISABLED
-> AccessControl
.AccessRequired
.UNSATISFIABLE
;
520 case ENABLED
-> AccessControl
.AccessRequired
.ANY
;
521 case ENABLED_WITH_APPROVAL
-> AccessControl
.AccessRequired
.ADMINISTRATOR
;
525 private AccessControl
.AccessRequired
toAccessControl(final GroupPermission permission
) {
526 return switch (permission
) {
527 case EVERY_MEMBER
-> AccessControl
.AccessRequired
.MEMBER
;
528 case ONLY_ADMINS
-> AccessControl
.AccessRequired
.ADMINISTRATOR
;
532 private GroupsV2Operations
.GroupOperations
getGroupOperations(final GroupInfoV2 groupInfoV2
) {
533 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
534 return dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
537 private Pair
<DecryptedGroup
, GroupChangeResponse
> revokeInvites(
538 GroupInfoV2 groupInfoV2
,
539 Set
<DecryptedPendingMember
> pendingMembers
540 ) throws IOException
{
541 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
542 final var uuidCipherTexts
= pendingMembers
.stream().map(member
-> {
544 return new UuidCiphertext(member
.serviceIdCipherText
.toByteArray());
545 } catch (InvalidInputException e
) {
546 throw new AssertionError(e
);
548 }).collect(Collectors
.toSet());
549 return commitChange(groupInfoV2
, groupOperations
.createRemoveInvitationChange(uuidCipherTexts
));
552 private Pair
<DecryptedGroup
, GroupChangeResponse
> approveJoinRequest(
553 GroupInfoV2 groupInfoV2
,
555 ) throws IOException
{
556 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
557 return commitChange(groupInfoV2
, groupOperations
.createApproveGroupJoinRequest(uuids
));
560 private Pair
<DecryptedGroup
, GroupChangeResponse
> refuseJoinRequest(
561 GroupInfoV2 groupInfoV2
,
562 Set
<ServiceId
> serviceIds
563 ) throws IOException
{
564 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
565 return commitChange(groupInfoV2
, groupOperations
.createRefuseGroupJoinRequest(serviceIds
, false, List
.of()));
568 private Pair
<DecryptedGroup
, GroupChangeResponse
> ejectMembers(
569 GroupInfoV2 groupInfoV2
,
571 ) throws IOException
{
572 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
573 return commitChange(groupInfoV2
, groupOperations
.createRemoveMembersChange(members
, false, List
.of()));
576 private Pair
<DecryptedGroup
, GroupChangeResponse
> commitChange(
577 GroupInfoV2 groupInfoV2
,
578 GroupChange
.Actions
.Builder change
579 ) throws IOException
{
580 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
581 final var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
582 final var previousGroupState
= groupInfoV2
.getGroup();
583 final var nextRevision
= previousGroupState
.revision
+ 1;
584 final var changeActions
= change
.revision(nextRevision
).build();
585 final DecryptedGroupChange decryptedChange
;
586 final DecryptedGroup decryptedGroupState
;
589 decryptedChange
= groupOperations
.decryptChange(changeActions
, getSelfAci());
590 decryptedGroupState
= DecryptedGroupUtil
.apply(previousGroupState
, decryptedChange
);
591 } catch (VerificationFailedException
| InvalidGroupStateException
| NotAbleToApplyGroupV2ChangeException e
) {
592 throw new IOException(e
);
595 var signedGroupChange
= dependencies
.getGroupsV2Api()
596 .patchGroup(changeActions
, getGroupAuthForToday(groupSecretParams
), Optional
.empty());
598 groupInfoV2
.setGroup(decryptedGroupState
);
600 return new Pair
<>(decryptedGroupState
, signedGroupChange
);
603 private GroupChangeResponse
commitChange(
604 GroupSecretParams groupSecretParams
,
606 GroupChange
.Actions
.Builder change
,
607 GroupLinkPassword password
608 ) throws IOException
{
609 final var nextRevision
= currentRevision
+ 1;
610 final var changeActions
= change
.revision(nextRevision
).build();
612 return dependencies
.getGroupsV2Api()
613 .patchGroup(changeActions
,
614 getGroupAuthForToday(groupSecretParams
),
615 Optional
.ofNullable(password
).map(GroupLinkPassword
::serialize
));
618 Pair
<ServiceId
, ProfileKey
> getAuthoritativeProfileKeyFromChange(final DecryptedGroupChange change
) {
619 UUID editor
= UuidUtil
.fromByteStringOrNull(change
.editorServiceIdBytes
);
620 final var editorProfileKeyBytes
= Stream
.concat(Stream
.of(change
.newMembers
.stream(),
621 change
.promotePendingMembers
.stream(),
622 change
.modifiedProfileKeys
.stream())
623 .flatMap(Function
.identity())
624 .filter(m
-> UuidUtil
.fromByteString(m
.aciBytes
).equals(editor
))
625 .map(m
-> m
.profileKey
),
626 change
.newRequestingMembers
.stream()
627 .filter(m
-> UuidUtil
.fromByteString(m
.aciBytes
).equals(editor
))
628 .map(m
-> m
.profileKey
)).findFirst();
630 if (editorProfileKeyBytes
.isEmpty()) {
634 ProfileKey profileKey
;
636 profileKey
= new ProfileKey(editorProfileKeyBytes
.get().toByteArray());
637 } catch (InvalidInputException e
) {
638 logger
.debug("Bad profile key in group");
642 return new Pair
<>(ACI
.from(editor
), profileKey
);
645 DecryptedGroup
getUpdatedDecryptedGroup(DecryptedGroup group
, DecryptedGroupChange decryptedGroupChange
) {
647 return DecryptedGroupUtil
.apply(group
, decryptedGroupChange
);
648 } catch (NotAbleToApplyGroupV2ChangeException e
) {
653 DecryptedGroupChange
getDecryptedGroupChange(byte[] signedGroupChange
, GroupMasterKey groupMasterKey
) {
654 if (signedGroupChange
!= null) {
655 var groupOperations
= dependencies
.getGroupsV2Operations()
656 .forGroup(GroupSecretParams
.deriveFromMasterKey(groupMasterKey
));
659 return groupOperations
.decryptChange(GroupChange
.ADAPTER
.decode(signedGroupChange
), true).orElse(null);
660 } catch (VerificationFailedException
| InvalidGroupStateException
| IOException e
) {
668 private static long currentDaySeconds() {
669 return TimeUnit
.DAYS
.toSeconds(TimeUnit
.MILLISECONDS
.toDays(System
.currentTimeMillis()));
672 private GroupsV2AuthorizationString
getGroupAuthForToday(
673 final GroupSecretParams groupSecretParams
674 ) throws IOException
{
675 final var todaySeconds
= currentDaySeconds();
676 if (groupApiCredentials
== null || !groupApiCredentials
.containsKey(todaySeconds
)) {
677 // Returns credentials for the next 7 days
678 groupApiCredentials
= dependencies
.getGroupsV2Api()
679 .getCredentials(todaySeconds
)
680 .getAuthCredentialWithPniResponseHashMap();
681 // TODO cache credentials on disk until they expire
684 return getAuthorizationString(groupSecretParams
, todaySeconds
);
685 } catch (VerificationFailedException e
) {
686 logger
.debug("Group api credentials invalid, renewing and trying again.");
687 groupApiCredentials
.clear();
690 groupApiCredentials
= dependencies
.getGroupsV2Api()
691 .getCredentials(todaySeconds
)
692 .getAuthCredentialWithPniResponseHashMap();
694 return getAuthorizationString(groupSecretParams
, todaySeconds
);
695 } catch (VerificationFailedException e
) {
696 throw new IOException(e
);
700 private GroupsV2AuthorizationString
getAuthorizationString(
701 final GroupSecretParams groupSecretParams
,
702 final long todaySeconds
703 ) throws VerificationFailedException
{
704 var authCredentialResponse
= groupApiCredentials
.get(todaySeconds
);
705 final var aci
= getSelfAci();
706 final var pni
= getSelfPni();
707 return dependencies
.getGroupsV2Api()
708 .getGroupsV2AuthorizationString(aci
, pni
, todaySeconds
, groupSecretParams
, authCredentialResponse
);
711 private ACI
getSelfAci() {
712 return context
.getAccount().getAci();
715 private PNI
getSelfPni() {
716 return context
.getAccount().getPni();