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