]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/manager/helper/GroupHelper.java
Implement join group via invitation link
[signal-cli] / src / main / java / org / asamk / signal / manager / helper / GroupHelper.java
1 package org.asamk.signal.manager.helper;
2
3 import com.google.protobuf.InvalidProtocolBufferException;
4
5 import org.asamk.signal.manager.GroupLinkPassword;
6 import org.asamk.signal.storage.groups.GroupInfoV2;
7 import org.asamk.signal.util.IOUtils;
8 import org.signal.storageservice.protos.groups.AccessControl;
9 import org.signal.storageservice.protos.groups.GroupChange;
10 import org.signal.storageservice.protos.groups.Member;
11 import org.signal.storageservice.protos.groups.local.DecryptedGroup;
12 import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
13 import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
14 import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
15 import org.signal.zkgroup.InvalidInputException;
16 import org.signal.zkgroup.VerificationFailedException;
17 import org.signal.zkgroup.groups.GroupMasterKey;
18 import org.signal.zkgroup.groups.GroupSecretParams;
19 import org.signal.zkgroup.groups.UuidCiphertext;
20 import org.signal.zkgroup.profiles.ProfileKeyCredential;
21 import org.whispersystems.libsignal.util.Pair;
22 import org.whispersystems.libsignal.util.guava.Optional;
23 import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
24 import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
25 import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
26 import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
27 import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
28 import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
29 import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
30 import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException;
31 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
32 import org.whispersystems.signalservice.api.util.UuidUtil;
33
34 import java.io.FileInputStream;
35 import java.io.IOException;
36 import java.io.InputStream;
37 import java.util.Collection;
38 import java.util.List;
39 import java.util.Set;
40 import java.util.UUID;
41 import java.util.stream.Collectors;
42
43 public class GroupHelper {
44
45 private final ProfileKeyCredentialProvider profileKeyCredentialProvider;
46
47 private final ProfileProvider profileProvider;
48
49 private final SelfAddressProvider selfAddressProvider;
50
51 private final GroupsV2Operations groupsV2Operations;
52
53 private final GroupsV2Api groupsV2Api;
54
55 private final GroupAuthorizationProvider groupAuthorizationProvider;
56
57 public GroupHelper(
58 final ProfileKeyCredentialProvider profileKeyCredentialProvider,
59 final ProfileProvider profileProvider,
60 final SelfAddressProvider selfAddressProvider,
61 final GroupsV2Operations groupsV2Operations,
62 final GroupsV2Api groupsV2Api,
63 final GroupAuthorizationProvider groupAuthorizationProvider
64 ) {
65 this.profileKeyCredentialProvider = profileKeyCredentialProvider;
66 this.profileProvider = profileProvider;
67 this.selfAddressProvider = selfAddressProvider;
68 this.groupsV2Operations = groupsV2Operations;
69 this.groupsV2Api = groupsV2Api;
70 this.groupAuthorizationProvider = groupAuthorizationProvider;
71 }
72
73 public DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) {
74 try {
75 final GroupsV2AuthorizationString groupsV2AuthorizationString = groupAuthorizationProvider.getAuthorizationForToday(
76 groupSecretParams);
77 return groupsV2Api.getGroup(groupSecretParams, groupsV2AuthorizationString);
78 } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
79 System.err.println("Failed to retrieve Group V2 info, ignoring ...");
80 return null;
81 }
82 }
83
84 public DecryptedGroupJoinInfo getDecryptedGroupJoinInfo(
85 GroupMasterKey groupMasterKey, GroupLinkPassword password
86 ) throws IOException, GroupLinkNotActiveException {
87 GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
88
89 return groupsV2Api.getGroupJoinInfo(groupSecretParams,
90 Optional.fromNullable(password).transform(GroupLinkPassword::serialize),
91 groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams));
92 }
93
94 public GroupInfoV2 createGroupV2(
95 String name, Collection<SignalServiceAddress> members, String avatarFile
96 ) throws IOException {
97 final byte[] avatarBytes = readAvatarBytes(avatarFile);
98 final GroupsV2Operations.NewGroup newGroup = buildNewGroupV2(name, members, avatarBytes);
99 if (newGroup == null) {
100 return null;
101 }
102
103 final GroupSecretParams groupSecretParams = newGroup.getGroupSecretParams();
104
105 final GroupsV2AuthorizationString groupAuthForToday;
106 final DecryptedGroup decryptedGroup;
107 try {
108 groupAuthForToday = groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams);
109 groupsV2Api.putNewGroup(newGroup, groupAuthForToday);
110 decryptedGroup = groupsV2Api.getGroup(groupSecretParams, groupAuthForToday);
111 } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
112 System.err.println("Failed to create V2 group: " + e.getMessage());
113 return null;
114 }
115 if (decryptedGroup == null) {
116 System.err.println("Failed to create V2 group!");
117 return null;
118 }
119
120 final byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize();
121 final GroupMasterKey masterKey = groupSecretParams.getMasterKey();
122 GroupInfoV2 g = new GroupInfoV2(groupId, masterKey);
123 g.setGroup(decryptedGroup);
124
125 return g;
126 }
127
128 private byte[] readAvatarBytes(final String avatarFile) throws IOException {
129 final byte[] avatarBytes;
130 try (InputStream avatar = avatarFile == null ? null : new FileInputStream(avatarFile)) {
131 avatarBytes = avatar == null ? null : IOUtils.readFully(avatar);
132 }
133 return avatarBytes;
134 }
135
136 private GroupsV2Operations.NewGroup buildNewGroupV2(
137 String name, Collection<SignalServiceAddress> members, byte[] avatar
138 ) {
139 final ProfileKeyCredential profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(
140 selfAddressProvider.getSelfAddress());
141 if (profileKeyCredential == null) {
142 System.err.println("Cannot create a V2 group as self does not have a versioned profile");
143 return null;
144 }
145
146 if (!areMembersValid(members)) return null;
147
148 GroupCandidate self = new GroupCandidate(selfAddressProvider.getSelfAddress().getUuid().orNull(),
149 Optional.fromNullable(profileKeyCredential));
150 Set<GroupCandidate> candidates = members.stream()
151 .map(member -> new GroupCandidate(member.getUuid().get(),
152 Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member))))
153 .collect(Collectors.toSet());
154
155 final GroupSecretParams groupSecretParams = GroupSecretParams.generate();
156 return groupsV2Operations.createNewGroup(groupSecretParams,
157 name,
158 Optional.fromNullable(avatar),
159 self,
160 candidates,
161 Member.Role.DEFAULT,
162 0);
163 }
164
165 private boolean areMembersValid(final Collection<SignalServiceAddress> members) {
166 final int noUuidCapability = members.stream()
167 .filter(address -> !address.getUuid().isPresent())
168 .collect(Collectors.toUnmodifiableSet())
169 .size();
170 if (noUuidCapability > 0) {
171 System.err.println("Cannot create a V2 group as " + noUuidCapability + " members don't have a UUID.");
172 return false;
173 }
174
175 final int noGv2Capability = members.stream()
176 .map(profileProvider::getProfile)
177 .filter(profile -> profile != null && !profile.getCapabilities().gv2)
178 .collect(Collectors.toUnmodifiableSet())
179 .size();
180 if (noGv2Capability > 0) {
181 System.err.println("Cannot create a V2 group as " + noGv2Capability + " members don't support Groups V2.");
182 return false;
183 }
184
185 return true;
186 }
187
188 public Pair<DecryptedGroup, GroupChange> updateGroupV2(
189 GroupInfoV2 groupInfoV2, String name, String avatarFile
190 ) throws IOException {
191 final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
192 GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
193
194 GroupChange.Actions.Builder change = name != null
195 ? groupOperations.createModifyGroupTitle(name)
196 : GroupChange.Actions.newBuilder();
197
198 if (avatarFile != null) {
199 final byte[] avatarBytes = readAvatarBytes(avatarFile);
200 String avatarCdnKey = groupsV2Api.uploadAvatar(avatarBytes,
201 groupSecretParams,
202 groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams));
203 change.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder().setAvatar(avatarCdnKey));
204 }
205
206 final Optional<UUID> uuid = this.selfAddressProvider.getSelfAddress().getUuid();
207 if (uuid.isPresent()) {
208 change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
209 }
210
211 return commitChange(groupInfoV2, change);
212 }
213
214 public Pair<DecryptedGroup, GroupChange> updateGroupV2(
215 GroupInfoV2 groupInfoV2, Set<SignalServiceAddress> newMembers
216 ) throws IOException {
217 final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
218 GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
219
220 if (!areMembersValid(newMembers)) return null;
221
222 Set<GroupCandidate> candidates = newMembers.stream()
223 .map(member -> new GroupCandidate(member.getUuid().get(),
224 Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member))))
225 .collect(Collectors.toSet());
226
227 final GroupChange.Actions.Builder change = groupOperations.createModifyGroupMembershipChange(candidates,
228 selfAddressProvider.getSelfAddress().getUuid().get());
229
230 final Optional<UUID> uuid = this.selfAddressProvider.getSelfAddress().getUuid();
231 if (uuid.isPresent()) {
232 change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
233 }
234
235 return commitChange(groupInfoV2, change);
236 }
237
238 public Pair<DecryptedGroup, GroupChange> leaveGroup(GroupInfoV2 groupInfoV2) throws IOException {
239 List<DecryptedPendingMember> pendingMembersList = groupInfoV2.getGroup().getPendingMembersList();
240 final UUID selfUuid = selfAddressProvider.getSelfAddress().getUuid().get();
241 Optional<DecryptedPendingMember> selfPendingMember = DecryptedGroupUtil.findPendingByUuid(pendingMembersList,
242 selfUuid);
243
244 if (selfPendingMember.isPresent()) {
245 return revokeInvites(groupInfoV2, Set.of(selfPendingMember.get()));
246 } else {
247 return ejectMembers(groupInfoV2, Set.of(selfUuid));
248 }
249 }
250
251 public GroupChange joinGroup(
252 GroupMasterKey groupMasterKey,
253 GroupLinkPassword groupLinkPassword,
254 DecryptedGroupJoinInfo decryptedGroupJoinInfo
255 ) throws IOException {
256 final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
257 final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
258
259 final SignalServiceAddress selfAddress = this.selfAddressProvider.getSelfAddress();
260 final ProfileKeyCredential profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(
261 selfAddress);
262 if (profileKeyCredential == null) {
263 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
264 }
265
266 boolean requestToJoin = decryptedGroupJoinInfo.getAddFromInviteLink()
267 == AccessControl.AccessRequired.ADMINISTRATOR;
268 GroupChange.Actions.Builder change = requestToJoin
269 ? groupOperations.createGroupJoinRequest(profileKeyCredential)
270 : groupOperations.createGroupJoinDirect(profileKeyCredential);
271
272 change.setSourceUuid(UuidUtil.toByteString(selfAddress.getUuid().get()));
273
274 return commitChange(groupSecretParams, decryptedGroupJoinInfo.getRevision(), change, groupLinkPassword);
275 }
276
277 public Pair<DecryptedGroup, GroupChange> acceptInvite(GroupInfoV2 groupInfoV2) throws IOException {
278 final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
279 final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
280
281 final SignalServiceAddress selfAddress = this.selfAddressProvider.getSelfAddress();
282 final ProfileKeyCredential profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(
283 selfAddress);
284 if (profileKeyCredential == null) {
285 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
286 }
287
288 final GroupChange.Actions.Builder change = groupOperations.createAcceptInviteChange(profileKeyCredential);
289
290 final Optional<UUID> uuid = selfAddress.getUuid();
291 if (uuid.isPresent()) {
292 change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
293 }
294
295 return commitChange(groupInfoV2, change);
296 }
297
298 public Pair<DecryptedGroup, GroupChange> revokeInvites(
299 GroupInfoV2 groupInfoV2, Set<DecryptedPendingMember> pendingMembers
300 ) throws IOException {
301 final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
302 final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
303 final Set<UuidCiphertext> uuidCipherTexts = pendingMembers.stream().map(member -> {
304 try {
305 return new UuidCiphertext(member.getUuidCipherText().toByteArray());
306 } catch (InvalidInputException e) {
307 throw new AssertionError(e);
308 }
309 }).collect(Collectors.toSet());
310 return commitChange(groupInfoV2, groupOperations.createRemoveInvitationChange(uuidCipherTexts));
311 }
312
313 public Pair<DecryptedGroup, GroupChange> ejectMembers(GroupInfoV2 groupInfoV2, Set<UUID> uuids) throws IOException {
314 final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
315 final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
316 return commitChange(groupInfoV2, groupOperations.createRemoveMembersChange(uuids));
317 }
318
319 private Pair<DecryptedGroup, GroupChange> commitChange(
320 GroupInfoV2 groupInfoV2, GroupChange.Actions.Builder change
321 ) throws IOException {
322 final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
323 final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
324 final DecryptedGroup previousGroupState = groupInfoV2.getGroup();
325 final int nextRevision = previousGroupState.getRevision() + 1;
326 final GroupChange.Actions changeActions = change.setRevision(nextRevision).build();
327 final DecryptedGroupChange decryptedChange;
328 final DecryptedGroup decryptedGroupState;
329
330 try {
331 decryptedChange = groupOperations.decryptChange(changeActions,
332 selfAddressProvider.getSelfAddress().getUuid().get());
333 decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange);
334 } catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
335 throw new IOException(e);
336 }
337
338 GroupChange signedGroupChange = groupsV2Api.patchGroup(changeActions,
339 groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams),
340 Optional.absent());
341
342 return new Pair<>(decryptedGroupState, signedGroupChange);
343 }
344
345 private GroupChange commitChange(
346 GroupSecretParams groupSecretParams,
347 int currentRevision,
348 GroupChange.Actions.Builder change,
349 GroupLinkPassword password
350 ) throws IOException {
351 final int nextRevision = currentRevision + 1;
352 final GroupChange.Actions changeActions = change.setRevision(nextRevision).build();
353
354 return groupsV2Api.patchGroup(changeActions,
355 groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams),
356 Optional.fromNullable(password).transform(GroupLinkPassword::serialize));
357 }
358
359 public DecryptedGroup getUpdatedDecryptedGroup(
360 DecryptedGroup group, byte[] signedGroupChange, GroupMasterKey groupMasterKey
361 ) {
362 try {
363 final DecryptedGroupChange decryptedGroupChange = getDecryptedGroupChange(signedGroupChange,
364 groupMasterKey);
365 if (decryptedGroupChange == null) {
366 return null;
367 }
368 return DecryptedGroupUtil.apply(group, decryptedGroupChange);
369 } catch (NotAbleToApplyGroupV2ChangeException e) {
370 return null;
371 }
372 }
373
374 private DecryptedGroupChange getDecryptedGroupChange(byte[] signedGroupChange, GroupMasterKey groupMasterKey) {
375 if (signedGroupChange != null) {
376 GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(GroupSecretParams.deriveFromMasterKey(
377 groupMasterKey));
378
379 try {
380 return groupOperations.decryptChange(GroupChange.parseFrom(signedGroupChange), true).orNull();
381 } catch (VerificationFailedException | InvalidGroupStateException | InvalidProtocolBufferException e) {
382 return null;
383 }
384 }
385
386 return null;
387 }
388 }