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