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
.SignalDependencies
;
7 import org
.asamk
.signal
.manager
.api
.Pair
;
8 import org
.asamk
.signal
.manager
.groups
.GroupLinkPassword
;
9 import org
.asamk
.signal
.manager
.groups
.GroupLinkState
;
10 import org
.asamk
.signal
.manager
.groups
.GroupPermission
;
11 import org
.asamk
.signal
.manager
.groups
.GroupUtils
;
12 import org
.asamk
.signal
.manager
.groups
.NotAGroupMemberException
;
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
.IOUtils
;
16 import org
.asamk
.signal
.manager
.util
.Utils
;
17 import org
.signal
.libsignal
.zkgroup
.InvalidInputException
;
18 import org
.signal
.libsignal
.zkgroup
.VerificationFailedException
;
19 import org
.signal
.libsignal
.zkgroup
.auth
.AuthCredentialResponse
;
20 import org
.signal
.libsignal
.zkgroup
.groups
.GroupMasterKey
;
21 import org
.signal
.libsignal
.zkgroup
.groups
.GroupSecretParams
;
22 import org
.signal
.libsignal
.zkgroup
.groups
.UuidCiphertext
;
23 import org
.signal
.libsignal
.zkgroup
.profiles
.ProfileKey
;
24 import org
.signal
.storageservice
.protos
.groups
.AccessControl
;
25 import org
.signal
.storageservice
.protos
.groups
.GroupChange
;
26 import org
.signal
.storageservice
.protos
.groups
.Member
;
27 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroup
;
28 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroupChange
;
29 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroupJoinInfo
;
30 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedMember
;
31 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedPendingMember
;
32 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedRequestingMember
;
33 import org
.slf4j
.Logger
;
34 import org
.slf4j
.LoggerFactory
;
35 import org
.whispersystems
.signalservice
.api
.groupsv2
.DecryptedGroupUtil
;
36 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupCandidate
;
37 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupHistoryPage
;
38 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupLinkNotActiveException
;
39 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2AuthorizationString
;
40 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2Operations
;
41 import org
.whispersystems
.signalservice
.api
.groupsv2
.InvalidGroupStateException
;
42 import org
.whispersystems
.signalservice
.api
.groupsv2
.NotAbleToApplyGroupV2ChangeException
;
43 import org
.whispersystems
.signalservice
.api
.push
.ACI
;
44 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
;
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
;
50 import java
.io
.FileInputStream
;
51 import java
.io
.IOException
;
52 import java
.io
.InputStream
;
53 import java
.util
.ArrayList
;
54 import java
.util
.Arrays
;
55 import java
.util
.HashMap
;
56 import java
.util
.List
;
57 import java
.util
.Optional
;
59 import java
.util
.UUID
;
60 import java
.util
.concurrent
.TimeUnit
;
61 import java
.util
.function
.Function
;
62 import java
.util
.stream
.Collectors
;
63 import java
.util
.stream
.Stream
;
67 private final static Logger logger
= LoggerFactory
.getLogger(GroupV2Helper
.class);
69 private final SignalDependencies dependencies
;
70 private final Context context
;
72 private HashMap
<Integer
, AuthCredentialResponse
> groupApiCredentials
;
74 GroupV2Helper(final Context context
) {
75 this.dependencies
= context
.getDependencies();
76 this.context
= context
;
79 DecryptedGroup
getDecryptedGroup(final GroupSecretParams groupSecretParams
) throws NotAGroupMemberException
{
81 final var groupsV2AuthorizationString
= getGroupAuthForToday(groupSecretParams
);
82 return dependencies
.getGroupsV2Api().getGroup(groupSecretParams
, groupsV2AuthorizationString
);
83 } catch (NonSuccessfulResponseCodeException e
) {
84 if (e
.getCode() == 403) {
85 throw new NotAGroupMemberException(GroupUtils
.getGroupIdV2(groupSecretParams
), null);
87 logger
.warn("Failed to retrieve Group V2 info, ignoring: {}", e
.getMessage());
89 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
90 logger
.warn("Failed to retrieve Group V2 info, ignoring: {}", e
.getMessage());
95 DecryptedGroupJoinInfo
getDecryptedGroupJoinInfo(
96 GroupMasterKey groupMasterKey
, GroupLinkPassword password
97 ) throws IOException
, GroupLinkNotActiveException
{
98 var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
100 return dependencies
.getGroupsV2Api()
101 .getGroupJoinInfo(groupSecretParams
,
102 Optional
.ofNullable(password
).map(GroupLinkPassword
::serialize
),
103 getGroupAuthForToday(groupSecretParams
));
106 GroupHistoryPage
getDecryptedGroupHistoryPage(
107 final GroupSecretParams groupSecretParams
, int fromRevision
108 ) throws NotAGroupMemberException
{
110 final var groupsV2AuthorizationString
= getGroupAuthForToday(groupSecretParams
);
111 return dependencies
.getGroupsV2Api()
112 .getGroupHistoryPage(groupSecretParams
, fromRevision
, groupsV2AuthorizationString
, false);
113 } catch (NonSuccessfulResponseCodeException e
) {
114 if (e
.getCode() == 403) {
115 throw new NotAGroupMemberException(GroupUtils
.getGroupIdV2(groupSecretParams
), null);
117 logger
.warn("Failed to retrieve Group V2 history, ignoring: {}", e
.getMessage());
119 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
120 logger
.warn("Failed to retrieve Group V2 history, ignoring: {}", e
.getMessage());
125 int findRevisionWeWereAdded(DecryptedGroup partialDecryptedGroup
) {
126 ByteString bytes
= UuidUtil
.toByteString(getSelfAci().uuid());
127 for (DecryptedMember decryptedMember
: partialDecryptedGroup
.getMembersList()) {
128 if (decryptedMember
.getUuid().equals(bytes
)) {
129 return decryptedMember
.getJoinedAtRevision();
132 return partialDecryptedGroup
.getRevision();
135 Pair
<GroupInfoV2
, DecryptedGroup
> createGroup(
136 String name
, Set
<RecipientId
> members
, File avatarFile
137 ) throws IOException
{
138 final var avatarBytes
= readAvatarBytes(avatarFile
);
139 final var newGroup
= buildNewGroup(name
, members
, avatarBytes
);
140 if (newGroup
== null) {
144 final var groupSecretParams
= newGroup
.getGroupSecretParams();
146 final GroupsV2AuthorizationString groupAuthForToday
;
147 final DecryptedGroup decryptedGroup
;
149 groupAuthForToday
= getGroupAuthForToday(groupSecretParams
);
150 dependencies
.getGroupsV2Api().putNewGroup(newGroup
, groupAuthForToday
);
151 decryptedGroup
= dependencies
.getGroupsV2Api().getGroup(groupSecretParams
, groupAuthForToday
);
152 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
153 logger
.warn("Failed to create V2 group: {}", e
.getMessage());
156 if (decryptedGroup
== null) {
157 logger
.warn("Failed to create V2 group, unknown error!");
161 final var groupId
= GroupUtils
.getGroupIdV2(groupSecretParams
);
162 final var masterKey
= groupSecretParams
.getMasterKey();
163 var g
= new GroupInfoV2(groupId
, masterKey
);
165 return new Pair
<>(g
, decryptedGroup
);
168 private byte[] readAvatarBytes(final File avatarFile
) throws IOException
{
169 final byte[] avatarBytes
;
170 try (InputStream avatar
= avatarFile
== null ?
null : new FileInputStream(avatarFile
)) {
171 avatarBytes
= avatar
== null ?
null : IOUtils
.readFully(avatar
);
176 private GroupsV2Operations
.NewGroup
buildNewGroup(
177 String name
, Set
<RecipientId
> members
, byte[] avatar
179 final var profileKeyCredential
= context
.getProfileHelper()
180 .getRecipientProfileKeyCredential(context
.getAccount().getSelfRecipientId());
181 if (profileKeyCredential
== null) {
182 logger
.warn("Cannot create a V2 group as self does not have a versioned profile");
186 final var self
= new GroupCandidate(getSelfAci().uuid(), Optional
.of(profileKeyCredential
));
187 final var memberList
= new ArrayList
<>(members
);
188 final var credentials
= context
.getProfileHelper().getRecipientProfileKeyCredential(memberList
).stream();
189 final var uuids
= memberList
.stream()
190 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId().uuid());
191 var candidates
= Utils
.zip(uuids
,
193 (uuid
, credential
) -> new GroupCandidate(uuid
, Optional
.ofNullable(credential
)))
194 .collect(Collectors
.toSet());
196 final var groupSecretParams
= GroupSecretParams
.generate();
197 return dependencies
.getGroupsV2Operations()
198 .createNewGroup(groupSecretParams
,
200 Optional
.ofNullable(avatar
),
207 Pair
<DecryptedGroup
, GroupChange
> updateGroup(
208 GroupInfoV2 groupInfoV2
, String name
, String description
, File avatarFile
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
) : GroupChange
.Actions
.newBuilder();
215 if (description
!= null) {
216 change
.setModifyDescription(groupOperations
.createModifyGroupDescriptionAction(description
));
219 if (avatarFile
!= null) {
220 final var avatarBytes
= readAvatarBytes(avatarFile
);
221 var avatarCdnKey
= dependencies
.getGroupsV2Api()
222 .uploadAvatar(avatarBytes
, groupSecretParams
, getGroupAuthForToday(groupSecretParams
));
223 change
.setModifyAvatar(GroupChange
.Actions
.ModifyAvatarAction
.newBuilder().setAvatar(avatarCdnKey
));
226 change
.setSourceUuid(getSelfAci().toByteString());
228 return commitChange(groupInfoV2
, change
);
231 Pair
<DecryptedGroup
, GroupChange
> addMembers(
232 GroupInfoV2 groupInfoV2
, 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().getRecipientProfileKeyCredential(memberList
).stream();
238 final var uuids
= memberList
.stream()
239 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId().uuid());
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().uuid())
247 .collect(Collectors
.toSet());
249 final var aci
= getSelfAci();
250 final var change
= groupOperations
.createModifyGroupMembershipChange(candidates
, bannedUuids
, aci
.uuid());
252 change
.setSourceUuid(getSelfAci().toByteString());
254 return commitChange(groupInfoV2
, change
);
257 Pair
<DecryptedGroup
, GroupChange
> leaveGroup(
258 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> membersToMakeAdmin
259 ) throws IOException
{
260 var pendingMembersList
= groupInfoV2
.getGroup().getPendingMembersList();
261 final var selfAci
= getSelfAci();
262 var selfPendingMember
= DecryptedGroupUtil
.findPendingByUuid(pendingMembersList
, selfAci
.uuid());
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
::uuid
)
273 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
274 return commitChange(groupInfoV2
,
275 groupOperations
.createLeaveAndPromoteMembersToAdmin(selfAci
.uuid(), adminUuids
));
278 Pair
<DecryptedGroup
, GroupChange
> removeMembers(
279 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> members
280 ) throws IOException
{
281 final var memberUuids
= members
.stream()
282 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
283 .map(SignalServiceAddress
::getServiceId
)
284 .map(ServiceId
::uuid
)
285 .collect(Collectors
.toSet());
286 return ejectMembers(groupInfoV2
, memberUuids
);
289 Pair
<DecryptedGroup
, GroupChange
> revokeInvitedMembers(
290 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> members
291 ) throws IOException
{
292 var pendingMembersList
= groupInfoV2
.getGroup().getPendingMembersList();
293 final var memberUuids
= members
.stream()
294 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
295 .map(SignalServiceAddress
::getServiceId
)
296 .map(ServiceId
::uuid
)
297 .map(uuid
-> DecryptedGroupUtil
.findPendingByUuid(pendingMembersList
, uuid
))
298 .filter(Optional
::isPresent
)
300 .collect(Collectors
.toSet());
301 return revokeInvites(groupInfoV2
, memberUuids
);
304 Pair
<DecryptedGroup
, GroupChange
> banMembers(
305 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> block
306 ) throws IOException
{
307 GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
309 final var uuids
= block
.stream()
310 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId().uuid())
311 .collect(Collectors
.toSet());
313 final var change
= groupOperations
.createBanUuidsChange(uuids
,
315 groupInfoV2
.getGroup().getBannedMembersList());
317 change
.setSourceUuid(getSelfAci().toByteString());
319 return commitChange(groupInfoV2
, change
);
322 Pair
<DecryptedGroup
, GroupChange
> unbanMembers(
323 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> block
324 ) throws IOException
{
325 GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
327 final var uuids
= block
.stream()
328 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId().uuid())
329 .collect(Collectors
.toSet());
331 final var change
= groupOperations
.createUnbanUuidsChange(uuids
);
333 change
.setSourceUuid(getSelfAci().toByteString());
335 return commitChange(groupInfoV2
, change
);
338 Pair
<DecryptedGroup
, GroupChange
> resetGroupLinkPassword(GroupInfoV2 groupInfoV2
) throws IOException
{
339 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
340 final var newGroupLinkPassword
= GroupLinkPassword
.createNew().serialize();
341 final var change
= groupOperations
.createModifyGroupLinkPasswordChange(newGroupLinkPassword
);
342 return commitChange(groupInfoV2
, change
);
345 Pair
<DecryptedGroup
, GroupChange
> setGroupLinkState(
346 GroupInfoV2 groupInfoV2
, GroupLinkState state
347 ) throws IOException
{
348 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
350 final var accessRequired
= toAccessControl(state
);
351 final var requiresNewPassword
= state
!= GroupLinkState
.DISABLED
&& groupInfoV2
.getGroup()
352 .getInviteLinkPassword()
355 final var change
= requiresNewPassword ? groupOperations
.createModifyGroupLinkPasswordAndRightsChange(
356 GroupLinkPassword
.createNew().serialize(),
357 accessRequired
) : groupOperations
.createChangeJoinByLinkRights(accessRequired
);
358 return commitChange(groupInfoV2
, change
);
361 Pair
<DecryptedGroup
, GroupChange
> setEditDetailsPermission(
362 GroupInfoV2 groupInfoV2
, GroupPermission permission
363 ) throws IOException
{
364 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
366 final var accessRequired
= toAccessControl(permission
);
367 final var change
= groupOperations
.createChangeAttributesRights(accessRequired
);
368 return commitChange(groupInfoV2
, change
);
371 Pair
<DecryptedGroup
, GroupChange
> setAddMemberPermission(
372 GroupInfoV2 groupInfoV2
, GroupPermission permission
373 ) throws IOException
{
374 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
376 final var accessRequired
= toAccessControl(permission
);
377 final var change
= groupOperations
.createChangeMembershipRights(accessRequired
);
378 return commitChange(groupInfoV2
, change
);
381 Pair
<DecryptedGroup
, GroupChange
> updateSelfProfileKey(GroupInfoV2 groupInfoV2
) throws IOException
{
382 Optional
<DecryptedMember
> selfInGroup
= groupInfoV2
.getGroup() == null
384 : DecryptedGroupUtil
.findMemberByUuid(groupInfoV2
.getGroup().getMembersList(), getSelfAci().uuid());
385 if (selfInGroup
.isEmpty()) {
386 logger
.trace("Not updating group, self not in group " + groupInfoV2
.getGroupId().toBase64());
390 final var profileKey
= context
.getAccount().getProfileKey();
391 if (Arrays
.equals(profileKey
.serialize(), selfInGroup
.get().getProfileKey().toByteArray())) {
392 logger
.trace("Not updating group, own Profile Key is already up to date in group "
393 + groupInfoV2
.getGroupId().toBase64());
396 logger
.debug("Updating own profile key in group " + groupInfoV2
.getGroupId().toBase64());
398 final var selfRecipientId
= context
.getAccount().getSelfRecipientId();
399 final var profileKeyCredential
= context
.getProfileHelper().getRecipientProfileKeyCredential(selfRecipientId
);
400 if (profileKeyCredential
== null) {
401 logger
.trace("Cannot update profile key as self does not have a versioned profile");
405 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
406 final var change
= groupOperations
.createUpdateProfileKeyCredentialChange(profileKeyCredential
);
407 change
.setSourceUuid(getSelfAci().toByteString());
408 return commitChange(groupInfoV2
, change
);
411 GroupChange
joinGroup(
412 GroupMasterKey groupMasterKey
,
413 GroupLinkPassword groupLinkPassword
,
414 DecryptedGroupJoinInfo decryptedGroupJoinInfo
415 ) throws IOException
{
416 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
417 final var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
419 final var selfRecipientId
= context
.getAccount().getSelfRecipientId();
420 final var profileKeyCredential
= context
.getProfileHelper().getRecipientProfileKeyCredential(selfRecipientId
);
421 if (profileKeyCredential
== null) {
422 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
425 var requestToJoin
= decryptedGroupJoinInfo
.getAddFromInviteLink() == AccessControl
.AccessRequired
.ADMINISTRATOR
;
426 var change
= requestToJoin
427 ? groupOperations
.createGroupJoinRequest(profileKeyCredential
)
428 : groupOperations
.createGroupJoinDirect(profileKeyCredential
);
430 change
.setSourceUuid(context
.getRecipientHelper()
431 .resolveSignalServiceAddress(selfRecipientId
)
435 return commitChange(groupSecretParams
, decryptedGroupJoinInfo
.getRevision(), change
, groupLinkPassword
);
438 Pair
<DecryptedGroup
, GroupChange
> acceptInvite(GroupInfoV2 groupInfoV2
) throws IOException
{
439 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
441 final var selfRecipientId
= context
.getAccount().getSelfRecipientId();
442 final var profileKeyCredential
= context
.getProfileHelper().getRecipientProfileKeyCredential(selfRecipientId
);
443 if (profileKeyCredential
== null) {
444 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
447 final var change
= groupOperations
.createAcceptInviteChange(profileKeyCredential
);
449 final var aci
= context
.getRecipientHelper().resolveSignalServiceAddress(selfRecipientId
).getServiceId();
450 change
.setSourceUuid(aci
.toByteString());
452 return commitChange(groupInfoV2
, change
);
455 Pair
<DecryptedGroup
, GroupChange
> setMemberAdmin(
456 GroupInfoV2 groupInfoV2
, RecipientId recipientId
, boolean admin
457 ) throws IOException
{
458 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
459 final var address
= context
.getRecipientHelper().resolveSignalServiceAddress(recipientId
);
460 final var newRole
= admin ? Member
.Role
.ADMINISTRATOR
: Member
.Role
.DEFAULT
;
461 final var change
= groupOperations
.createChangeMemberRole(address
.getServiceId().uuid(), newRole
);
462 return commitChange(groupInfoV2
, change
);
465 Pair
<DecryptedGroup
, GroupChange
> setMessageExpirationTimer(
466 GroupInfoV2 groupInfoV2
, int messageExpirationTimer
467 ) throws IOException
{
468 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
469 final var change
= groupOperations
.createModifyGroupTimerChange(messageExpirationTimer
);
470 return commitChange(groupInfoV2
, change
);
473 Pair
<DecryptedGroup
, GroupChange
> setIsAnnouncementGroup(
474 GroupInfoV2 groupInfoV2
, boolean isAnnouncementGroup
475 ) throws IOException
{
476 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
477 final var change
= groupOperations
.createAnnouncementGroupChange(isAnnouncementGroup
);
478 return commitChange(groupInfoV2
, change
);
481 private AccessControl
.AccessRequired
toAccessControl(final GroupLinkState state
) {
482 return switch (state
) {
483 case DISABLED
-> AccessControl
.AccessRequired
.UNSATISFIABLE
;
484 case ENABLED
-> AccessControl
.AccessRequired
.ANY
;
485 case ENABLED_WITH_APPROVAL
-> AccessControl
.AccessRequired
.ADMINISTRATOR
;
489 private AccessControl
.AccessRequired
toAccessControl(final GroupPermission permission
) {
490 return switch (permission
) {
491 case EVERY_MEMBER
-> AccessControl
.AccessRequired
.MEMBER
;
492 case ONLY_ADMINS
-> AccessControl
.AccessRequired
.ADMINISTRATOR
;
496 private GroupsV2Operations
.GroupOperations
getGroupOperations(final GroupInfoV2 groupInfoV2
) {
497 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
498 return dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
501 private Pair
<DecryptedGroup
, GroupChange
> revokeInvites(
502 GroupInfoV2 groupInfoV2
, Set
<DecryptedPendingMember
> pendingMembers
503 ) throws IOException
{
504 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
505 final var uuidCipherTexts
= pendingMembers
.stream().map(member
-> {
507 return new UuidCiphertext(member
.getUuidCipherText().toByteArray());
508 } catch (InvalidInputException e
) {
509 throw new AssertionError(e
);
511 }).collect(Collectors
.toSet());
512 return commitChange(groupInfoV2
, groupOperations
.createRemoveInvitationChange(uuidCipherTexts
));
515 private Pair
<DecryptedGroup
, GroupChange
> ejectMembers(
516 GroupInfoV2 groupInfoV2
, Set
<UUID
> uuids
517 ) throws IOException
{
518 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
519 return commitChange(groupInfoV2
, groupOperations
.createRemoveMembersChange(uuids
, false, List
.of()));
522 private Pair
<DecryptedGroup
, GroupChange
> commitChange(
523 GroupInfoV2 groupInfoV2
, GroupChange
.Actions
.Builder change
524 ) throws IOException
{
525 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
526 final var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
527 final var previousGroupState
= groupInfoV2
.getGroup();
528 final var nextRevision
= previousGroupState
.getRevision() + 1;
529 final var changeActions
= change
.setRevision(nextRevision
).build();
530 final DecryptedGroupChange decryptedChange
;
531 final DecryptedGroup decryptedGroupState
;
534 decryptedChange
= groupOperations
.decryptChange(changeActions
, getSelfAci().uuid());
535 decryptedGroupState
= DecryptedGroupUtil
.apply(previousGroupState
, decryptedChange
);
536 } catch (VerificationFailedException
| InvalidGroupStateException
| NotAbleToApplyGroupV2ChangeException e
) {
537 throw new IOException(e
);
540 var signedGroupChange
= dependencies
.getGroupsV2Api()
541 .patchGroup(changeActions
, getGroupAuthForToday(groupSecretParams
), Optional
.empty());
543 return new Pair
<>(decryptedGroupState
, signedGroupChange
);
546 private GroupChange
commitChange(
547 GroupSecretParams groupSecretParams
,
549 GroupChange
.Actions
.Builder change
,
550 GroupLinkPassword password
551 ) throws IOException
{
552 final var nextRevision
= currentRevision
+ 1;
553 final var changeActions
= change
.setRevision(nextRevision
).build();
555 return dependencies
.getGroupsV2Api()
556 .patchGroup(changeActions
,
557 getGroupAuthForToday(groupSecretParams
),
558 Optional
.ofNullable(password
).map(GroupLinkPassword
::serialize
));
561 Pair
<ServiceId
, ProfileKey
> getAuthoritativeProfileKeyFromChange(final DecryptedGroupChange change
) {
562 UUID editor
= UuidUtil
.fromByteStringOrNull(change
.getEditor());
563 final var editorProfileKeyBytes
= Stream
.concat(Stream
.of(change
.getNewMembersList().stream(),
564 change
.getPromotePendingMembersList().stream(),
565 change
.getModifiedProfileKeysList().stream())
566 .flatMap(Function
.identity())
567 .filter(m
-> UuidUtil
.fromByteString(m
.getUuid()).equals(editor
))
568 .map(DecryptedMember
::getProfileKey
),
569 change
.getNewRequestingMembersList()
571 .filter(m
-> UuidUtil
.fromByteString(m
.getUuid()).equals(editor
))
572 .map(DecryptedRequestingMember
::getProfileKey
)).findFirst();
574 if (editorProfileKeyBytes
.isEmpty()) {
578 ProfileKey profileKey
;
580 profileKey
= new ProfileKey(editorProfileKeyBytes
.get().toByteArray());
581 } catch (InvalidInputException e
) {
582 logger
.debug("Bad profile key in group");
586 return new Pair
<>(ServiceId
.from(editor
), profileKey
);
589 DecryptedGroup
getUpdatedDecryptedGroup(DecryptedGroup group
, DecryptedGroupChange decryptedGroupChange
) {
591 return DecryptedGroupUtil
.apply(group
, decryptedGroupChange
);
592 } catch (NotAbleToApplyGroupV2ChangeException e
) {
597 DecryptedGroupChange
getDecryptedGroupChange(byte[] signedGroupChange
, GroupMasterKey groupMasterKey
) {
598 if (signedGroupChange
!= null) {
599 var groupOperations
= dependencies
.getGroupsV2Operations()
600 .forGroup(GroupSecretParams
.deriveFromMasterKey(groupMasterKey
));
603 return groupOperations
.decryptChange(GroupChange
.parseFrom(signedGroupChange
), true).orElse(null);
604 } catch (VerificationFailedException
| InvalidGroupStateException
| InvalidProtocolBufferException e
) {
612 private static int currentTimeDays() {
613 return (int) TimeUnit
.MILLISECONDS
.toDays(System
.currentTimeMillis());
616 private GroupsV2AuthorizationString
getGroupAuthForToday(
617 final GroupSecretParams groupSecretParams
618 ) throws IOException
{
619 final var today
= currentTimeDays();
620 if (groupApiCredentials
== null || !groupApiCredentials
.containsKey(today
)) {
621 // Returns credentials for the next 7 days
622 final var isAci
= true; // TODO enable group handling with PNI
623 groupApiCredentials
= dependencies
.getGroupsV2Api().getCredentials(today
, isAci
);
624 // TODO cache credentials on disk until they expire
626 var authCredentialResponse
= groupApiCredentials
.get(today
);
627 final var aci
= getSelfAci();
629 return dependencies
.getGroupsV2Api()
630 .getGroupsV2AuthorizationString(aci
, today
, groupSecretParams
, authCredentialResponse
);
631 } catch (VerificationFailedException e
) {
632 throw new IOException(e
);
636 private ACI
getSelfAci() {
637 return context
.getAccount().getAci();