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
.AuthCredentialWithPniResponse
;
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
.PNI
;
45 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
;
46 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
47 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.NonSuccessfulResponseCodeException
;
48 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
51 import java
.io
.FileInputStream
;
52 import java
.io
.IOException
;
53 import java
.io
.InputStream
;
54 import java
.util
.ArrayList
;
55 import java
.util
.Arrays
;
56 import java
.util
.HashMap
;
57 import java
.util
.List
;
58 import java
.util
.Optional
;
60 import java
.util
.UUID
;
61 import java
.util
.concurrent
.TimeUnit
;
62 import java
.util
.function
.Function
;
63 import java
.util
.stream
.Collectors
;
64 import java
.util
.stream
.Stream
;
68 private final static Logger logger
= LoggerFactory
.getLogger(GroupV2Helper
.class);
70 private final SignalDependencies dependencies
;
71 private final Context context
;
73 private HashMap
<Long
, AuthCredentialWithPniResponse
> groupApiCredentials
;
75 GroupV2Helper(final Context context
) {
76 this.dependencies
= context
.getDependencies();
77 this.context
= context
;
80 void clearAuthCredentialCache() {
81 groupApiCredentials
= null;
84 DecryptedGroup
getDecryptedGroup(final GroupSecretParams groupSecretParams
) throws NotAGroupMemberException
{
86 final var groupsV2AuthorizationString
= getGroupAuthForToday(groupSecretParams
);
87 return dependencies
.getGroupsV2Api().getGroup(groupSecretParams
, groupsV2AuthorizationString
);
88 } catch (NonSuccessfulResponseCodeException e
) {
89 if (e
.getCode() == 403) {
90 throw new NotAGroupMemberException(GroupUtils
.getGroupIdV2(groupSecretParams
), null);
92 logger
.warn("Failed to retrieve Group V2 info, ignoring: {}", e
.getMessage());
94 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
95 logger
.warn("Failed to retrieve Group V2 info, ignoring: {}", e
.getMessage());
100 DecryptedGroupJoinInfo
getDecryptedGroupJoinInfo(
101 GroupMasterKey groupMasterKey
, GroupLinkPassword password
102 ) throws IOException
, GroupLinkNotActiveException
{
103 var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
105 return dependencies
.getGroupsV2Api()
106 .getGroupJoinInfo(groupSecretParams
,
107 Optional
.ofNullable(password
).map(GroupLinkPassword
::serialize
),
108 getGroupAuthForToday(groupSecretParams
));
111 GroupHistoryPage
getDecryptedGroupHistoryPage(
112 final GroupSecretParams groupSecretParams
, int fromRevision
113 ) throws NotAGroupMemberException
{
115 final var groupsV2AuthorizationString
= getGroupAuthForToday(groupSecretParams
);
116 return dependencies
.getGroupsV2Api()
117 .getGroupHistoryPage(groupSecretParams
, fromRevision
, groupsV2AuthorizationString
, false);
118 } catch (NonSuccessfulResponseCodeException e
) {
119 if (e
.getCode() == 403) {
120 throw new NotAGroupMemberException(GroupUtils
.getGroupIdV2(groupSecretParams
), null);
122 logger
.warn("Failed to retrieve Group V2 history, ignoring: {}", e
.getMessage());
124 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
125 logger
.warn("Failed to retrieve Group V2 history, ignoring: {}", e
.getMessage());
130 int findRevisionWeWereAdded(DecryptedGroup partialDecryptedGroup
) {
131 ByteString bytes
= UuidUtil
.toByteString(getSelfAci().uuid());
132 for (DecryptedMember decryptedMember
: partialDecryptedGroup
.getMembersList()) {
133 if (decryptedMember
.getUuid().equals(bytes
)) {
134 return decryptedMember
.getJoinedAtRevision();
137 return partialDecryptedGroup
.getRevision();
140 Pair
<GroupInfoV2
, DecryptedGroup
> createGroup(
141 String name
, Set
<RecipientId
> members
, File avatarFile
142 ) throws IOException
{
143 final var avatarBytes
= readAvatarBytes(avatarFile
);
144 final var newGroup
= buildNewGroup(name
, members
, avatarBytes
);
145 if (newGroup
== null) {
149 final var groupSecretParams
= newGroup
.getGroupSecretParams();
151 final GroupsV2AuthorizationString groupAuthForToday
;
152 final DecryptedGroup decryptedGroup
;
154 groupAuthForToday
= getGroupAuthForToday(groupSecretParams
);
155 dependencies
.getGroupsV2Api().putNewGroup(newGroup
, groupAuthForToday
);
156 decryptedGroup
= dependencies
.getGroupsV2Api().getGroup(groupSecretParams
, groupAuthForToday
);
157 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
158 logger
.warn("Failed to create V2 group: {}", e
.getMessage());
161 if (decryptedGroup
== null) {
162 logger
.warn("Failed to create V2 group, unknown error!");
166 final var groupId
= GroupUtils
.getGroupIdV2(groupSecretParams
);
167 final var masterKey
= groupSecretParams
.getMasterKey();
168 var g
= new GroupInfoV2(groupId
, masterKey
, context
.getAccount().getRecipientResolver());
170 return new Pair
<>(g
, decryptedGroup
);
173 private byte[] readAvatarBytes(final File avatarFile
) throws IOException
{
174 final byte[] avatarBytes
;
175 try (InputStream avatar
= avatarFile
== null ?
null : new FileInputStream(avatarFile
)) {
176 avatarBytes
= avatar
== null ?
null : IOUtils
.readFully(avatar
);
181 private GroupsV2Operations
.NewGroup
buildNewGroup(
182 String name
, Set
<RecipientId
> members
, byte[] avatar
184 final var profileKeyCredential
= context
.getProfileHelper()
185 .getExpiringProfileKeyCredential(context
.getAccount().getSelfRecipientId());
186 if (profileKeyCredential
== null) {
187 logger
.warn("Cannot create a V2 group as self does not have a versioned profile");
191 final var self
= new GroupCandidate(getSelfAci().uuid(), Optional
.of(profileKeyCredential
));
192 final var memberList
= new ArrayList
<>(members
);
193 final var credentials
= context
.getProfileHelper().getExpiringProfileKeyCredential(memberList
).stream();
194 final var uuids
= memberList
.stream()
195 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId().uuid());
196 var candidates
= Utils
.zip(uuids
,
198 (uuid
, credential
) -> new GroupCandidate(uuid
, Optional
.ofNullable(credential
)))
199 .collect(Collectors
.toSet());
201 final var groupSecretParams
= GroupSecretParams
.generate();
202 return dependencies
.getGroupsV2Operations()
203 .createNewGroup(groupSecretParams
,
205 Optional
.ofNullable(avatar
),
212 Pair
<DecryptedGroup
, GroupChange
> updateGroup(
213 GroupInfoV2 groupInfoV2
, String name
, String description
, File avatarFile
214 ) throws IOException
{
215 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
216 var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
218 var change
= name
!= null ? groupOperations
.createModifyGroupTitle(name
) : GroupChange
.Actions
.newBuilder();
220 if (description
!= null) {
221 change
.setModifyDescription(groupOperations
.createModifyGroupDescriptionAction(description
));
224 if (avatarFile
!= null) {
225 final var avatarBytes
= readAvatarBytes(avatarFile
);
226 var avatarCdnKey
= dependencies
.getGroupsV2Api()
227 .uploadAvatar(avatarBytes
, groupSecretParams
, getGroupAuthForToday(groupSecretParams
));
228 change
.setModifyAvatar(GroupChange
.Actions
.ModifyAvatarAction
.newBuilder().setAvatar(avatarCdnKey
));
231 change
.setSourceUuid(getSelfAci().toByteString());
233 return commitChange(groupInfoV2
, change
);
236 Pair
<DecryptedGroup
, GroupChange
> addMembers(
237 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> newMembers
238 ) throws IOException
{
239 GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
241 final var memberList
= new ArrayList
<>(newMembers
);
242 final var credentials
= context
.getProfileHelper().getExpiringProfileKeyCredential(memberList
).stream();
243 final var uuids
= memberList
.stream()
244 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId().uuid());
245 var candidates
= Utils
.zip(uuids
,
247 (uuid
, credential
) -> new GroupCandidate(uuid
, Optional
.ofNullable(credential
)))
248 .collect(Collectors
.toSet());
249 final var bannedUuids
= groupInfoV2
.getBannedMembers()
251 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId().uuid())
252 .collect(Collectors
.toSet());
254 final var aci
= getSelfAci();
255 final var change
= groupOperations
.createModifyGroupMembershipChange(candidates
, bannedUuids
, aci
.uuid());
257 change
.setSourceUuid(getSelfAci().toByteString());
259 return commitChange(groupInfoV2
, change
);
262 Pair
<DecryptedGroup
, GroupChange
> leaveGroup(
263 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> membersToMakeAdmin
264 ) throws IOException
{
265 var pendingMembersList
= groupInfoV2
.getGroup().getPendingMembersList();
266 final var selfAci
= getSelfAci();
267 var selfPendingMember
= DecryptedGroupUtil
.findPendingByUuid(pendingMembersList
, selfAci
.uuid());
269 if (selfPendingMember
.isPresent()) {
270 return revokeInvites(groupInfoV2
, Set
.of(selfPendingMember
.get()));
273 final var adminUuids
= membersToMakeAdmin
.stream()
274 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
275 .map(SignalServiceAddress
::getServiceId
)
276 .map(ServiceId
::uuid
)
278 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
279 return commitChange(groupInfoV2
,
280 groupOperations
.createLeaveAndPromoteMembersToAdmin(selfAci
.uuid(), adminUuids
));
283 Pair
<DecryptedGroup
, GroupChange
> removeMembers(
284 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> members
285 ) throws IOException
{
286 final var memberUuids
= members
.stream()
287 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
288 .map(SignalServiceAddress
::getServiceId
)
289 .map(ServiceId
::uuid
)
290 .collect(Collectors
.toSet());
291 return ejectMembers(groupInfoV2
, memberUuids
);
294 Pair
<DecryptedGroup
, GroupChange
> approveJoinRequestMembers(
295 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> members
296 ) throws IOException
{
297 final var memberUuids
= members
.stream()
298 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
299 .map(SignalServiceAddress
::getServiceId
)
300 .map(ServiceId
::uuid
)
301 .collect(Collectors
.toSet());
302 return approveJoinRequest(groupInfoV2
, memberUuids
);
305 Pair
<DecryptedGroup
, GroupChange
> refuseJoinRequestMembers(
306 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> members
307 ) throws IOException
{
308 final var memberUuids
= members
.stream()
309 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
310 .map(SignalServiceAddress
::getServiceId
)
311 .map(ServiceId
::uuid
)
312 .collect(Collectors
.toSet());
313 return refuseJoinRequest(groupInfoV2
, memberUuids
);
316 Pair
<DecryptedGroup
, GroupChange
> revokeInvitedMembers(
317 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> members
318 ) throws IOException
{
319 var pendingMembersList
= groupInfoV2
.getGroup().getPendingMembersList();
320 final var memberUuids
= members
.stream()
321 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
322 .map(SignalServiceAddress
::getServiceId
)
323 .map(ServiceId
::uuid
)
324 .map(uuid
-> DecryptedGroupUtil
.findPendingByUuid(pendingMembersList
, uuid
))
325 .filter(Optional
::isPresent
)
327 .collect(Collectors
.toSet());
328 return revokeInvites(groupInfoV2
, memberUuids
);
331 Pair
<DecryptedGroup
, GroupChange
> banMembers(
332 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> block
333 ) throws IOException
{
334 GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
336 final var uuids
= block
.stream()
337 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId().uuid())
338 .collect(Collectors
.toSet());
340 final var change
= groupOperations
.createBanUuidsChange(uuids
,
342 groupInfoV2
.getGroup().getBannedMembersList());
344 change
.setSourceUuid(getSelfAci().toByteString());
346 return commitChange(groupInfoV2
, change
);
349 Pair
<DecryptedGroup
, GroupChange
> unbanMembers(
350 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> block
351 ) throws IOException
{
352 GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
354 final var uuids
= block
.stream()
355 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId().uuid())
356 .collect(Collectors
.toSet());
358 final var change
= groupOperations
.createUnbanUuidsChange(uuids
);
360 change
.setSourceUuid(getSelfAci().toByteString());
362 return commitChange(groupInfoV2
, change
);
365 Pair
<DecryptedGroup
, GroupChange
> 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
, GroupChange
> setGroupLinkState(
373 GroupInfoV2 groupInfoV2
, GroupLinkState state
374 ) throws IOException
{
375 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
377 final var accessRequired
= toAccessControl(state
);
378 final var requiresNewPassword
= state
!= GroupLinkState
.DISABLED
&& groupInfoV2
.getGroup()
379 .getInviteLinkPassword()
382 final var change
= requiresNewPassword ? groupOperations
.createModifyGroupLinkPasswordAndRightsChange(
383 GroupLinkPassword
.createNew().serialize(),
384 accessRequired
) : groupOperations
.createChangeJoinByLinkRights(accessRequired
);
385 return commitChange(groupInfoV2
, change
);
388 Pair
<DecryptedGroup
, GroupChange
> setEditDetailsPermission(
389 GroupInfoV2 groupInfoV2
, 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
, GroupChange
> setAddMemberPermission(
399 GroupInfoV2 groupInfoV2
, GroupPermission permission
400 ) throws IOException
{
401 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
403 final var accessRequired
= toAccessControl(permission
);
404 final var change
= groupOperations
.createChangeMembershipRights(accessRequired
);
405 return commitChange(groupInfoV2
, change
);
408 Pair
<DecryptedGroup
, GroupChange
> updateSelfProfileKey(GroupInfoV2 groupInfoV2
) throws IOException
{
409 Optional
<DecryptedMember
> selfInGroup
= groupInfoV2
.getGroup() == null
411 : DecryptedGroupUtil
.findMemberByUuid(groupInfoV2
.getGroup().getMembersList(), getSelfAci().uuid());
412 if (selfInGroup
.isEmpty()) {
413 logger
.trace("Not updating group, self not in group " + groupInfoV2
.getGroupId().toBase64());
417 final var profileKey
= context
.getAccount().getProfileKey();
418 if (Arrays
.equals(profileKey
.serialize(), selfInGroup
.get().getProfileKey().toByteArray())) {
419 logger
.trace("Not updating group, own Profile Key is already up to date in group "
420 + groupInfoV2
.getGroupId().toBase64());
423 logger
.debug("Updating own profile key in group " + groupInfoV2
.getGroupId().toBase64());
425 final var selfRecipientId
= context
.getAccount().getSelfRecipientId();
426 final var profileKeyCredential
= context
.getProfileHelper().getExpiringProfileKeyCredential(selfRecipientId
);
427 if (profileKeyCredential
== null) {
428 logger
.trace("Cannot update profile key as self does not have a versioned profile");
432 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
433 final var change
= groupOperations
.createUpdateProfileKeyCredentialChange(profileKeyCredential
);
434 change
.setSourceUuid(getSelfAci().toByteString());
435 return commitChange(groupInfoV2
, change
);
438 GroupChange
joinGroup(
439 GroupMasterKey groupMasterKey
,
440 GroupLinkPassword groupLinkPassword
,
441 DecryptedGroupJoinInfo decryptedGroupJoinInfo
442 ) throws IOException
{
443 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
444 final var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
446 final var selfRecipientId
= context
.getAccount().getSelfRecipientId();
447 final var profileKeyCredential
= context
.getProfileHelper().getExpiringProfileKeyCredential(selfRecipientId
);
448 if (profileKeyCredential
== null) {
449 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
452 var requestToJoin
= decryptedGroupJoinInfo
.getAddFromInviteLink() == AccessControl
.AccessRequired
.ADMINISTRATOR
;
453 var change
= requestToJoin
454 ? groupOperations
.createGroupJoinRequest(profileKeyCredential
)
455 : groupOperations
.createGroupJoinDirect(profileKeyCredential
);
457 change
.setSourceUuid(context
.getRecipientHelper()
458 .resolveSignalServiceAddress(selfRecipientId
)
462 return commitChange(groupSecretParams
, decryptedGroupJoinInfo
.getRevision(), change
, groupLinkPassword
);
465 Pair
<DecryptedGroup
, GroupChange
> acceptInvite(GroupInfoV2 groupInfoV2
) throws IOException
{
466 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
468 final var selfRecipientId
= context
.getAccount().getSelfRecipientId();
469 final var profileKeyCredential
= context
.getProfileHelper().getExpiringProfileKeyCredential(selfRecipientId
);
470 if (profileKeyCredential
== null) {
471 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
474 final var change
= groupOperations
.createAcceptInviteChange(profileKeyCredential
);
476 final var aci
= context
.getRecipientHelper().resolveSignalServiceAddress(selfRecipientId
).getServiceId();
477 change
.setSourceUuid(aci
.toByteString());
479 return commitChange(groupInfoV2
, change
);
482 Pair
<DecryptedGroup
, GroupChange
> setMemberAdmin(
483 GroupInfoV2 groupInfoV2
, RecipientId recipientId
, boolean admin
484 ) throws IOException
{
485 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
486 final var address
= context
.getRecipientHelper().resolveSignalServiceAddress(recipientId
);
487 final var newRole
= admin ? Member
.Role
.ADMINISTRATOR
: Member
.Role
.DEFAULT
;
488 final var change
= groupOperations
.createChangeMemberRole(address
.getServiceId().uuid(), newRole
);
489 return commitChange(groupInfoV2
, change
);
492 Pair
<DecryptedGroup
, GroupChange
> setMessageExpirationTimer(
493 GroupInfoV2 groupInfoV2
, int messageExpirationTimer
494 ) throws IOException
{
495 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
496 final var change
= groupOperations
.createModifyGroupTimerChange(messageExpirationTimer
);
497 return commitChange(groupInfoV2
, change
);
500 Pair
<DecryptedGroup
, GroupChange
> setIsAnnouncementGroup(
501 GroupInfoV2 groupInfoV2
, boolean isAnnouncementGroup
502 ) throws IOException
{
503 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
504 final var change
= groupOperations
.createAnnouncementGroupChange(isAnnouncementGroup
);
505 return commitChange(groupInfoV2
, change
);
508 private AccessControl
.AccessRequired
toAccessControl(final GroupLinkState state
) {
509 return switch (state
) {
510 case DISABLED
-> AccessControl
.AccessRequired
.UNSATISFIABLE
;
511 case ENABLED
-> AccessControl
.AccessRequired
.ANY
;
512 case ENABLED_WITH_APPROVAL
-> AccessControl
.AccessRequired
.ADMINISTRATOR
;
516 private AccessControl
.AccessRequired
toAccessControl(final GroupPermission permission
) {
517 return switch (permission
) {
518 case EVERY_MEMBER
-> AccessControl
.AccessRequired
.MEMBER
;
519 case ONLY_ADMINS
-> AccessControl
.AccessRequired
.ADMINISTRATOR
;
523 private GroupsV2Operations
.GroupOperations
getGroupOperations(final GroupInfoV2 groupInfoV2
) {
524 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
525 return dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
528 private Pair
<DecryptedGroup
, GroupChange
> revokeInvites(
529 GroupInfoV2 groupInfoV2
, Set
<DecryptedPendingMember
> pendingMembers
530 ) throws IOException
{
531 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
532 final var uuidCipherTexts
= pendingMembers
.stream().map(member
-> {
534 return new UuidCiphertext(member
.getUuidCipherText().toByteArray());
535 } catch (InvalidInputException e
) {
536 throw new AssertionError(e
);
538 }).collect(Collectors
.toSet());
539 return commitChange(groupInfoV2
, groupOperations
.createRemoveInvitationChange(uuidCipherTexts
));
542 private Pair
<DecryptedGroup
, GroupChange
> approveJoinRequest(
543 GroupInfoV2 groupInfoV2
, Set
<UUID
> uuids
544 ) throws IOException
{
545 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
546 return commitChange(groupInfoV2
, groupOperations
.createApproveGroupJoinRequest(uuids
));
549 private Pair
<DecryptedGroup
, GroupChange
> refuseJoinRequest(
550 GroupInfoV2 groupInfoV2
, Set
<UUID
> uuids
551 ) throws IOException
{
552 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
553 return commitChange(groupInfoV2
, groupOperations
.createRefuseGroupJoinRequest(uuids
, false, List
.of()));
556 private Pair
<DecryptedGroup
, GroupChange
> ejectMembers(
557 GroupInfoV2 groupInfoV2
, Set
<UUID
> uuids
558 ) throws IOException
{
559 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
560 return commitChange(groupInfoV2
, groupOperations
.createRemoveMembersChange(uuids
, false, List
.of()));
563 private Pair
<DecryptedGroup
, GroupChange
> commitChange(
564 GroupInfoV2 groupInfoV2
, GroupChange
.Actions
.Builder change
565 ) throws IOException
{
566 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
567 final var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
568 final var previousGroupState
= groupInfoV2
.getGroup();
569 final var nextRevision
= previousGroupState
.getRevision() + 1;
570 final var changeActions
= change
.setRevision(nextRevision
).build();
571 final DecryptedGroupChange decryptedChange
;
572 final DecryptedGroup decryptedGroupState
;
575 decryptedChange
= groupOperations
.decryptChange(changeActions
, getSelfAci().uuid());
576 decryptedGroupState
= DecryptedGroupUtil
.apply(previousGroupState
, decryptedChange
);
577 } catch (VerificationFailedException
| InvalidGroupStateException
| NotAbleToApplyGroupV2ChangeException e
) {
578 throw new IOException(e
);
581 var signedGroupChange
= dependencies
.getGroupsV2Api()
582 .patchGroup(changeActions
, getGroupAuthForToday(groupSecretParams
), Optional
.empty());
584 return new Pair
<>(decryptedGroupState
, signedGroupChange
);
587 private GroupChange
commitChange(
588 GroupSecretParams groupSecretParams
,
590 GroupChange
.Actions
.Builder change
,
591 GroupLinkPassword password
592 ) throws IOException
{
593 final var nextRevision
= currentRevision
+ 1;
594 final var changeActions
= change
.setRevision(nextRevision
).build();
596 return dependencies
.getGroupsV2Api()
597 .patchGroup(changeActions
,
598 getGroupAuthForToday(groupSecretParams
),
599 Optional
.ofNullable(password
).map(GroupLinkPassword
::serialize
));
602 Pair
<ServiceId
, ProfileKey
> getAuthoritativeProfileKeyFromChange(final DecryptedGroupChange change
) {
603 UUID editor
= UuidUtil
.fromByteStringOrNull(change
.getEditor());
604 final var editorProfileKeyBytes
= Stream
.concat(Stream
.of(change
.getNewMembersList().stream(),
605 change
.getPromotePendingMembersList().stream(),
606 change
.getModifiedProfileKeysList().stream())
607 .flatMap(Function
.identity())
608 .filter(m
-> UuidUtil
.fromByteString(m
.getUuid()).equals(editor
))
609 .map(DecryptedMember
::getProfileKey
),
610 change
.getNewRequestingMembersList()
612 .filter(m
-> UuidUtil
.fromByteString(m
.getUuid()).equals(editor
))
613 .map(DecryptedRequestingMember
::getProfileKey
)).findFirst();
615 if (editorProfileKeyBytes
.isEmpty()) {
619 ProfileKey profileKey
;
621 profileKey
= new ProfileKey(editorProfileKeyBytes
.get().toByteArray());
622 } catch (InvalidInputException e
) {
623 logger
.debug("Bad profile key in group");
627 return new Pair
<>(ServiceId
.from(editor
), profileKey
);
630 DecryptedGroup
getUpdatedDecryptedGroup(DecryptedGroup group
, DecryptedGroupChange decryptedGroupChange
) {
632 return DecryptedGroupUtil
.apply(group
, decryptedGroupChange
);
633 } catch (NotAbleToApplyGroupV2ChangeException e
) {
638 DecryptedGroupChange
getDecryptedGroupChange(byte[] signedGroupChange
, GroupMasterKey groupMasterKey
) {
639 if (signedGroupChange
!= null) {
640 var groupOperations
= dependencies
.getGroupsV2Operations()
641 .forGroup(GroupSecretParams
.deriveFromMasterKey(groupMasterKey
));
644 return groupOperations
.decryptChange(GroupChange
.parseFrom(signedGroupChange
), true).orElse(null);
645 } catch (VerificationFailedException
| InvalidGroupStateException
| InvalidProtocolBufferException e
) {
653 private static long currentDaySeconds() {
654 return TimeUnit
.DAYS
.toSeconds(TimeUnit
.MILLISECONDS
.toDays(System
.currentTimeMillis()));
657 private GroupsV2AuthorizationString
getGroupAuthForToday(
658 final GroupSecretParams groupSecretParams
659 ) throws IOException
{
660 final var todaySeconds
= currentDaySeconds();
661 if (groupApiCredentials
== null || !groupApiCredentials
.containsKey(todaySeconds
)) {
662 // Returns credentials for the next 7 days
663 groupApiCredentials
= dependencies
.getGroupsV2Api().getCredentials(todaySeconds
);
664 // TODO cache credentials on disk until they expire
667 return getAuthorizationString(groupSecretParams
, todaySeconds
);
668 } catch (VerificationFailedException e
) {
669 logger
.debug("Group api credentials invalid, renewing and trying again.");
670 groupApiCredentials
.clear();
673 groupApiCredentials
= dependencies
.getGroupsV2Api().getCredentials(todaySeconds
);
675 return getAuthorizationString(groupSecretParams
, todaySeconds
);
676 } catch (VerificationFailedException e
) {
677 throw new IOException(e
);
681 private GroupsV2AuthorizationString
getAuthorizationString(
682 final GroupSecretParams groupSecretParams
, final long todaySeconds
683 ) throws VerificationFailedException
{
684 var authCredentialResponse
= groupApiCredentials
.get(todaySeconds
);
685 final var aci
= getSelfAci();
686 final var pni
= getSelfPni();
687 return dependencies
.getGroupsV2Api()
688 .getGroupsV2AuthorizationString(aci
, pni
, todaySeconds
, groupSecretParams
, authCredentialResponse
);
691 private ACI
getSelfAci() {
692 return context
.getAccount().getAci();
695 private PNI
getSelfPni() {
696 return context
.getAccount().getPni();