]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java
Implement setting group permissions
[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(GroupInfoV2 groupInfoV2) throws IOException {
254 var pendingMembersList = groupInfoV2.getGroup().getPendingMembersList();
255 final var selfUuid = addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId())
256 .getUuid()
257 .get();
258 var selfPendingMember = DecryptedGroupUtil.findPendingByUuid(pendingMembersList, selfUuid);
259
260 if (selfPendingMember.isPresent()) {
261 return revokeInvites(groupInfoV2, Set.of(selfPendingMember.get()));
262 } else {
263 return ejectMembers(groupInfoV2, Set.of(selfUuid));
264 }
265 }
266
267 public Pair<DecryptedGroup, GroupChange> removeMembers(
268 GroupInfoV2 groupInfoV2, Set<RecipientId> members
269 ) throws IOException {
270 final var memberUuids = members.stream()
271 .map(addressResolver::resolveSignalServiceAddress)
272 .map(SignalServiceAddress::getUuid)
273 .filter(Optional::isPresent)
274 .map(Optional::get)
275 .collect(Collectors.toSet());
276 return ejectMembers(groupInfoV2, memberUuids);
277 }
278
279 public Pair<DecryptedGroup, GroupChange> revokeInvitedMembers(
280 GroupInfoV2 groupInfoV2, Set<RecipientId> members
281 ) throws IOException {
282 var pendingMembersList = groupInfoV2.getGroup().getPendingMembersList();
283 final var memberUuids = members.stream()
284 .map(addressResolver::resolveSignalServiceAddress)
285 .map(SignalServiceAddress::getUuid)
286 .filter(Optional::isPresent)
287 .map(Optional::get)
288 .map(uuid -> DecryptedGroupUtil.findPendingByUuid(pendingMembersList, uuid))
289 .filter(Optional::isPresent)
290 .map(Optional::get)
291 .collect(Collectors.toSet());
292 return revokeInvites(groupInfoV2, memberUuids);
293 }
294
295 public Pair<DecryptedGroup, GroupChange> resetGroupLinkPassword(GroupInfoV2 groupInfoV2) throws IOException {
296 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
297 final var newGroupLinkPassword = GroupLinkPassword.createNew().serialize();
298 final var change = groupOperations.createModifyGroupLinkPasswordChange(newGroupLinkPassword);
299 return commitChange(groupInfoV2, change);
300 }
301
302 public Pair<DecryptedGroup, GroupChange> setGroupLinkState(
303 GroupInfoV2 groupInfoV2, GroupLinkState state
304 ) throws IOException {
305 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
306
307 final var accessRequired = toAccessControl(state);
308 final var requiresNewPassword = state != GroupLinkState.DISABLED && groupInfoV2.getGroup()
309 .getInviteLinkPassword()
310 .isEmpty();
311
312 final var change = requiresNewPassword ? groupOperations.createModifyGroupLinkPasswordAndRightsChange(
313 GroupLinkPassword.createNew().serialize(),
314 accessRequired) : groupOperations.createChangeJoinByLinkRights(accessRequired);
315 return commitChange(groupInfoV2, change);
316 }
317
318 public Pair<DecryptedGroup, GroupChange> setEditDetailsPermission(
319 GroupInfoV2 groupInfoV2, GroupPermission permission
320 ) throws IOException {
321 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
322
323 final var accessRequired = toAccessControl(permission);
324 final var change = groupOperations.createChangeAttributesRights(accessRequired);
325 return commitChange(groupInfoV2, change);
326 }
327
328 public Pair<DecryptedGroup, GroupChange> setAddMemberPermission(
329 GroupInfoV2 groupInfoV2, GroupPermission permission
330 ) throws IOException {
331 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
332
333 final var accessRequired = toAccessControl(permission);
334 final var change = groupOperations.createChangeMembershipRights(accessRequired);
335 return commitChange(groupInfoV2, change);
336 }
337
338 public GroupChange joinGroup(
339 GroupMasterKey groupMasterKey,
340 GroupLinkPassword groupLinkPassword,
341 DecryptedGroupJoinInfo decryptedGroupJoinInfo
342 ) throws IOException {
343 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
344 final var groupOperations = groupsV2Operations.forGroup(groupSecretParams);
345
346 final var selfRecipientId = this.selfRecipientIdProvider.getSelfRecipientId();
347 final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfRecipientId);
348 if (profileKeyCredential == null) {
349 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
350 }
351
352 var requestToJoin = decryptedGroupJoinInfo.getAddFromInviteLink() == AccessControl.AccessRequired.ADMINISTRATOR;
353 var change = requestToJoin
354 ? groupOperations.createGroupJoinRequest(profileKeyCredential)
355 : groupOperations.createGroupJoinDirect(profileKeyCredential);
356
357 change.setSourceUuid(UuidUtil.toByteString(addressResolver.resolveSignalServiceAddress(selfRecipientId)
358 .getUuid()
359 .get()));
360
361 return commitChange(groupSecretParams, decryptedGroupJoinInfo.getRevision(), change, groupLinkPassword);
362 }
363
364 public Pair<DecryptedGroup, GroupChange> acceptInvite(GroupInfoV2 groupInfoV2) throws IOException {
365 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
366
367 final var selfRecipientId = this.selfRecipientIdProvider.getSelfRecipientId();
368 final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfRecipientId);
369 if (profileKeyCredential == null) {
370 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
371 }
372
373 final var change = groupOperations.createAcceptInviteChange(profileKeyCredential);
374
375 final var uuid = addressResolver.resolveSignalServiceAddress(selfRecipientId).getUuid();
376 if (uuid.isPresent()) {
377 change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
378 }
379
380 return commitChange(groupInfoV2, change);
381 }
382
383 public Pair<DecryptedGroup, GroupChange> setMemberAdmin(
384 GroupInfoV2 groupInfoV2, RecipientId recipientId, boolean admin
385 ) throws IOException {
386 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
387 final var address = addressResolver.resolveSignalServiceAddress(recipientId);
388 final var newRole = admin ? Member.Role.ADMINISTRATOR : Member.Role.DEFAULT;
389 final var change = groupOperations.createChangeMemberRole(address.getUuid().get(), newRole);
390 return commitChange(groupInfoV2, change);
391 }
392
393 public Pair<DecryptedGroup, GroupChange> setMessageExpirationTimer(
394 GroupInfoV2 groupInfoV2, int messageExpirationTimer
395 ) throws IOException {
396 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
397 final var change = groupOperations.createModifyGroupTimerChange(messageExpirationTimer);
398 return commitChange(groupInfoV2, change);
399 }
400
401 private AccessControl.AccessRequired toAccessControl(final GroupLinkState state) {
402 switch (state) {
403 case DISABLED:
404 return AccessControl.AccessRequired.UNSATISFIABLE;
405 case ENABLED:
406 return AccessControl.AccessRequired.ANY;
407 case ENABLED_WITH_APPROVAL:
408 return AccessControl.AccessRequired.ADMINISTRATOR;
409 default:
410 throw new AssertionError();
411 }
412 }
413
414 private AccessControl.AccessRequired toAccessControl(final GroupPermission permission) {
415 switch (permission) {
416 case EVERY_MEMBER:
417 return AccessControl.AccessRequired.MEMBER;
418 case ONLY_ADMINS:
419 return AccessControl.AccessRequired.ADMINISTRATOR;
420 default:
421 throw new AssertionError();
422 }
423 }
424
425 private GroupsV2Operations.GroupOperations getGroupOperations(final GroupInfoV2 groupInfoV2) {
426 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
427 return groupsV2Operations.forGroup(groupSecretParams);
428 }
429
430 private Pair<DecryptedGroup, GroupChange> revokeInvites(
431 GroupInfoV2 groupInfoV2, Set<DecryptedPendingMember> pendingMembers
432 ) throws IOException {
433 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
434 final var uuidCipherTexts = pendingMembers.stream().map(member -> {
435 try {
436 return new UuidCiphertext(member.getUuidCipherText().toByteArray());
437 } catch (InvalidInputException e) {
438 throw new AssertionError(e);
439 }
440 }).collect(Collectors.toSet());
441 return commitChange(groupInfoV2, groupOperations.createRemoveInvitationChange(uuidCipherTexts));
442 }
443
444 private Pair<DecryptedGroup, GroupChange> ejectMembers(
445 GroupInfoV2 groupInfoV2, Set<UUID> uuids
446 ) throws IOException {
447 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
448 return commitChange(groupInfoV2, groupOperations.createRemoveMembersChange(uuids));
449 }
450
451 private Pair<DecryptedGroup, GroupChange> commitChange(
452 GroupInfoV2 groupInfoV2, GroupChange.Actions.Builder change
453 ) throws IOException {
454 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
455 final var groupOperations = groupsV2Operations.forGroup(groupSecretParams);
456 final var previousGroupState = groupInfoV2.getGroup();
457 final var nextRevision = previousGroupState.getRevision() + 1;
458 final var changeActions = change.setRevision(nextRevision).build();
459 final DecryptedGroupChange decryptedChange;
460 final DecryptedGroup decryptedGroupState;
461
462 try {
463 decryptedChange = groupOperations.decryptChange(changeActions,
464 addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId())
465 .getUuid()
466 .get());
467 decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange);
468 } catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
469 throw new IOException(e);
470 }
471
472 var signedGroupChange = groupsV2Api.patchGroup(changeActions,
473 groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams),
474 Optional.absent());
475
476 return new Pair<>(decryptedGroupState, signedGroupChange);
477 }
478
479 private GroupChange commitChange(
480 GroupSecretParams groupSecretParams,
481 int currentRevision,
482 GroupChange.Actions.Builder change,
483 GroupLinkPassword password
484 ) throws IOException {
485 final var nextRevision = currentRevision + 1;
486 final var changeActions = change.setRevision(nextRevision).build();
487
488 return groupsV2Api.patchGroup(changeActions,
489 groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams),
490 Optional.fromNullable(password).transform(GroupLinkPassword::serialize));
491 }
492
493 public 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 = groupsV2Operations.forGroup(GroupSecretParams.deriveFromMasterKey(groupMasterKey));
510
511 try {
512 return groupOperations.decryptChange(GroupChange.parseFrom(signedGroupChange), true).orNull();
513 } catch (VerificationFailedException | InvalidGroupStateException | InvalidProtocolBufferException e) {
514 return null;
515 }
516 }
517
518 return null;
519 }
520 }