1 package org
.asamk
.signal
.manager
.helper
;
3 import com
.google
.protobuf
.ByteString
;
4 import com
.google
.protobuf
.InvalidProtocolBufferException
;
6 import org
.asamk
.signal
.manager
.api
.GroupLinkState
;
7 import org
.asamk
.signal
.manager
.api
.GroupPermission
;
8 import org
.asamk
.signal
.manager
.api
.NotAGroupMemberException
;
9 import org
.asamk
.signal
.manager
.api
.Pair
;
10 import org
.asamk
.signal
.manager
.groups
.GroupLinkPassword
;
11 import org
.asamk
.signal
.manager
.groups
.GroupUtils
;
12 import org
.asamk
.signal
.manager
.internal
.SignalDependencies
;
13 import org
.asamk
.signal
.manager
.storage
.groups
.GroupInfoV2
;
14 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientId
;
15 import org
.asamk
.signal
.manager
.util
.Utils
;
16 import org
.signal
.libsignal
.zkgroup
.InvalidInputException
;
17 import org
.signal
.libsignal
.zkgroup
.VerificationFailedException
;
18 import org
.signal
.libsignal
.zkgroup
.auth
.AuthCredentialWithPniResponse
;
19 import org
.signal
.libsignal
.zkgroup
.groups
.GroupMasterKey
;
20 import org
.signal
.libsignal
.zkgroup
.groups
.GroupSecretParams
;
21 import org
.signal
.libsignal
.zkgroup
.groups
.UuidCiphertext
;
22 import org
.signal
.libsignal
.zkgroup
.profiles
.ProfileKey
;
23 import org
.signal
.storageservice
.protos
.groups
.AccessControl
;
24 import org
.signal
.storageservice
.protos
.groups
.GroupChange
;
25 import org
.signal
.storageservice
.protos
.groups
.Member
;
26 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroup
;
27 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroupChange
;
28 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroupJoinInfo
;
29 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedMember
;
30 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedPendingMember
;
31 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedRequestingMember
;
32 import org
.slf4j
.Logger
;
33 import org
.slf4j
.LoggerFactory
;
34 import org
.whispersystems
.signalservice
.api
.groupsv2
.DecryptedGroupUtil
;
35 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupCandidate
;
36 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupHistoryPage
;
37 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupLinkNotActiveException
;
38 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2AuthorizationString
;
39 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2Operations
;
40 import org
.whispersystems
.signalservice
.api
.groupsv2
.InvalidGroupStateException
;
41 import org
.whispersystems
.signalservice
.api
.groupsv2
.NotAbleToApplyGroupV2ChangeException
;
42 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
;
43 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
.ACI
;
44 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
.PNI
;
45 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
46 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.NonSuccessfulResponseCodeException
;
47 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
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
;
64 private final static 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 DecryptedGroup
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 e
) {
91 logger
.warn("Failed to retrieve Group V2 info, ignoring: {}", e
.getMessage());
96 DecryptedGroupJoinInfo
getDecryptedGroupJoinInfo(
97 GroupMasterKey groupMasterKey
, GroupLinkPassword password
98 ) throws IOException
, GroupLinkNotActiveException
{
99 var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
101 return dependencies
.getGroupsV2Api()
102 .getGroupJoinInfo(groupSecretParams
,
103 Optional
.ofNullable(password
).map(GroupLinkPassword
::serialize
),
104 getGroupAuthForToday(groupSecretParams
));
107 GroupHistoryPage
getDecryptedGroupHistoryPage(
108 final GroupSecretParams groupSecretParams
, int fromRevision
109 ) throws NotAGroupMemberException
{
111 final var groupsV2AuthorizationString
= getGroupAuthForToday(groupSecretParams
);
112 return dependencies
.getGroupsV2Api()
113 .getGroupHistoryPage(groupSecretParams
, fromRevision
, groupsV2AuthorizationString
, false);
114 } catch (NonSuccessfulResponseCodeException e
) {
115 if (e
.getCode() == 403) {
116 throw new NotAGroupMemberException(GroupUtils
.getGroupIdV2(groupSecretParams
), null);
118 logger
.warn("Failed to retrieve Group V2 history, ignoring: {}", e
.getMessage());
120 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
121 logger
.warn("Failed to retrieve Group V2 history, ignoring: {}", e
.getMessage());
126 int findRevisionWeWereAdded(DecryptedGroup partialDecryptedGroup
) {
127 ByteString aciBytes
= getSelfAci().toByteString();
128 ByteString pniBytes
= getSelfPni().toByteString();
129 for (DecryptedMember decryptedMember
: partialDecryptedGroup
.getMembersList()) {
130 if (decryptedMember
.getAciBytes().equals(aciBytes
) || decryptedMember
.getPniBytes().equals(pniBytes
)) {
131 return decryptedMember
.getJoinedAtRevision();
134 return partialDecryptedGroup
.getRevision();
137 Pair
<GroupInfoV2
, DecryptedGroup
> createGroup(
138 String name
, Set
<RecipientId
> members
, byte[] avatarFile
140 final var newGroup
= buildNewGroup(name
, members
, avatarFile
);
141 if (newGroup
== null) {
145 final var groupSecretParams
= newGroup
.getGroupSecretParams();
147 final GroupsV2AuthorizationString groupAuthForToday
;
148 final DecryptedGroup decryptedGroup
;
150 groupAuthForToday
= getGroupAuthForToday(groupSecretParams
);
151 dependencies
.getGroupsV2Api().putNewGroup(newGroup
, groupAuthForToday
);
152 decryptedGroup
= dependencies
.getGroupsV2Api().getGroup(groupSecretParams
, groupAuthForToday
);
153 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
154 logger
.warn("Failed to create V2 group: {}", e
.getMessage());
157 if (decryptedGroup
== null) {
158 logger
.warn("Failed to create V2 group, unknown error!");
162 final var groupId
= GroupUtils
.getGroupIdV2(groupSecretParams
);
163 final var masterKey
= groupSecretParams
.getMasterKey();
164 var g
= new GroupInfoV2(groupId
, masterKey
, context
.getAccount().getRecipientResolver());
166 return new Pair
<>(g
, decryptedGroup
);
169 private GroupsV2Operations
.NewGroup
buildNewGroup(
170 String name
, Set
<RecipientId
> members
, byte[] avatar
172 final var profileKeyCredential
= context
.getProfileHelper()
173 .getExpiringProfileKeyCredential(context
.getAccount().getSelfRecipientId());
174 if (profileKeyCredential
== null) {
175 logger
.warn("Cannot create a V2 group as self does not have a versioned profile");
179 final var self
= new GroupCandidate(getSelfAci(), Optional
.of(profileKeyCredential
));
180 final var memberList
= new ArrayList
<>(members
);
181 final var credentials
= context
.getProfileHelper().getExpiringProfileKeyCredential(memberList
).stream();
182 final var uuids
= memberList
.stream()
183 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId());
184 var candidates
= Utils
.zip(uuids
,
186 (uuid
, credential
) -> new GroupCandidate(uuid
, Optional
.ofNullable(credential
)))
187 .collect(Collectors
.toSet());
189 final var groupSecretParams
= GroupSecretParams
.generate();
190 return dependencies
.getGroupsV2Operations()
191 .createNewGroup(groupSecretParams
,
193 Optional
.ofNullable(avatar
),
200 Pair
<DecryptedGroup
, GroupChange
> updateGroup(
201 GroupInfoV2 groupInfoV2
, String name
, String description
, byte[] avatarFile
202 ) throws IOException
{
203 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
204 var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
206 var change
= name
!= null ? groupOperations
.createModifyGroupTitle(name
) : GroupChange
.Actions
.newBuilder();
208 if (description
!= null) {
209 change
.setModifyDescription(groupOperations
.createModifyGroupDescriptionAction(description
));
212 if (avatarFile
!= null) {
213 var avatarCdnKey
= dependencies
.getGroupsV2Api()
214 .uploadAvatar(avatarFile
, groupSecretParams
, getGroupAuthForToday(groupSecretParams
));
215 change
.setModifyAvatar(GroupChange
.Actions
.ModifyAvatarAction
.newBuilder().setAvatar(avatarCdnKey
));
218 change
.setSourceServiceId(getSelfAci().toByteString());
220 return commitChange(groupInfoV2
, change
);
223 Pair
<DecryptedGroup
, GroupChange
> addMembers(
224 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> newMembers
225 ) throws IOException
{
226 GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
228 final var memberList
= new ArrayList
<>(newMembers
);
229 final var credentials
= context
.getProfileHelper().getExpiringProfileKeyCredential(memberList
).stream();
230 final var uuids
= memberList
.stream()
231 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId());
232 var candidates
= Utils
.zip(uuids
,
234 (uuid
, credential
) -> new GroupCandidate(uuid
, Optional
.ofNullable(credential
)))
235 .collect(Collectors
.toSet());
236 final var bannedUuids
= groupInfoV2
.getBannedMembers()
238 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId())
239 .collect(Collectors
.toSet());
241 final var aci
= getSelfAci();
242 final var change
= groupOperations
.createModifyGroupMembershipChange(candidates
, bannedUuids
, aci
);
244 change
.setSourceServiceId(getSelfAci().toByteString());
246 return commitChange(groupInfoV2
, change
);
249 Pair
<DecryptedGroup
, GroupChange
> leaveGroup(
250 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> membersToMakeAdmin
251 ) throws IOException
{
252 var pendingMembersList
= groupInfoV2
.getGroup().getPendingMembersList();
253 final var selfAci
= getSelfAci();
254 var selfPendingMember
= DecryptedGroupUtil
.findPendingByServiceId(pendingMembersList
, selfAci
);
256 if (selfPendingMember
.isPresent()) {
257 return revokeInvites(groupInfoV2
, Set
.of(selfPendingMember
.get()));
260 final var adminUuids
= membersToMakeAdmin
.stream()
261 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
262 .map(SignalServiceAddress
::getServiceId
)
263 .map(ServiceId
::getRawUuid
)
265 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
266 return commitChange(groupInfoV2
, groupOperations
.createLeaveAndPromoteMembersToAdmin(selfAci
, adminUuids
));
269 Pair
<DecryptedGroup
, GroupChange
> removeMembers(
270 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> members
271 ) throws IOException
{
272 final var memberUuids
= members
.stream()
273 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
274 .map(SignalServiceAddress
::getServiceId
)
275 .filter(m
-> m
instanceof ACI
)
277 .collect(Collectors
.toSet());
278 return ejectMembers(groupInfoV2
, memberUuids
);
281 Pair
<DecryptedGroup
, GroupChange
> approveJoinRequestMembers(
282 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> members
283 ) throws IOException
{
284 final var memberUuids
= members
.stream()
285 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
286 .map(SignalServiceAddress
::getServiceId
)
287 .map(ServiceId
::getRawUuid
)
288 .collect(Collectors
.toSet());
289 return approveJoinRequest(groupInfoV2
, memberUuids
);
292 Pair
<DecryptedGroup
, GroupChange
> refuseJoinRequestMembers(
293 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> members
294 ) throws IOException
{
295 final var memberUuids
= members
.stream()
296 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
297 .map(SignalServiceAddress
::getServiceId
)
298 .collect(Collectors
.toSet());
299 return refuseJoinRequest(groupInfoV2
, memberUuids
);
302 Pair
<DecryptedGroup
, GroupChange
> revokeInvitedMembers(
303 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> members
304 ) throws IOException
{
305 var pendingMembersList
= groupInfoV2
.getGroup().getPendingMembersList();
306 final var memberUuids
= members
.stream()
307 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
308 .map(SignalServiceAddress
::getServiceId
)
309 .map(uuid
-> DecryptedGroupUtil
.findPendingByServiceId(pendingMembersList
, uuid
))
310 .filter(Optional
::isPresent
)
312 .collect(Collectors
.toSet());
313 return revokeInvites(groupInfoV2
, memberUuids
);
316 Pair
<DecryptedGroup
, GroupChange
> banMembers(
317 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> block
318 ) throws IOException
{
319 GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
321 final var serviceIds
= block
.stream()
322 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId())
323 .collect(Collectors
.toSet());
325 final var change
= groupOperations
.createBanServiceIdsChange(serviceIds
,
327 groupInfoV2
.getGroup().getBannedMembersList());
329 change
.setSourceServiceId(getSelfAci().toByteString());
331 return commitChange(groupInfoV2
, change
);
334 Pair
<DecryptedGroup
, GroupChange
> unbanMembers(
335 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> block
336 ) throws IOException
{
337 GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
339 final var serviceIds
= block
.stream()
340 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId())
341 .collect(Collectors
.toSet());
343 final var change
= groupOperations
.createUnbanServiceIdsChange(serviceIds
);
345 change
.setSourceServiceId(getSelfAci().toByteString());
347 return commitChange(groupInfoV2
, change
);
350 Pair
<DecryptedGroup
, GroupChange
> resetGroupLinkPassword(GroupInfoV2 groupInfoV2
) throws IOException
{
351 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
352 final var newGroupLinkPassword
= GroupLinkPassword
.createNew().serialize();
353 final var change
= groupOperations
.createModifyGroupLinkPasswordChange(newGroupLinkPassword
);
354 return commitChange(groupInfoV2
, change
);
357 Pair
<DecryptedGroup
, GroupChange
> setGroupLinkState(
358 GroupInfoV2 groupInfoV2
, GroupLinkState state
359 ) throws IOException
{
360 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
362 final var accessRequired
= toAccessControl(state
);
363 final var requiresNewPassword
= state
!= GroupLinkState
.DISABLED
&& groupInfoV2
.getGroup()
364 .getInviteLinkPassword()
367 final var change
= requiresNewPassword ? groupOperations
.createModifyGroupLinkPasswordAndRightsChange(
368 GroupLinkPassword
.createNew().serialize(),
369 accessRequired
) : groupOperations
.createChangeJoinByLinkRights(accessRequired
);
370 return commitChange(groupInfoV2
, change
);
373 Pair
<DecryptedGroup
, GroupChange
> setEditDetailsPermission(
374 GroupInfoV2 groupInfoV2
, GroupPermission permission
375 ) throws IOException
{
376 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
378 final var accessRequired
= toAccessControl(permission
);
379 final var change
= groupOperations
.createChangeAttributesRights(accessRequired
);
380 return commitChange(groupInfoV2
, change
);
383 Pair
<DecryptedGroup
, GroupChange
> setAddMemberPermission(
384 GroupInfoV2 groupInfoV2
, GroupPermission permission
385 ) throws IOException
{
386 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
388 final var accessRequired
= toAccessControl(permission
);
389 final var change
= groupOperations
.createChangeMembershipRights(accessRequired
);
390 return commitChange(groupInfoV2
, change
);
393 Pair
<DecryptedGroup
, GroupChange
> updateSelfProfileKey(GroupInfoV2 groupInfoV2
) throws IOException
{
394 Optional
<DecryptedMember
> selfInGroup
= groupInfoV2
.getGroup() == null
396 : DecryptedGroupUtil
.findMemberByAci(groupInfoV2
.getGroup().getMembersList(), getSelfAci());
397 if (selfInGroup
.isEmpty()) {
398 logger
.trace("Not updating group, self not in group " + groupInfoV2
.getGroupId().toBase64());
402 final var profileKey
= context
.getAccount().getProfileKey();
403 if (Arrays
.equals(profileKey
.serialize(), selfInGroup
.get().getProfileKey().toByteArray())) {
404 logger
.trace("Not updating group, own Profile Key is already up to date in group "
405 + groupInfoV2
.getGroupId().toBase64());
408 logger
.debug("Updating own profile key in group " + groupInfoV2
.getGroupId().toBase64());
410 final var selfRecipientId
= context
.getAccount().getSelfRecipientId();
411 final var profileKeyCredential
= context
.getProfileHelper().getExpiringProfileKeyCredential(selfRecipientId
);
412 if (profileKeyCredential
== null) {
413 logger
.trace("Cannot update profile key as self does not have a versioned profile");
417 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
418 final var change
= groupOperations
.createUpdateProfileKeyCredentialChange(profileKeyCredential
);
419 change
.setSourceServiceId(getSelfAci().toByteString());
420 return commitChange(groupInfoV2
, change
);
423 GroupChange
joinGroup(
424 GroupMasterKey groupMasterKey
,
425 GroupLinkPassword groupLinkPassword
,
426 DecryptedGroupJoinInfo decryptedGroupJoinInfo
427 ) throws IOException
{
428 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
429 final var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
431 final var selfRecipientId
= context
.getAccount().getSelfRecipientId();
432 final var profileKeyCredential
= context
.getProfileHelper().getExpiringProfileKeyCredential(selfRecipientId
);
433 if (profileKeyCredential
== null) {
434 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
437 var requestToJoin
= decryptedGroupJoinInfo
.getAddFromInviteLink() == AccessControl
.AccessRequired
.ADMINISTRATOR
;
438 var change
= requestToJoin
439 ? groupOperations
.createGroupJoinRequest(profileKeyCredential
)
440 : groupOperations
.createGroupJoinDirect(profileKeyCredential
);
442 change
.setSourceServiceId(context
.getRecipientHelper()
443 .resolveSignalServiceAddress(selfRecipientId
)
447 return commitChange(groupSecretParams
, decryptedGroupJoinInfo
.getRevision(), change
, groupLinkPassword
);
450 Pair
<DecryptedGroup
, GroupChange
> acceptInvite(GroupInfoV2 groupInfoV2
) throws IOException
{
451 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
453 final var selfRecipientId
= context
.getAccount().getSelfRecipientId();
454 final var profileKeyCredential
= context
.getProfileHelper().getExpiringProfileKeyCredential(selfRecipientId
);
455 if (profileKeyCredential
== null) {
456 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
459 final var change
= groupOperations
.createAcceptInviteChange(profileKeyCredential
);
461 final var aci
= context
.getRecipientHelper().resolveSignalServiceAddress(selfRecipientId
).getServiceId();
462 change
.setSourceServiceId(aci
.toByteString());
464 return commitChange(groupInfoV2
, change
);
467 Pair
<DecryptedGroup
, GroupChange
> setMemberAdmin(
468 GroupInfoV2 groupInfoV2
, RecipientId recipientId
, boolean admin
469 ) throws IOException
{
470 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
471 final var address
= context
.getRecipientHelper().resolveSignalServiceAddress(recipientId
);
472 final var newRole
= admin ? Member
.Role
.ADMINISTRATOR
: Member
.Role
.DEFAULT
;
473 if (address
.getServiceId() instanceof ACI aci
) {
474 final var change
= groupOperations
.createChangeMemberRole(aci
, newRole
);
475 return commitChange(groupInfoV2
, change
);
477 throw new IllegalArgumentException("Can't make a PNI a group admin.");
481 Pair
<DecryptedGroup
, GroupChange
> setMessageExpirationTimer(
482 GroupInfoV2 groupInfoV2
, int messageExpirationTimer
483 ) throws IOException
{
484 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
485 final var change
= groupOperations
.createModifyGroupTimerChange(messageExpirationTimer
);
486 return commitChange(groupInfoV2
, change
);
489 Pair
<DecryptedGroup
, GroupChange
> setIsAnnouncementGroup(
490 GroupInfoV2 groupInfoV2
, boolean isAnnouncementGroup
491 ) throws IOException
{
492 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
493 final var change
= groupOperations
.createAnnouncementGroupChange(isAnnouncementGroup
);
494 return commitChange(groupInfoV2
, change
);
497 private AccessControl
.AccessRequired
toAccessControl(final GroupLinkState state
) {
498 return switch (state
) {
499 case DISABLED
-> AccessControl
.AccessRequired
.UNSATISFIABLE
;
500 case ENABLED
-> AccessControl
.AccessRequired
.ANY
;
501 case ENABLED_WITH_APPROVAL
-> AccessControl
.AccessRequired
.ADMINISTRATOR
;
505 private AccessControl
.AccessRequired
toAccessControl(final GroupPermission permission
) {
506 return switch (permission
) {
507 case EVERY_MEMBER
-> AccessControl
.AccessRequired
.MEMBER
;
508 case ONLY_ADMINS
-> AccessControl
.AccessRequired
.ADMINISTRATOR
;
512 private GroupsV2Operations
.GroupOperations
getGroupOperations(final GroupInfoV2 groupInfoV2
) {
513 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
514 return dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
517 private Pair
<DecryptedGroup
, GroupChange
> revokeInvites(
518 GroupInfoV2 groupInfoV2
, Set
<DecryptedPendingMember
> pendingMembers
519 ) throws IOException
{
520 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
521 final var uuidCipherTexts
= pendingMembers
.stream().map(member
-> {
523 return new UuidCiphertext(member
.getServiceIdCipherText().toByteArray());
524 } catch (InvalidInputException e
) {
525 throw new AssertionError(e
);
527 }).collect(Collectors
.toSet());
528 return commitChange(groupInfoV2
, groupOperations
.createRemoveInvitationChange(uuidCipherTexts
));
531 private Pair
<DecryptedGroup
, GroupChange
> approveJoinRequest(
532 GroupInfoV2 groupInfoV2
, Set
<UUID
> uuids
533 ) throws IOException
{
534 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
535 return commitChange(groupInfoV2
, groupOperations
.createApproveGroupJoinRequest(uuids
));
538 private Pair
<DecryptedGroup
, GroupChange
> refuseJoinRequest(
539 GroupInfoV2 groupInfoV2
, Set
<ServiceId
> serviceIds
540 ) throws IOException
{
541 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
542 return commitChange(groupInfoV2
, groupOperations
.createRefuseGroupJoinRequest(serviceIds
, false, List
.of()));
545 private Pair
<DecryptedGroup
, GroupChange
> ejectMembers(
546 GroupInfoV2 groupInfoV2
, Set
<ACI
> members
547 ) throws IOException
{
548 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
549 return commitChange(groupInfoV2
, groupOperations
.createRemoveMembersChange(members
, false, List
.of()));
552 private Pair
<DecryptedGroup
, GroupChange
> commitChange(
553 GroupInfoV2 groupInfoV2
, GroupChange
.Actions
.Builder change
554 ) throws IOException
{
555 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
556 final var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
557 final var previousGroupState
= groupInfoV2
.getGroup();
558 final var nextRevision
= previousGroupState
.getRevision() + 1;
559 final var changeActions
= change
.setRevision(nextRevision
).build();
560 final DecryptedGroupChange decryptedChange
;
561 final DecryptedGroup decryptedGroupState
;
564 decryptedChange
= groupOperations
.decryptChange(changeActions
, getSelfAci());
565 decryptedGroupState
= DecryptedGroupUtil
.apply(previousGroupState
, decryptedChange
);
566 } catch (VerificationFailedException
| InvalidGroupStateException
| NotAbleToApplyGroupV2ChangeException e
) {
567 throw new IOException(e
);
570 var signedGroupChange
= dependencies
.getGroupsV2Api()
571 .patchGroup(changeActions
, getGroupAuthForToday(groupSecretParams
), Optional
.empty());
573 return new Pair
<>(decryptedGroupState
, signedGroupChange
);
576 private GroupChange
commitChange(
577 GroupSecretParams groupSecretParams
,
579 GroupChange
.Actions
.Builder change
,
580 GroupLinkPassword password
581 ) throws IOException
{
582 final var nextRevision
= currentRevision
+ 1;
583 final var changeActions
= change
.setRevision(nextRevision
).build();
585 return dependencies
.getGroupsV2Api()
586 .patchGroup(changeActions
,
587 getGroupAuthForToday(groupSecretParams
),
588 Optional
.ofNullable(password
).map(GroupLinkPassword
::serialize
));
591 Pair
<ServiceId
, ProfileKey
> getAuthoritativeProfileKeyFromChange(final DecryptedGroupChange change
) {
592 UUID editor
= UuidUtil
.fromByteStringOrNull(change
.getEditorServiceIdBytes());
593 final var editorProfileKeyBytes
= Stream
.concat(Stream
.of(change
.getNewMembersList().stream(),
594 change
.getPromotePendingMembersList().stream(),
595 change
.getModifiedProfileKeysList().stream())
596 .flatMap(Function
.identity())
597 .filter(m
-> UuidUtil
.fromByteString(m
.getAciBytes()).equals(editor
))
598 .map(DecryptedMember
::getProfileKey
),
599 change
.getNewRequestingMembersList()
601 .filter(m
-> UuidUtil
.fromByteString(m
.getAciBytes()).equals(editor
))
602 .map(DecryptedRequestingMember
::getProfileKey
)).findFirst();
604 if (editorProfileKeyBytes
.isEmpty()) {
608 ProfileKey profileKey
;
610 profileKey
= new ProfileKey(editorProfileKeyBytes
.get().toByteArray());
611 } catch (InvalidInputException e
) {
612 logger
.debug("Bad profile key in group");
616 return new Pair
<>(ACI
.from(editor
), profileKey
);
619 DecryptedGroup
getUpdatedDecryptedGroup(DecryptedGroup group
, DecryptedGroupChange decryptedGroupChange
) {
621 return DecryptedGroupUtil
.apply(group
, decryptedGroupChange
);
622 } catch (NotAbleToApplyGroupV2ChangeException e
) {
627 DecryptedGroupChange
getDecryptedGroupChange(byte[] signedGroupChange
, GroupMasterKey groupMasterKey
) {
628 if (signedGroupChange
!= null) {
629 var groupOperations
= dependencies
.getGroupsV2Operations()
630 .forGroup(GroupSecretParams
.deriveFromMasterKey(groupMasterKey
));
633 return groupOperations
.decryptChange(GroupChange
.parseFrom(signedGroupChange
), true).orElse(null);
634 } catch (VerificationFailedException
| InvalidGroupStateException
| InvalidProtocolBufferException e
) {
642 private static long currentDaySeconds() {
643 return TimeUnit
.DAYS
.toSeconds(TimeUnit
.MILLISECONDS
.toDays(System
.currentTimeMillis()));
646 private GroupsV2AuthorizationString
getGroupAuthForToday(
647 final GroupSecretParams groupSecretParams
648 ) throws IOException
{
649 final var todaySeconds
= currentDaySeconds();
650 if (groupApiCredentials
== null || !groupApiCredentials
.containsKey(todaySeconds
)) {
651 // Returns credentials for the next 7 days
652 groupApiCredentials
= dependencies
.getGroupsV2Api()
653 .getCredentials(todaySeconds
)
654 .getAuthCredentialWithPniResponseHashMap();
655 // TODO cache credentials on disk until they expire
658 return getAuthorizationString(groupSecretParams
, todaySeconds
);
659 } catch (VerificationFailedException e
) {
660 logger
.debug("Group api credentials invalid, renewing and trying again.");
661 groupApiCredentials
.clear();
664 groupApiCredentials
= dependencies
.getGroupsV2Api()
665 .getCredentials(todaySeconds
)
666 .getAuthCredentialWithPniResponseHashMap();
668 return getAuthorizationString(groupSecretParams
, todaySeconds
);
669 } catch (VerificationFailedException e
) {
670 throw new IOException(e
);
674 private GroupsV2AuthorizationString
getAuthorizationString(
675 final GroupSecretParams groupSecretParams
, final long todaySeconds
676 ) throws VerificationFailedException
{
677 var authCredentialResponse
= groupApiCredentials
.get(todaySeconds
);
678 final var aci
= getSelfAci();
679 final var pni
= getSelfPni();
680 return dependencies
.getGroupsV2Api()
681 .getGroupsV2AuthorizationString(aci
, pni
, todaySeconds
, groupSecretParams
, authCredentialResponse
);
684 private ACI
getSelfAci() {
685 return context
.getAccount().getAci();
688 private PNI
getSelfPni() {
689 return context
.getAccount().getPni();