1 package org
.asamk
.signal
.manager
.helper
;
3 import com
.google
.protobuf
.InvalidProtocolBufferException
;
5 import org
.asamk
.signal
.manager
.SignalDependencies
;
6 import org
.asamk
.signal
.manager
.api
.Pair
;
7 import org
.asamk
.signal
.manager
.groups
.GroupLinkPassword
;
8 import org
.asamk
.signal
.manager
.groups
.GroupLinkState
;
9 import org
.asamk
.signal
.manager
.groups
.GroupPermission
;
10 import org
.asamk
.signal
.manager
.groups
.GroupUtils
;
11 import org
.asamk
.signal
.manager
.groups
.NotAGroupMemberException
;
12 import org
.asamk
.signal
.manager
.storage
.groups
.GroupInfoV2
;
13 import org
.asamk
.signal
.manager
.storage
.recipients
.Profile
;
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
.storageservice
.protos
.groups
.AccessControl
;
18 import org
.signal
.storageservice
.protos
.groups
.GroupChange
;
19 import org
.signal
.storageservice
.protos
.groups
.Member
;
20 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroup
;
21 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroupChange
;
22 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroupJoinInfo
;
23 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedPendingMember
;
24 import org
.signal
.zkgroup
.InvalidInputException
;
25 import org
.signal
.zkgroup
.VerificationFailedException
;
26 import org
.signal
.zkgroup
.auth
.AuthCredentialResponse
;
27 import org
.signal
.zkgroup
.groups
.GroupMasterKey
;
28 import org
.signal
.zkgroup
.groups
.GroupSecretParams
;
29 import org
.signal
.zkgroup
.groups
.UuidCiphertext
;
30 import org
.slf4j
.Logger
;
31 import org
.slf4j
.LoggerFactory
;
32 import org
.whispersystems
.libsignal
.util
.guava
.Optional
;
33 import org
.whispersystems
.signalservice
.api
.groupsv2
.DecryptedGroupUtil
;
34 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupCandidate
;
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
.ACI
;
41 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
42 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.NonSuccessfulResponseCodeException
;
45 import java
.io
.FileInputStream
;
46 import java
.io
.IOException
;
47 import java
.io
.InputStream
;
48 import java
.util
.ArrayList
;
49 import java
.util
.HashMap
;
51 import java
.util
.UUID
;
52 import java
.util
.concurrent
.TimeUnit
;
53 import java
.util
.stream
.Collectors
;
55 public class GroupV2Helper
{
57 private final static Logger logger
= LoggerFactory
.getLogger(GroupV2Helper
.class);
59 private final SignalDependencies dependencies
;
60 private final Context context
;
62 private HashMap
<Integer
, AuthCredentialResponse
> groupApiCredentials
;
64 public GroupV2Helper(final Context context
) {
65 this.dependencies
= context
.getDependencies();
66 this.context
= context
;
69 public DecryptedGroup
getDecryptedGroup(final GroupSecretParams groupSecretParams
) throws NotAGroupMemberException
{
71 final var groupsV2AuthorizationString
= getGroupAuthForToday(groupSecretParams
);
72 return dependencies
.getGroupsV2Api().getGroup(groupSecretParams
, groupsV2AuthorizationString
);
73 } catch (NonSuccessfulResponseCodeException e
) {
74 if (e
.getCode() == 403) {
75 throw new NotAGroupMemberException(GroupUtils
.getGroupIdV2(groupSecretParams
), null);
77 logger
.warn("Failed to retrieve Group V2 info, ignoring: {}", e
.getMessage());
79 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
80 logger
.warn("Failed to retrieve Group V2 info, ignoring: {}", e
.getMessage());
85 public DecryptedGroupJoinInfo
getDecryptedGroupJoinInfo(
86 GroupMasterKey groupMasterKey
, GroupLinkPassword password
87 ) throws IOException
, GroupLinkNotActiveException
{
88 var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
90 return dependencies
.getGroupsV2Api()
91 .getGroupJoinInfo(groupSecretParams
,
92 Optional
.fromNullable(password
).transform(GroupLinkPassword
::serialize
),
93 getGroupAuthForToday(groupSecretParams
));
96 public Pair
<GroupInfoV2
, DecryptedGroup
> createGroup(
97 String name
, Set
<RecipientId
> members
, File avatarFile
98 ) throws IOException
{
99 final var avatarBytes
= readAvatarBytes(avatarFile
);
100 final var newGroup
= buildNewGroup(name
, members
, avatarBytes
);
101 if (newGroup
== null) {
105 final var groupSecretParams
= newGroup
.getGroupSecretParams();
107 final GroupsV2AuthorizationString groupAuthForToday
;
108 final DecryptedGroup decryptedGroup
;
110 groupAuthForToday
= getGroupAuthForToday(groupSecretParams
);
111 dependencies
.getGroupsV2Api().putNewGroup(newGroup
, groupAuthForToday
);
112 decryptedGroup
= dependencies
.getGroupsV2Api().getGroup(groupSecretParams
, groupAuthForToday
);
113 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
114 logger
.warn("Failed to create V2 group: {}", e
.getMessage());
117 if (decryptedGroup
== null) {
118 logger
.warn("Failed to create V2 group, unknown error!");
122 final var groupId
= GroupUtils
.getGroupIdV2(groupSecretParams
);
123 final var masterKey
= groupSecretParams
.getMasterKey();
124 var g
= new GroupInfoV2(groupId
, masterKey
);
126 return new Pair
<>(g
, decryptedGroup
);
129 private byte[] readAvatarBytes(final File avatarFile
) throws IOException
{
130 final byte[] avatarBytes
;
131 try (InputStream avatar
= avatarFile
== null ?
null : new FileInputStream(avatarFile
)) {
132 avatarBytes
= avatar
== null ?
null : IOUtils
.readFully(avatar
);
137 private GroupsV2Operations
.NewGroup
buildNewGroup(
138 String name
, Set
<RecipientId
> members
, byte[] avatar
140 final var profileKeyCredential
= context
.getProfileHelper()
141 .getRecipientProfileKeyCredential(context
.getAccount().getSelfRecipientId());
142 if (profileKeyCredential
== null) {
143 logger
.warn("Cannot create a V2 group as self does not have a versioned profile");
147 if (!areMembersValid(members
)) return null;
149 final var self
= new GroupCandidate(getSelfAci().uuid(), Optional
.fromNullable(profileKeyCredential
));
150 final var memberList
= new ArrayList
<>(members
);
151 final var credentials
= context
.getProfileHelper().getRecipientProfileKeyCredential(memberList
).stream();
152 final var uuids
= memberList
.stream()
153 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getAci().uuid());
154 var candidates
= Utils
.zip(uuids
,
156 (uuid
, credential
) -> new GroupCandidate(uuid
, Optional
.fromNullable(credential
)))
157 .collect(Collectors
.toSet());
159 final var groupSecretParams
= GroupSecretParams
.generate();
160 return dependencies
.getGroupsV2Operations()
161 .createNewGroup(groupSecretParams
,
163 Optional
.fromNullable(avatar
),
170 private boolean areMembersValid(final Set
<RecipientId
> members
) {
171 final var noGv2Capability
= context
.getProfileHelper()
172 .getRecipientProfile(new ArrayList
<>(members
))
174 .filter(profile
-> profile
!= null && !profile
.getCapabilities().contains(Profile
.Capability
.gv2
))
175 .collect(Collectors
.toSet());
176 if (noGv2Capability
.size() > 0) {
177 logger
.warn("Cannot create a V2 group as some members don't support Groups V2: {}",
178 noGv2Capability
.stream().map(Profile
::getDisplayName
).collect(Collectors
.joining(", ")));
185 public Pair
<DecryptedGroup
, GroupChange
> updateGroup(
186 GroupInfoV2 groupInfoV2
, String name
, String description
, File avatarFile
187 ) throws IOException
{
188 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
189 var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
191 var change
= name
!= null ? groupOperations
.createModifyGroupTitle(name
) : GroupChange
.Actions
.newBuilder();
193 if (description
!= null) {
194 change
.setModifyDescription(groupOperations
.createModifyGroupDescriptionAction(description
));
197 if (avatarFile
!= null) {
198 final var avatarBytes
= readAvatarBytes(avatarFile
);
199 var avatarCdnKey
= dependencies
.getGroupsV2Api()
200 .uploadAvatar(avatarBytes
, groupSecretParams
, getGroupAuthForToday(groupSecretParams
));
201 change
.setModifyAvatar(GroupChange
.Actions
.ModifyAvatarAction
.newBuilder().setAvatar(avatarCdnKey
));
204 change
.setSourceUuid(getSelfAci().toByteString());
206 return commitChange(groupInfoV2
, change
);
209 public Pair
<DecryptedGroup
, GroupChange
> addMembers(
210 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> newMembers
211 ) throws IOException
{
212 GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
214 if (!areMembersValid(newMembers
)) {
215 throw new IOException("Failed to update group");
218 final var memberList
= new ArrayList
<>(newMembers
);
219 final var credentials
= context
.getProfileHelper().getRecipientProfileKeyCredential(memberList
).stream();
220 final var uuids
= memberList
.stream()
221 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getAci().uuid());
222 var candidates
= Utils
.zip(uuids
,
224 (uuid
, credential
) -> new GroupCandidate(uuid
, Optional
.fromNullable(credential
)))
225 .collect(Collectors
.toSet());
227 final var aci
= getSelfAci();
228 final var change
= groupOperations
.createModifyGroupMembershipChange(candidates
, aci
.uuid());
230 change
.setSourceUuid(getSelfAci().toByteString());
232 return commitChange(groupInfoV2
, change
);
235 public Pair
<DecryptedGroup
, GroupChange
> leaveGroup(
236 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> membersToMakeAdmin
237 ) throws IOException
{
238 var pendingMembersList
= groupInfoV2
.getGroup().getPendingMembersList();
239 final var selfAci
= getSelfAci();
240 var selfPendingMember
= DecryptedGroupUtil
.findPendingByUuid(pendingMembersList
, selfAci
.uuid());
242 if (selfPendingMember
.isPresent()) {
243 return revokeInvites(groupInfoV2
, Set
.of(selfPendingMember
.get()));
246 final var adminUuids
= membersToMakeAdmin
.stream()
247 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
248 .map(SignalServiceAddress
::getAci
)
251 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
252 return commitChange(groupInfoV2
,
253 groupOperations
.createLeaveAndPromoteMembersToAdmin(selfAci
.uuid(), adminUuids
));
256 public Pair
<DecryptedGroup
, GroupChange
> removeMembers(
257 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> members
258 ) throws IOException
{
259 final var memberUuids
= members
.stream()
260 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
261 .map(SignalServiceAddress
::getAci
)
263 .collect(Collectors
.toSet());
264 return ejectMembers(groupInfoV2
, memberUuids
);
267 public Pair
<DecryptedGroup
, GroupChange
> revokeInvitedMembers(
268 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> members
269 ) throws IOException
{
270 var pendingMembersList
= groupInfoV2
.getGroup().getPendingMembersList();
271 final var memberUuids
= members
.stream()
272 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
273 .map(SignalServiceAddress
::getAci
)
275 .map(uuid
-> DecryptedGroupUtil
.findPendingByUuid(pendingMembersList
, uuid
))
276 .filter(Optional
::isPresent
)
278 .collect(Collectors
.toSet());
279 return revokeInvites(groupInfoV2
, memberUuids
);
282 public Pair
<DecryptedGroup
, GroupChange
> resetGroupLinkPassword(GroupInfoV2 groupInfoV2
) throws IOException
{
283 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
284 final var newGroupLinkPassword
= GroupLinkPassword
.createNew().serialize();
285 final var change
= groupOperations
.createModifyGroupLinkPasswordChange(newGroupLinkPassword
);
286 return commitChange(groupInfoV2
, change
);
289 public Pair
<DecryptedGroup
, GroupChange
> setGroupLinkState(
290 GroupInfoV2 groupInfoV2
, GroupLinkState state
291 ) throws IOException
{
292 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
294 final var accessRequired
= toAccessControl(state
);
295 final var requiresNewPassword
= state
!= GroupLinkState
.DISABLED
&& groupInfoV2
.getGroup()
296 .getInviteLinkPassword()
299 final var change
= requiresNewPassword ? groupOperations
.createModifyGroupLinkPasswordAndRightsChange(
300 GroupLinkPassword
.createNew().serialize(),
301 accessRequired
) : groupOperations
.createChangeJoinByLinkRights(accessRequired
);
302 return commitChange(groupInfoV2
, change
);
305 public Pair
<DecryptedGroup
, GroupChange
> setEditDetailsPermission(
306 GroupInfoV2 groupInfoV2
, GroupPermission permission
307 ) throws IOException
{
308 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
310 final var accessRequired
= toAccessControl(permission
);
311 final var change
= groupOperations
.createChangeAttributesRights(accessRequired
);
312 return commitChange(groupInfoV2
, change
);
315 public Pair
<DecryptedGroup
, GroupChange
> setAddMemberPermission(
316 GroupInfoV2 groupInfoV2
, GroupPermission permission
317 ) throws IOException
{
318 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
320 final var accessRequired
= toAccessControl(permission
);
321 final var change
= groupOperations
.createChangeMembershipRights(accessRequired
);
322 return commitChange(groupInfoV2
, change
);
325 public GroupChange
joinGroup(
326 GroupMasterKey groupMasterKey
,
327 GroupLinkPassword groupLinkPassword
,
328 DecryptedGroupJoinInfo decryptedGroupJoinInfo
329 ) throws IOException
{
330 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
331 final var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
333 final var selfRecipientId
= context
.getAccount().getSelfRecipientId();
334 final var profileKeyCredential
= context
.getProfileHelper().getRecipientProfileKeyCredential(selfRecipientId
);
335 if (profileKeyCredential
== null) {
336 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
339 var requestToJoin
= decryptedGroupJoinInfo
.getAddFromInviteLink() == AccessControl
.AccessRequired
.ADMINISTRATOR
;
340 var change
= requestToJoin
341 ? groupOperations
.createGroupJoinRequest(profileKeyCredential
)
342 : groupOperations
.createGroupJoinDirect(profileKeyCredential
);
344 change
.setSourceUuid(context
.getRecipientHelper()
345 .resolveSignalServiceAddress(selfRecipientId
)
349 return commitChange(groupSecretParams
, decryptedGroupJoinInfo
.getRevision(), change
, groupLinkPassword
);
352 public Pair
<DecryptedGroup
, GroupChange
> acceptInvite(GroupInfoV2 groupInfoV2
) throws IOException
{
353 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
355 final var selfRecipientId
= context
.getAccount().getSelfRecipientId();
356 final var profileKeyCredential
= context
.getProfileHelper().getRecipientProfileKeyCredential(selfRecipientId
);
357 if (profileKeyCredential
== null) {
358 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
361 final var change
= groupOperations
.createAcceptInviteChange(profileKeyCredential
);
363 final var aci
= context
.getRecipientHelper().resolveSignalServiceAddress(selfRecipientId
).getAci();
364 change
.setSourceUuid(aci
.toByteString());
366 return commitChange(groupInfoV2
, change
);
369 public Pair
<DecryptedGroup
, GroupChange
> setMemberAdmin(
370 GroupInfoV2 groupInfoV2
, RecipientId recipientId
, boolean admin
371 ) throws IOException
{
372 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
373 final var address
= context
.getRecipientHelper().resolveSignalServiceAddress(recipientId
);
374 final var newRole
= admin ? Member
.Role
.ADMINISTRATOR
: Member
.Role
.DEFAULT
;
375 final var change
= groupOperations
.createChangeMemberRole(address
.getAci().uuid(), newRole
);
376 return commitChange(groupInfoV2
, change
);
379 public Pair
<DecryptedGroup
, GroupChange
> setMessageExpirationTimer(
380 GroupInfoV2 groupInfoV2
, int messageExpirationTimer
381 ) throws IOException
{
382 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
383 final var change
= groupOperations
.createModifyGroupTimerChange(messageExpirationTimer
);
384 return commitChange(groupInfoV2
, change
);
387 public Pair
<DecryptedGroup
, GroupChange
> setIsAnnouncementGroup(
388 GroupInfoV2 groupInfoV2
, boolean isAnnouncementGroup
389 ) throws IOException
{
390 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
391 final var change
= groupOperations
.createAnnouncementGroupChange(isAnnouncementGroup
);
392 return commitChange(groupInfoV2
, change
);
395 private AccessControl
.AccessRequired
toAccessControl(final GroupLinkState state
) {
396 return switch (state
) {
397 case DISABLED
-> AccessControl
.AccessRequired
.UNSATISFIABLE
;
398 case ENABLED
-> AccessControl
.AccessRequired
.ANY
;
399 case ENABLED_WITH_APPROVAL
-> AccessControl
.AccessRequired
.ADMINISTRATOR
;
403 private AccessControl
.AccessRequired
toAccessControl(final GroupPermission permission
) {
404 return switch (permission
) {
405 case EVERY_MEMBER
-> AccessControl
.AccessRequired
.MEMBER
;
406 case ONLY_ADMINS
-> AccessControl
.AccessRequired
.ADMINISTRATOR
;
410 private GroupsV2Operations
.GroupOperations
getGroupOperations(final GroupInfoV2 groupInfoV2
) {
411 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
412 return dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
415 private Pair
<DecryptedGroup
, GroupChange
> revokeInvites(
416 GroupInfoV2 groupInfoV2
, Set
<DecryptedPendingMember
> pendingMembers
417 ) throws IOException
{
418 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
419 final var uuidCipherTexts
= pendingMembers
.stream().map(member
-> {
421 return new UuidCiphertext(member
.getUuidCipherText().toByteArray());
422 } catch (InvalidInputException e
) {
423 throw new AssertionError(e
);
425 }).collect(Collectors
.toSet());
426 return commitChange(groupInfoV2
, groupOperations
.createRemoveInvitationChange(uuidCipherTexts
));
429 private Pair
<DecryptedGroup
, GroupChange
> ejectMembers(
430 GroupInfoV2 groupInfoV2
, Set
<UUID
> uuids
431 ) throws IOException
{
432 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
433 return commitChange(groupInfoV2
, groupOperations
.createRemoveMembersChange(uuids
));
436 private Pair
<DecryptedGroup
, GroupChange
> commitChange(
437 GroupInfoV2 groupInfoV2
, GroupChange
.Actions
.Builder change
438 ) throws IOException
{
439 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
440 final var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
441 final var previousGroupState
= groupInfoV2
.getGroup();
442 final var nextRevision
= previousGroupState
.getRevision() + 1;
443 final var changeActions
= change
.setRevision(nextRevision
).build();
444 final DecryptedGroupChange decryptedChange
;
445 final DecryptedGroup decryptedGroupState
;
448 decryptedChange
= groupOperations
.decryptChange(changeActions
, getSelfAci().uuid());
449 decryptedGroupState
= DecryptedGroupUtil
.apply(previousGroupState
, decryptedChange
);
450 } catch (VerificationFailedException
| InvalidGroupStateException
| NotAbleToApplyGroupV2ChangeException e
) {
451 throw new IOException(e
);
454 var signedGroupChange
= dependencies
.getGroupsV2Api()
455 .patchGroup(changeActions
, getGroupAuthForToday(groupSecretParams
), Optional
.absent());
457 return new Pair
<>(decryptedGroupState
, signedGroupChange
);
460 private GroupChange
commitChange(
461 GroupSecretParams groupSecretParams
,
463 GroupChange
.Actions
.Builder change
,
464 GroupLinkPassword password
465 ) throws IOException
{
466 final var nextRevision
= currentRevision
+ 1;
467 final var changeActions
= change
.setRevision(nextRevision
).build();
469 return dependencies
.getGroupsV2Api()
470 .patchGroup(changeActions
,
471 getGroupAuthForToday(groupSecretParams
),
472 Optional
.fromNullable(password
).transform(GroupLinkPassword
::serialize
));
475 public DecryptedGroup
getUpdatedDecryptedGroup(
476 DecryptedGroup group
, byte[] signedGroupChange
, GroupMasterKey groupMasterKey
479 final var decryptedGroupChange
= getDecryptedGroupChange(signedGroupChange
, groupMasterKey
);
480 if (decryptedGroupChange
== null) {
483 return DecryptedGroupUtil
.apply(group
, decryptedGroupChange
);
484 } catch (NotAbleToApplyGroupV2ChangeException e
) {
489 private DecryptedGroupChange
getDecryptedGroupChange(byte[] signedGroupChange
, GroupMasterKey groupMasterKey
) {
490 if (signedGroupChange
!= null) {
491 var groupOperations
= dependencies
.getGroupsV2Operations()
492 .forGroup(GroupSecretParams
.deriveFromMasterKey(groupMasterKey
));
495 return groupOperations
.decryptChange(GroupChange
.parseFrom(signedGroupChange
), true).orNull();
496 } catch (VerificationFailedException
| InvalidGroupStateException
| InvalidProtocolBufferException e
) {
504 private static int currentTimeDays() {
505 return (int) TimeUnit
.MILLISECONDS
.toDays(System
.currentTimeMillis());
508 private GroupsV2AuthorizationString
getGroupAuthForToday(
509 final GroupSecretParams groupSecretParams
510 ) throws IOException
{
511 final var today
= currentTimeDays();
512 if (groupApiCredentials
== null || !groupApiCredentials
.containsKey(today
)) {
513 // Returns credentials for the next 7 days
514 groupApiCredentials
= dependencies
.getGroupsV2Api().getCredentials(today
);
515 // TODO cache credentials on disk until they expire
517 var authCredentialResponse
= groupApiCredentials
.get(today
);
518 final var aci
= getSelfAci();
520 return dependencies
.getGroupsV2Api()
521 .getGroupsV2AuthorizationString(aci
, today
, groupSecretParams
, authCredentialResponse
);
522 } catch (VerificationFailedException e
) {
523 throw new IOException(e
);
527 private ACI
getSelfAci() {
528 return context
.getAccount().getAci();