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