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