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