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