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