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