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