]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java
Prevent last admin from leaving group
[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::getLegacyIdentifier)
180 .collect(Collectors.toSet());
181 if (noUuidCapability.size() > 0) {
182 logger.warn("Cannot create a V2 group as some members don't have a UUID: {}",
183 String.join(", ", noUuidCapability));
184 return false;
185 }
186
187 final var noGv2Capability = members.stream()
188 .map(profileProvider::getProfile)
189 .filter(profile -> profile != null && !profile.getCapabilities().contains(Profile.Capability.gv2))
190 .collect(Collectors.toSet());
191 if (noGv2Capability.size() > 0) {
192 logger.warn("Cannot create a V2 group as some members don't support Groups V2: {}",
193 noGv2Capability.stream().map(Profile::getDisplayName).collect(Collectors.joining(", ")));
194 return false;
195 }
196
197 return true;
198 }
199
200 public Pair<DecryptedGroup, GroupChange> updateGroup(
201 GroupInfoV2 groupInfoV2, String name, String description, File avatarFile
202 ) throws IOException {
203 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
204 var groupOperations = groupsV2Operations.forGroup(groupSecretParams);
205
206 var change = name != null ? groupOperations.createModifyGroupTitle(name) : GroupChange.Actions.newBuilder();
207
208 if (description != null) {
209 change.setModifyDescription(groupOperations.createModifyGroupDescription(description));
210 }
211
212 if (avatarFile != null) {
213 final var avatarBytes = readAvatarBytes(avatarFile);
214 var avatarCdnKey = groupsV2Api.uploadAvatar(avatarBytes,
215 groupSecretParams,
216 groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams));
217 change.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder().setAvatar(avatarCdnKey));
218 }
219
220 final var uuid = addressResolver.resolveSignalServiceAddress(this.selfRecipientIdProvider.getSelfRecipientId())
221 .getUuid();
222 if (uuid.isPresent()) {
223 change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
224 }
225
226 return commitChange(groupInfoV2, change);
227 }
228
229 public Pair<DecryptedGroup, GroupChange> addMembers(
230 GroupInfoV2 groupInfoV2, Set<RecipientId> newMembers
231 ) throws IOException {
232 GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
233
234 if (!areMembersValid(newMembers)) {
235 throw new IOException("Failed to update group");
236 }
237
238 var candidates = newMembers.stream()
239 .map(member -> new GroupCandidate(addressResolver.resolveSignalServiceAddress(member).getUuid().get(),
240 Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member))))
241 .collect(Collectors.toSet());
242
243 final var uuid = addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId())
244 .getUuid()
245 .get();
246 final var change = groupOperations.createModifyGroupMembershipChange(candidates, uuid);
247
248 change.setSourceUuid(UuidUtil.toByteString(uuid));
249
250 return commitChange(groupInfoV2, change);
251 }
252
253 public Pair<DecryptedGroup, GroupChange> leaveGroup(
254 GroupInfoV2 groupInfoV2, Set<RecipientId> membersToMakeAdmin
255 ) throws IOException {
256 var pendingMembersList = groupInfoV2.getGroup().getPendingMembersList();
257 final var selfUuid = addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId())
258 .getUuid()
259 .get();
260 var selfPendingMember = DecryptedGroupUtil.findPendingByUuid(pendingMembersList, selfUuid);
261
262 if (selfPendingMember.isPresent()) {
263 return revokeInvites(groupInfoV2, Set.of(selfPendingMember.get()));
264 }
265
266 final var adminUuids = membersToMakeAdmin.stream()
267 .map(addressResolver::resolveSignalServiceAddress)
268 .map(SignalServiceAddress::getUuid)
269 .map(Optional::get)
270 .collect(Collectors.toList());
271 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
272 return commitChange(groupInfoV2, groupOperations.createLeaveAndPromoteMembersToAdmin(selfUuid, adminUuids));
273 }
274
275 public Pair<DecryptedGroup, GroupChange> removeMembers(
276 GroupInfoV2 groupInfoV2, Set<RecipientId> members
277 ) throws IOException {
278 final var memberUuids = members.stream()
279 .map(addressResolver::resolveSignalServiceAddress)
280 .map(SignalServiceAddress::getUuid)
281 .filter(Optional::isPresent)
282 .map(Optional::get)
283 .collect(Collectors.toSet());
284 return ejectMembers(groupInfoV2, memberUuids);
285 }
286
287 public Pair<DecryptedGroup, GroupChange> revokeInvitedMembers(
288 GroupInfoV2 groupInfoV2, Set<RecipientId> members
289 ) throws IOException {
290 var pendingMembersList = groupInfoV2.getGroup().getPendingMembersList();
291 final var memberUuids = members.stream()
292 .map(addressResolver::resolveSignalServiceAddress)
293 .map(SignalServiceAddress::getUuid)
294 .filter(Optional::isPresent)
295 .map(Optional::get)
296 .map(uuid -> DecryptedGroupUtil.findPendingByUuid(pendingMembersList, uuid))
297 .filter(Optional::isPresent)
298 .map(Optional::get)
299 .collect(Collectors.toSet());
300 return revokeInvites(groupInfoV2, memberUuids);
301 }
302
303 public Pair<DecryptedGroup, GroupChange> resetGroupLinkPassword(GroupInfoV2 groupInfoV2) throws IOException {
304 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
305 final var newGroupLinkPassword = GroupLinkPassword.createNew().serialize();
306 final var change = groupOperations.createModifyGroupLinkPasswordChange(newGroupLinkPassword);
307 return commitChange(groupInfoV2, change);
308 }
309
310 public Pair<DecryptedGroup, GroupChange> setGroupLinkState(
311 GroupInfoV2 groupInfoV2, GroupLinkState state
312 ) throws IOException {
313 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
314
315 final var accessRequired = toAccessControl(state);
316 final var requiresNewPassword = state != GroupLinkState.DISABLED && groupInfoV2.getGroup()
317 .getInviteLinkPassword()
318 .isEmpty();
319
320 final var change = requiresNewPassword ? groupOperations.createModifyGroupLinkPasswordAndRightsChange(
321 GroupLinkPassword.createNew().serialize(),
322 accessRequired) : groupOperations.createChangeJoinByLinkRights(accessRequired);
323 return commitChange(groupInfoV2, change);
324 }
325
326 public Pair<DecryptedGroup, GroupChange> setEditDetailsPermission(
327 GroupInfoV2 groupInfoV2, GroupPermission permission
328 ) throws IOException {
329 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
330
331 final var accessRequired = toAccessControl(permission);
332 final var change = groupOperations.createChangeAttributesRights(accessRequired);
333 return commitChange(groupInfoV2, change);
334 }
335
336 public Pair<DecryptedGroup, GroupChange> setAddMemberPermission(
337 GroupInfoV2 groupInfoV2, GroupPermission permission
338 ) throws IOException {
339 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
340
341 final var accessRequired = toAccessControl(permission);
342 final var change = groupOperations.createChangeMembershipRights(accessRequired);
343 return commitChange(groupInfoV2, change);
344 }
345
346 public GroupChange joinGroup(
347 GroupMasterKey groupMasterKey,
348 GroupLinkPassword groupLinkPassword,
349 DecryptedGroupJoinInfo decryptedGroupJoinInfo
350 ) throws IOException {
351 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
352 final var groupOperations = groupsV2Operations.forGroup(groupSecretParams);
353
354 final var selfRecipientId = this.selfRecipientIdProvider.getSelfRecipientId();
355 final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfRecipientId);
356 if (profileKeyCredential == null) {
357 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
358 }
359
360 var requestToJoin = decryptedGroupJoinInfo.getAddFromInviteLink() == AccessControl.AccessRequired.ADMINISTRATOR;
361 var change = requestToJoin
362 ? groupOperations.createGroupJoinRequest(profileKeyCredential)
363 : groupOperations.createGroupJoinDirect(profileKeyCredential);
364
365 change.setSourceUuid(UuidUtil.toByteString(addressResolver.resolveSignalServiceAddress(selfRecipientId)
366 .getUuid()
367 .get()));
368
369 return commitChange(groupSecretParams, decryptedGroupJoinInfo.getRevision(), change, groupLinkPassword);
370 }
371
372 public Pair<DecryptedGroup, GroupChange> acceptInvite(GroupInfoV2 groupInfoV2) throws IOException {
373 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
374
375 final var selfRecipientId = this.selfRecipientIdProvider.getSelfRecipientId();
376 final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfRecipientId);
377 if (profileKeyCredential == null) {
378 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
379 }
380
381 final var change = groupOperations.createAcceptInviteChange(profileKeyCredential);
382
383 final var uuid = addressResolver.resolveSignalServiceAddress(selfRecipientId).getUuid();
384 if (uuid.isPresent()) {
385 change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
386 }
387
388 return commitChange(groupInfoV2, change);
389 }
390
391 public Pair<DecryptedGroup, GroupChange> setMemberAdmin(
392 GroupInfoV2 groupInfoV2, RecipientId recipientId, boolean admin
393 ) throws IOException {
394 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
395 final var address = addressResolver.resolveSignalServiceAddress(recipientId);
396 final var newRole = admin ? Member.Role.ADMINISTRATOR : Member.Role.DEFAULT;
397 final var change = groupOperations.createChangeMemberRole(address.getUuid().get(), newRole);
398 return commitChange(groupInfoV2, change);
399 }
400
401 public Pair<DecryptedGroup, GroupChange> setMessageExpirationTimer(
402 GroupInfoV2 groupInfoV2, int messageExpirationTimer
403 ) throws IOException {
404 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
405 final var change = groupOperations.createModifyGroupTimerChange(messageExpirationTimer);
406 return commitChange(groupInfoV2, change);
407 }
408
409 private AccessControl.AccessRequired toAccessControl(final GroupLinkState state) {
410 switch (state) {
411 case DISABLED:
412 return AccessControl.AccessRequired.UNSATISFIABLE;
413 case ENABLED:
414 return AccessControl.AccessRequired.ANY;
415 case ENABLED_WITH_APPROVAL:
416 return AccessControl.AccessRequired.ADMINISTRATOR;
417 default:
418 throw new AssertionError();
419 }
420 }
421
422 private AccessControl.AccessRequired toAccessControl(final GroupPermission permission) {
423 switch (permission) {
424 case EVERY_MEMBER:
425 return AccessControl.AccessRequired.MEMBER;
426 case ONLY_ADMINS:
427 return AccessControl.AccessRequired.ADMINISTRATOR;
428 default:
429 throw new AssertionError();
430 }
431 }
432
433 private GroupsV2Operations.GroupOperations getGroupOperations(final GroupInfoV2 groupInfoV2) {
434 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
435 return groupsV2Operations.forGroup(groupSecretParams);
436 }
437
438 private Pair<DecryptedGroup, GroupChange> revokeInvites(
439 GroupInfoV2 groupInfoV2, Set<DecryptedPendingMember> pendingMembers
440 ) throws IOException {
441 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
442 final var uuidCipherTexts = pendingMembers.stream().map(member -> {
443 try {
444 return new UuidCiphertext(member.getUuidCipherText().toByteArray());
445 } catch (InvalidInputException e) {
446 throw new AssertionError(e);
447 }
448 }).collect(Collectors.toSet());
449 return commitChange(groupInfoV2, groupOperations.createRemoveInvitationChange(uuidCipherTexts));
450 }
451
452 private Pair<DecryptedGroup, GroupChange> ejectMembers(
453 GroupInfoV2 groupInfoV2, Set<UUID> uuids
454 ) throws IOException {
455 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
456 return commitChange(groupInfoV2, groupOperations.createRemoveMembersChange(uuids));
457 }
458
459 private Pair<DecryptedGroup, GroupChange> commitChange(
460 GroupInfoV2 groupInfoV2, GroupChange.Actions.Builder change
461 ) throws IOException {
462 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
463 final var groupOperations = groupsV2Operations.forGroup(groupSecretParams);
464 final var previousGroupState = groupInfoV2.getGroup();
465 final var nextRevision = previousGroupState.getRevision() + 1;
466 final var changeActions = change.setRevision(nextRevision).build();
467 final DecryptedGroupChange decryptedChange;
468 final DecryptedGroup decryptedGroupState;
469
470 try {
471 decryptedChange = groupOperations.decryptChange(changeActions,
472 addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId())
473 .getUuid()
474 .get());
475 decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange);
476 } catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
477 throw new IOException(e);
478 }
479
480 var signedGroupChange = groupsV2Api.patchGroup(changeActions,
481 groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams),
482 Optional.absent());
483
484 return new Pair<>(decryptedGroupState, signedGroupChange);
485 }
486
487 private GroupChange commitChange(
488 GroupSecretParams groupSecretParams,
489 int currentRevision,
490 GroupChange.Actions.Builder change,
491 GroupLinkPassword password
492 ) throws IOException {
493 final var nextRevision = currentRevision + 1;
494 final var changeActions = change.setRevision(nextRevision).build();
495
496 return groupsV2Api.patchGroup(changeActions,
497 groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams),
498 Optional.fromNullable(password).transform(GroupLinkPassword::serialize));
499 }
500
501 public DecryptedGroup getUpdatedDecryptedGroup(
502 DecryptedGroup group, byte[] signedGroupChange, GroupMasterKey groupMasterKey
503 ) {
504 try {
505 final var decryptedGroupChange = getDecryptedGroupChange(signedGroupChange, groupMasterKey);
506 if (decryptedGroupChange == null) {
507 return null;
508 }
509 return DecryptedGroupUtil.apply(group, decryptedGroupChange);
510 } catch (NotAbleToApplyGroupV2ChangeException e) {
511 return null;
512 }
513 }
514
515 private DecryptedGroupChange getDecryptedGroupChange(byte[] signedGroupChange, GroupMasterKey groupMasterKey) {
516 if (signedGroupChange != null) {
517 var groupOperations = groupsV2Operations.forGroup(GroupSecretParams.deriveFromMasterKey(groupMasterKey));
518
519 try {
520 return groupOperations.decryptChange(GroupChange.parseFrom(signedGroupChange), true).orNull();
521 } catch (VerificationFailedException | InvalidGroupStateException | InvalidProtocolBufferException e) {
522 return null;
523 }
524 }
525
526 return null;
527 }
528 }