]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java
a40c62234b89fd584dbe2463653c84ba324fa0e2
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / helper / GroupV2Helper.java
1 package org.asamk.signal.manager.helper;
2
3 import com.google.protobuf.InvalidProtocolBufferException;
4
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.ServiceId;
42 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
43 import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
44
45 import java.io.File;
46 import java.io.FileInputStream;
47 import java.io.IOException;
48 import java.io.InputStream;
49 import java.util.ArrayList;
50 import java.util.HashMap;
51 import java.util.Set;
52 import java.util.UUID;
53 import java.util.concurrent.TimeUnit;
54 import java.util.stream.Collectors;
55
56 class GroupV2Helper {
57
58 private final static Logger logger = LoggerFactory.getLogger(GroupV2Helper.class);
59
60 private final SignalDependencies dependencies;
61 private final Context context;
62
63 private HashMap<Integer, AuthCredentialResponse> groupApiCredentials;
64
65 GroupV2Helper(final Context context) {
66 this.dependencies = context.getDependencies();
67 this.context = context;
68 }
69
70 DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) throws NotAGroupMemberException {
71 try {
72 final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams);
73 return dependencies.getGroupsV2Api().getGroup(groupSecretParams, groupsV2AuthorizationString);
74 } catch (NonSuccessfulResponseCodeException e) {
75 if (e.getCode() == 403) {
76 throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
77 }
78 logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage());
79 return null;
80 } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
81 logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage());
82 return null;
83 }
84 }
85
86 DecryptedGroupJoinInfo getDecryptedGroupJoinInfo(
87 GroupMasterKey groupMasterKey, GroupLinkPassword password
88 ) throws IOException, GroupLinkNotActiveException {
89 var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
90
91 return dependencies.getGroupsV2Api()
92 .getGroupJoinInfo(groupSecretParams,
93 Optional.fromNullable(password).transform(GroupLinkPassword::serialize),
94 getGroupAuthForToday(groupSecretParams));
95 }
96
97 Pair<GroupInfoV2, DecryptedGroup> createGroup(
98 String name, Set<RecipientId> members, File avatarFile
99 ) throws IOException {
100 final var avatarBytes = readAvatarBytes(avatarFile);
101 final var newGroup = buildNewGroup(name, members, avatarBytes);
102 if (newGroup == null) {
103 return null;
104 }
105
106 final var groupSecretParams = newGroup.getGroupSecretParams();
107
108 final GroupsV2AuthorizationString groupAuthForToday;
109 final DecryptedGroup decryptedGroup;
110 try {
111 groupAuthForToday = getGroupAuthForToday(groupSecretParams);
112 dependencies.getGroupsV2Api().putNewGroup(newGroup, groupAuthForToday);
113 decryptedGroup = dependencies.getGroupsV2Api().getGroup(groupSecretParams, groupAuthForToday);
114 } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
115 logger.warn("Failed to create V2 group: {}", e.getMessage());
116 return null;
117 }
118 if (decryptedGroup == null) {
119 logger.warn("Failed to create V2 group, unknown error!");
120 return null;
121 }
122
123 final var groupId = GroupUtils.getGroupIdV2(groupSecretParams);
124 final var masterKey = groupSecretParams.getMasterKey();
125 var g = new GroupInfoV2(groupId, masterKey);
126
127 return new Pair<>(g, decryptedGroup);
128 }
129
130 private byte[] readAvatarBytes(final File avatarFile) throws IOException {
131 final byte[] avatarBytes;
132 try (InputStream avatar = avatarFile == null ? null : new FileInputStream(avatarFile)) {
133 avatarBytes = avatar == null ? null : IOUtils.readFully(avatar);
134 }
135 return avatarBytes;
136 }
137
138 private GroupsV2Operations.NewGroup buildNewGroup(
139 String name, Set<RecipientId> members, byte[] avatar
140 ) {
141 final var profileKeyCredential = context.getProfileHelper()
142 .getRecipientProfileKeyCredential(context.getAccount().getSelfRecipientId());
143 if (profileKeyCredential == null) {
144 logger.warn("Cannot create a V2 group as self does not have a versioned profile");
145 return null;
146 }
147
148 if (!areMembersValid(members)) return null;
149
150 final var self = new GroupCandidate(getSelfAci().uuid(), Optional.fromNullable(profileKeyCredential));
151 final var memberList = new ArrayList<>(members);
152 final var credentials = context.getProfileHelper().getRecipientProfileKeyCredential(memberList).stream();
153 final var uuids = memberList.stream()
154 .map(member -> context.getRecipientHelper().resolveSignalServiceAddress(member).getServiceId().uuid());
155 var candidates = Utils.zip(uuids,
156 credentials,
157 (uuid, credential) -> new GroupCandidate(uuid, Optional.fromNullable(credential)))
158 .collect(Collectors.toSet());
159
160 final var groupSecretParams = GroupSecretParams.generate();
161 return dependencies.getGroupsV2Operations()
162 .createNewGroup(groupSecretParams,
163 name,
164 Optional.fromNullable(avatar),
165 self,
166 candidates,
167 Member.Role.DEFAULT,
168 0);
169 }
170
171 private boolean areMembersValid(final Set<RecipientId> members) {
172 final var noGv2Capability = context.getProfileHelper()
173 .getRecipientProfile(new ArrayList<>(members))
174 .stream()
175 .filter(profile -> profile != null && !profile.getCapabilities().contains(Profile.Capability.gv2))
176 .collect(Collectors.toSet());
177 if (noGv2Capability.size() > 0) {
178 logger.warn("Cannot create a V2 group as some members don't support Groups V2: {}",
179 noGv2Capability.stream().map(Profile::getDisplayName).collect(Collectors.joining(", ")));
180 return false;
181 }
182
183 return true;
184 }
185
186 Pair<DecryptedGroup, GroupChange> updateGroup(
187 GroupInfoV2 groupInfoV2, String name, String description, File avatarFile
188 ) throws IOException {
189 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
190 var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
191
192 var change = name != null ? groupOperations.createModifyGroupTitle(name) : GroupChange.Actions.newBuilder();
193
194 if (description != null) {
195 change.setModifyDescription(groupOperations.createModifyGroupDescriptionAction(description));
196 }
197
198 if (avatarFile != null) {
199 final var avatarBytes = readAvatarBytes(avatarFile);
200 var avatarCdnKey = dependencies.getGroupsV2Api()
201 .uploadAvatar(avatarBytes, groupSecretParams, getGroupAuthForToday(groupSecretParams));
202 change.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder().setAvatar(avatarCdnKey));
203 }
204
205 change.setSourceUuid(getSelfAci().toByteString());
206
207 return commitChange(groupInfoV2, change);
208 }
209
210 Pair<DecryptedGroup, GroupChange> addMembers(
211 GroupInfoV2 groupInfoV2, Set<RecipientId> newMembers
212 ) throws IOException {
213 GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
214
215 if (!areMembersValid(newMembers)) {
216 throw new IOException("Failed to update group");
217 }
218
219 final var memberList = new ArrayList<>(newMembers);
220 final var credentials = context.getProfileHelper().getRecipientProfileKeyCredential(memberList).stream();
221 final var uuids = memberList.stream()
222 .map(member -> context.getRecipientHelper().resolveSignalServiceAddress(member).getServiceId().uuid());
223 var candidates = Utils.zip(uuids,
224 credentials,
225 (uuid, credential) -> new GroupCandidate(uuid, Optional.fromNullable(credential)))
226 .collect(Collectors.toSet());
227
228 final var aci = getSelfAci();
229 final var change = groupOperations.createModifyGroupMembershipChange(candidates, aci.uuid());
230
231 change.setSourceUuid(getSelfAci().toByteString());
232
233 return commitChange(groupInfoV2, change);
234 }
235
236 Pair<DecryptedGroup, GroupChange> leaveGroup(
237 GroupInfoV2 groupInfoV2, Set<RecipientId> membersToMakeAdmin
238 ) throws IOException {
239 var pendingMembersList = groupInfoV2.getGroup().getPendingMembersList();
240 final var selfAci = getSelfAci();
241 var selfPendingMember = DecryptedGroupUtil.findPendingByUuid(pendingMembersList, selfAci.uuid());
242
243 if (selfPendingMember.isPresent()) {
244 return revokeInvites(groupInfoV2, Set.of(selfPendingMember.get()));
245 }
246
247 final var adminUuids = membersToMakeAdmin.stream()
248 .map(context.getRecipientHelper()::resolveSignalServiceAddress)
249 .map(SignalServiceAddress::getServiceId)
250 .map(ServiceId::uuid)
251 .toList();
252 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
253 return commitChange(groupInfoV2,
254 groupOperations.createLeaveAndPromoteMembersToAdmin(selfAci.uuid(), adminUuids));
255 }
256
257 Pair<DecryptedGroup, GroupChange> removeMembers(
258 GroupInfoV2 groupInfoV2, Set<RecipientId> members
259 ) throws IOException {
260 final var memberUuids = members.stream()
261 .map(context.getRecipientHelper()::resolveSignalServiceAddress)
262 .map(SignalServiceAddress::getServiceId)
263 .map(ServiceId::uuid)
264 .collect(Collectors.toSet());
265 return ejectMembers(groupInfoV2, memberUuids);
266 }
267
268 Pair<DecryptedGroup, GroupChange> revokeInvitedMembers(
269 GroupInfoV2 groupInfoV2, Set<RecipientId> members
270 ) throws IOException {
271 var pendingMembersList = groupInfoV2.getGroup().getPendingMembersList();
272 final var memberUuids = members.stream()
273 .map(context.getRecipientHelper()::resolveSignalServiceAddress)
274 .map(SignalServiceAddress::getServiceId)
275 .map(ServiceId::uuid)
276 .map(uuid -> DecryptedGroupUtil.findPendingByUuid(pendingMembersList, uuid))
277 .filter(Optional::isPresent)
278 .map(Optional::get)
279 .collect(Collectors.toSet());
280 return revokeInvites(groupInfoV2, memberUuids);
281 }
282
283 Pair<DecryptedGroup, GroupChange> resetGroupLinkPassword(GroupInfoV2 groupInfoV2) throws IOException {
284 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
285 final var newGroupLinkPassword = GroupLinkPassword.createNew().serialize();
286 final var change = groupOperations.createModifyGroupLinkPasswordChange(newGroupLinkPassword);
287 return commitChange(groupInfoV2, change);
288 }
289
290 Pair<DecryptedGroup, GroupChange> setGroupLinkState(
291 GroupInfoV2 groupInfoV2, GroupLinkState state
292 ) throws IOException {
293 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
294
295 final var accessRequired = toAccessControl(state);
296 final var requiresNewPassword = state != GroupLinkState.DISABLED && groupInfoV2.getGroup()
297 .getInviteLinkPassword()
298 .isEmpty();
299
300 final var change = requiresNewPassword ? groupOperations.createModifyGroupLinkPasswordAndRightsChange(
301 GroupLinkPassword.createNew().serialize(),
302 accessRequired) : groupOperations.createChangeJoinByLinkRights(accessRequired);
303 return commitChange(groupInfoV2, change);
304 }
305
306 Pair<DecryptedGroup, GroupChange> setEditDetailsPermission(
307 GroupInfoV2 groupInfoV2, GroupPermission permission
308 ) throws IOException {
309 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
310
311 final var accessRequired = toAccessControl(permission);
312 final var change = groupOperations.createChangeAttributesRights(accessRequired);
313 return commitChange(groupInfoV2, change);
314 }
315
316 Pair<DecryptedGroup, GroupChange> setAddMemberPermission(
317 GroupInfoV2 groupInfoV2, GroupPermission permission
318 ) throws IOException {
319 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
320
321 final var accessRequired = toAccessControl(permission);
322 final var change = groupOperations.createChangeMembershipRights(accessRequired);
323 return commitChange(groupInfoV2, change);
324 }
325
326 GroupChange joinGroup(
327 GroupMasterKey groupMasterKey,
328 GroupLinkPassword groupLinkPassword,
329 DecryptedGroupJoinInfo decryptedGroupJoinInfo
330 ) throws IOException {
331 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
332 final var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
333
334 final var selfRecipientId = context.getAccount().getSelfRecipientId();
335 final var profileKeyCredential = context.getProfileHelper().getRecipientProfileKeyCredential(selfRecipientId);
336 if (profileKeyCredential == null) {
337 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
338 }
339
340 var requestToJoin = decryptedGroupJoinInfo.getAddFromInviteLink() == AccessControl.AccessRequired.ADMINISTRATOR;
341 var change = requestToJoin
342 ? groupOperations.createGroupJoinRequest(profileKeyCredential)
343 : groupOperations.createGroupJoinDirect(profileKeyCredential);
344
345 change.setSourceUuid(context.getRecipientHelper()
346 .resolveSignalServiceAddress(selfRecipientId)
347 .getServiceId()
348 .toByteString());
349
350 return commitChange(groupSecretParams, decryptedGroupJoinInfo.getRevision(), change, groupLinkPassword);
351 }
352
353 Pair<DecryptedGroup, GroupChange> acceptInvite(GroupInfoV2 groupInfoV2) throws IOException {
354 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
355
356 final var selfRecipientId = context.getAccount().getSelfRecipientId();
357 final var profileKeyCredential = context.getProfileHelper().getRecipientProfileKeyCredential(selfRecipientId);
358 if (profileKeyCredential == null) {
359 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
360 }
361
362 final var change = groupOperations.createAcceptInviteChange(profileKeyCredential);
363
364 final var aci = context.getRecipientHelper().resolveSignalServiceAddress(selfRecipientId).getServiceId();
365 change.setSourceUuid(aci.toByteString());
366
367 return commitChange(groupInfoV2, change);
368 }
369
370 Pair<DecryptedGroup, GroupChange> setMemberAdmin(
371 GroupInfoV2 groupInfoV2, RecipientId recipientId, boolean admin
372 ) throws IOException {
373 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
374 final var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
375 final var newRole = admin ? Member.Role.ADMINISTRATOR : Member.Role.DEFAULT;
376 final var change = groupOperations.createChangeMemberRole(address.getServiceId().uuid(), newRole);
377 return commitChange(groupInfoV2, change);
378 }
379
380 Pair<DecryptedGroup, GroupChange> setMessageExpirationTimer(
381 GroupInfoV2 groupInfoV2, int messageExpirationTimer
382 ) throws IOException {
383 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
384 final var change = groupOperations.createModifyGroupTimerChange(messageExpirationTimer);
385 return commitChange(groupInfoV2, change);
386 }
387
388 Pair<DecryptedGroup, GroupChange> setIsAnnouncementGroup(
389 GroupInfoV2 groupInfoV2, boolean isAnnouncementGroup
390 ) throws IOException {
391 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
392 final var change = groupOperations.createAnnouncementGroupChange(isAnnouncementGroup);
393 return commitChange(groupInfoV2, change);
394 }
395
396 private AccessControl.AccessRequired toAccessControl(final GroupLinkState state) {
397 return switch (state) {
398 case DISABLED -> AccessControl.AccessRequired.UNSATISFIABLE;
399 case ENABLED -> AccessControl.AccessRequired.ANY;
400 case ENABLED_WITH_APPROVAL -> AccessControl.AccessRequired.ADMINISTRATOR;
401 };
402 }
403
404 private AccessControl.AccessRequired toAccessControl(final GroupPermission permission) {
405 return switch (permission) {
406 case EVERY_MEMBER -> AccessControl.AccessRequired.MEMBER;
407 case ONLY_ADMINS -> AccessControl.AccessRequired.ADMINISTRATOR;
408 };
409 }
410
411 private GroupsV2Operations.GroupOperations getGroupOperations(final GroupInfoV2 groupInfoV2) {
412 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
413 return dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
414 }
415
416 private Pair<DecryptedGroup, GroupChange> revokeInvites(
417 GroupInfoV2 groupInfoV2, Set<DecryptedPendingMember> pendingMembers
418 ) throws IOException {
419 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
420 final var uuidCipherTexts = pendingMembers.stream().map(member -> {
421 try {
422 return new UuidCiphertext(member.getUuidCipherText().toByteArray());
423 } catch (InvalidInputException e) {
424 throw new AssertionError(e);
425 }
426 }).collect(Collectors.toSet());
427 return commitChange(groupInfoV2, groupOperations.createRemoveInvitationChange(uuidCipherTexts));
428 }
429
430 private Pair<DecryptedGroup, GroupChange> ejectMembers(
431 GroupInfoV2 groupInfoV2, Set<UUID> uuids
432 ) throws IOException {
433 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
434 return commitChange(groupInfoV2, groupOperations.createRemoveMembersChange(uuids));
435 }
436
437 private Pair<DecryptedGroup, GroupChange> commitChange(
438 GroupInfoV2 groupInfoV2, GroupChange.Actions.Builder change
439 ) throws IOException {
440 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
441 final var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
442 final var previousGroupState = groupInfoV2.getGroup();
443 final var nextRevision = previousGroupState.getRevision() + 1;
444 final var changeActions = change.setRevision(nextRevision).build();
445 final DecryptedGroupChange decryptedChange;
446 final DecryptedGroup decryptedGroupState;
447
448 try {
449 decryptedChange = groupOperations.decryptChange(changeActions, getSelfAci().uuid());
450 decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange);
451 } catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
452 throw new IOException(e);
453 }
454
455 var signedGroupChange = dependencies.getGroupsV2Api()
456 .patchGroup(changeActions, getGroupAuthForToday(groupSecretParams), Optional.absent());
457
458 return new Pair<>(decryptedGroupState, signedGroupChange);
459 }
460
461 private GroupChange commitChange(
462 GroupSecretParams groupSecretParams,
463 int currentRevision,
464 GroupChange.Actions.Builder change,
465 GroupLinkPassword password
466 ) throws IOException {
467 final var nextRevision = currentRevision + 1;
468 final var changeActions = change.setRevision(nextRevision).build();
469
470 return dependencies.getGroupsV2Api()
471 .patchGroup(changeActions,
472 getGroupAuthForToday(groupSecretParams),
473 Optional.fromNullable(password).transform(GroupLinkPassword::serialize));
474 }
475
476 DecryptedGroup getUpdatedDecryptedGroup(
477 DecryptedGroup group, byte[] signedGroupChange, GroupMasterKey groupMasterKey
478 ) {
479 try {
480 final var decryptedGroupChange = getDecryptedGroupChange(signedGroupChange, groupMasterKey);
481 if (decryptedGroupChange == null) {
482 return null;
483 }
484 return DecryptedGroupUtil.apply(group, decryptedGroupChange);
485 } catch (NotAbleToApplyGroupV2ChangeException e) {
486 return null;
487 }
488 }
489
490 private DecryptedGroupChange getDecryptedGroupChange(byte[] signedGroupChange, GroupMasterKey groupMasterKey) {
491 if (signedGroupChange != null) {
492 var groupOperations = dependencies.getGroupsV2Operations()
493 .forGroup(GroupSecretParams.deriveFromMasterKey(groupMasterKey));
494
495 try {
496 return groupOperations.decryptChange(GroupChange.parseFrom(signedGroupChange), true).orNull();
497 } catch (VerificationFailedException | InvalidGroupStateException | InvalidProtocolBufferException e) {
498 return null;
499 }
500 }
501
502 return null;
503 }
504
505 private static int currentTimeDays() {
506 return (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis());
507 }
508
509 private GroupsV2AuthorizationString getGroupAuthForToday(
510 final GroupSecretParams groupSecretParams
511 ) throws IOException {
512 final var today = currentTimeDays();
513 if (groupApiCredentials == null || !groupApiCredentials.containsKey(today)) {
514 // Returns credentials for the next 7 days
515 groupApiCredentials = dependencies.getGroupsV2Api().getCredentials(today);
516 // TODO cache credentials on disk until they expire
517 }
518 var authCredentialResponse = groupApiCredentials.get(today);
519 final var aci = getSelfAci();
520 try {
521 return dependencies.getGroupsV2Api()
522 .getGroupsV2AuthorizationString(aci, today, groupSecretParams, authCredentialResponse);
523 } catch (VerificationFailedException e) {
524 throw new IOException(e);
525 }
526 }
527
528 private ACI getSelfAci() {
529 return context.getAccount().getAci();
530 }
531 }