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