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