]> 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.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 private AccessControl.AccessRequired toAccessControl(final GroupLinkState state) {
411 switch (state) {
412 case DISABLED:
413 return AccessControl.AccessRequired.UNSATISFIABLE;
414 case ENABLED:
415 return AccessControl.AccessRequired.ANY;
416 case ENABLED_WITH_APPROVAL:
417 return AccessControl.AccessRequired.ADMINISTRATOR;
418 default:
419 throw new AssertionError();
420 }
421 }
422
423 private AccessControl.AccessRequired toAccessControl(final GroupPermission permission) {
424 switch (permission) {
425 case EVERY_MEMBER:
426 return AccessControl.AccessRequired.MEMBER;
427 case ONLY_ADMINS:
428 return AccessControl.AccessRequired.ADMINISTRATOR;
429 default:
430 throw new AssertionError();
431 }
432 }
433
434 private GroupsV2Operations.GroupOperations getGroupOperations(final GroupInfoV2 groupInfoV2) {
435 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
436 return groupsV2Operations.forGroup(groupSecretParams);
437 }
438
439 private Pair<DecryptedGroup, GroupChange> revokeInvites(
440 GroupInfoV2 groupInfoV2, Set<DecryptedPendingMember> pendingMembers
441 ) throws IOException {
442 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
443 final var uuidCipherTexts = pendingMembers.stream().map(member -> {
444 try {
445 return new UuidCiphertext(member.getUuidCipherText().toByteArray());
446 } catch (InvalidInputException e) {
447 throw new AssertionError(e);
448 }
449 }).collect(Collectors.toSet());
450 return commitChange(groupInfoV2, groupOperations.createRemoveInvitationChange(uuidCipherTexts));
451 }
452
453 private Pair<DecryptedGroup, GroupChange> ejectMembers(
454 GroupInfoV2 groupInfoV2, Set<UUID> uuids
455 ) throws IOException {
456 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
457 return commitChange(groupInfoV2, groupOperations.createRemoveMembersChange(uuids));
458 }
459
460 private Pair<DecryptedGroup, GroupChange> commitChange(
461 GroupInfoV2 groupInfoV2, GroupChange.Actions.Builder change
462 ) throws IOException {
463 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
464 final var groupOperations = groupsV2Operations.forGroup(groupSecretParams);
465 final var previousGroupState = groupInfoV2.getGroup();
466 final var nextRevision = previousGroupState.getRevision() + 1;
467 final var changeActions = change.setRevision(nextRevision).build();
468 final DecryptedGroupChange decryptedChange;
469 final DecryptedGroup decryptedGroupState;
470
471 try {
472 decryptedChange = groupOperations.decryptChange(changeActions,
473 addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId())
474 .getUuid()
475 .get());
476 decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange);
477 } catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
478 throw new IOException(e);
479 }
480
481 var signedGroupChange = groupsV2Api.patchGroup(changeActions,
482 groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams),
483 Optional.absent());
484
485 return new Pair<>(decryptedGroupState, signedGroupChange);
486 }
487
488 private GroupChange commitChange(
489 GroupSecretParams groupSecretParams,
490 int currentRevision,
491 GroupChange.Actions.Builder change,
492 GroupLinkPassword password
493 ) throws IOException {
494 final var nextRevision = currentRevision + 1;
495 final var changeActions = change.setRevision(nextRevision).build();
496
497 return groupsV2Api.patchGroup(changeActions,
498 groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams),
499 Optional.fromNullable(password).transform(GroupLinkPassword::serialize));
500 }
501
502 public DecryptedGroup getUpdatedDecryptedGroup(
503 DecryptedGroup group, byte[] signedGroupChange, GroupMasterKey groupMasterKey
504 ) {
505 try {
506 final var decryptedGroupChange = getDecryptedGroupChange(signedGroupChange, groupMasterKey);
507 if (decryptedGroupChange == null) {
508 return null;
509 }
510 return DecryptedGroupUtil.apply(group, decryptedGroupChange);
511 } catch (NotAbleToApplyGroupV2ChangeException e) {
512 return null;
513 }
514 }
515
516 private DecryptedGroupChange getDecryptedGroupChange(byte[] signedGroupChange, GroupMasterKey groupMasterKey) {
517 if (signedGroupChange != null) {
518 var groupOperations = groupsV2Operations.forGroup(GroupSecretParams.deriveFromMasterKey(groupMasterKey));
519
520 try {
521 return groupOperations.decryptChange(GroupChange.parseFrom(signedGroupChange), true).orNull();
522 } catch (VerificationFailedException | InvalidGroupStateException | InvalidProtocolBufferException e) {
523 return null;
524 }
525 }
526
527 return null;
528 }
529 }