]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java
Update libsignal-service-java
[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 Pair<GroupInfoV2, DecryptedGroup> 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
133 return new Pair<>(g, decryptedGroup);
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, Set<RecipientId> members, byte[] avatar
146 ) {
147 final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfRecipientIdProvider.getSelfRecipientId());
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 var self = new GroupCandidate(addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId())
156 .getUuid()
157 .orNull(), Optional.fromNullable(profileKeyCredential));
158 var candidates = members.stream()
159 .map(member -> new GroupCandidate(addressResolver.resolveSignalServiceAddress(member).getUuid().get(),
160 Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member))))
161 .collect(Collectors.toSet());
162
163 final var 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 Set<RecipientId> members) {
174 final var noUuidCapability = members.stream()
175 .map(addressResolver::resolveSignalServiceAddress)
176 .filter(address -> !address.getUuid().isPresent())
177 .map(SignalServiceAddress::getLegacyIdentifier)
178 .collect(Collectors.toSet());
179 if (noUuidCapability.size() > 0) {
180 logger.warn("Cannot create a V2 group as some members don't have a UUID: {}",
181 String.join(", ", noUuidCapability));
182 return false;
183 }
184
185 final var noGv2Capability = members.stream()
186 .map(profileProvider::getProfile)
187 .filter(profile -> profile != null && !profile.getCapabilities().contains(Profile.Capability.gv2))
188 .collect(Collectors.toSet());
189 if (noGv2Capability.size() > 0) {
190 logger.warn("Cannot create a V2 group as some members don't support Groups V2: {}",
191 noGv2Capability.stream().map(Profile::getDisplayName).collect(Collectors.joining(", ")));
192 return false;
193 }
194
195 return true;
196 }
197
198 public Pair<DecryptedGroup, GroupChange> updateGroupV2(
199 GroupInfoV2 groupInfoV2, String name, File avatarFile
200 ) throws IOException {
201 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
202 var groupOperations = groupsV2Operations.forGroup(groupSecretParams);
203
204 var change = name != null ? groupOperations.createModifyGroupTitle(name) : GroupChange.Actions.newBuilder();
205
206 if (avatarFile != null) {
207 final var avatarBytes = readAvatarBytes(avatarFile);
208 var avatarCdnKey = groupsV2Api.uploadAvatar(avatarBytes,
209 groupSecretParams,
210 groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams));
211 change.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder().setAvatar(avatarCdnKey));
212 }
213
214 final var uuid = addressResolver.resolveSignalServiceAddress(this.selfRecipientIdProvider.getSelfRecipientId())
215 .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<RecipientId> newMembers
225 ) throws IOException {
226 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
227 var groupOperations = groupsV2Operations.forGroup(groupSecretParams);
228
229 if (!areMembersValid(newMembers)) {
230 throw new IOException("Failed to update group");
231 }
232
233 var candidates = newMembers.stream()
234 .map(member -> new GroupCandidate(addressResolver.resolveSignalServiceAddress(member).getUuid().get(),
235 Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member))))
236 .collect(Collectors.toSet());
237
238 final var uuid = addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId())
239 .getUuid()
240 .get();
241 final var change = groupOperations.createModifyGroupMembershipChange(candidates, uuid);
242
243 change.setSourceUuid(UuidUtil.toByteString(uuid));
244
245 return commitChange(groupInfoV2, change);
246 }
247
248 public Pair<DecryptedGroup, GroupChange> leaveGroup(GroupInfoV2 groupInfoV2) throws IOException {
249 var pendingMembersList = groupInfoV2.getGroup().getPendingMembersList();
250 final var selfUuid = addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId())
251 .getUuid()
252 .get();
253 var selfPendingMember = DecryptedGroupUtil.findPendingByUuid(pendingMembersList, 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 var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
268 final var groupOperations = groupsV2Operations.forGroup(groupSecretParams);
269
270 final var selfRecipientId = this.selfRecipientIdProvider.getSelfRecipientId();
271 final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfRecipientId);
272 if (profileKeyCredential == null) {
273 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
274 }
275
276 var requestToJoin = decryptedGroupJoinInfo.getAddFromInviteLink() == AccessControl.AccessRequired.ADMINISTRATOR;
277 var change = requestToJoin
278 ? groupOperations.createGroupJoinRequest(profileKeyCredential)
279 : groupOperations.createGroupJoinDirect(profileKeyCredential);
280
281 change.setSourceUuid(UuidUtil.toByteString(addressResolver.resolveSignalServiceAddress(selfRecipientId)
282 .getUuid()
283 .get()));
284
285 return commitChange(groupSecretParams, decryptedGroupJoinInfo.getRevision(), change, groupLinkPassword);
286 }
287
288 public Pair<DecryptedGroup, GroupChange> acceptInvite(GroupInfoV2 groupInfoV2) throws IOException {
289 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
290 final var groupOperations = groupsV2Operations.forGroup(groupSecretParams);
291
292 final var selfRecipientId = this.selfRecipientIdProvider.getSelfRecipientId();
293 final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfRecipientId);
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 var change = groupOperations.createAcceptInviteChange(profileKeyCredential);
299
300 final var uuid = addressResolver.resolveSignalServiceAddress(selfRecipientId).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 var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
312 final var groupOperations = groupsV2Operations.forGroup(groupSecretParams);
313 final var 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 var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
325 final var 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 var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
333 final var groupOperations = groupsV2Operations.forGroup(groupSecretParams);
334 final var previousGroupState = groupInfoV2.getGroup();
335 final var nextRevision = previousGroupState.getRevision() + 1;
336 final var changeActions = change.setRevision(nextRevision).build();
337 final DecryptedGroupChange decryptedChange;
338 final DecryptedGroup decryptedGroupState;
339
340 try {
341 decryptedChange = groupOperations.decryptChange(changeActions,
342 addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId())
343 .getUuid()
344 .get());
345 decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange);
346 } catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
347 throw new IOException(e);
348 }
349
350 var signedGroupChange = groupsV2Api.patchGroup(changeActions,
351 groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams),
352 Optional.absent());
353
354 return new Pair<>(decryptedGroupState, signedGroupChange);
355 }
356
357 private GroupChange commitChange(
358 GroupSecretParams groupSecretParams,
359 int currentRevision,
360 GroupChange.Actions.Builder change,
361 GroupLinkPassword password
362 ) throws IOException {
363 final var nextRevision = currentRevision + 1;
364 final var changeActions = change.setRevision(nextRevision).build();
365
366 return groupsV2Api.patchGroup(changeActions,
367 groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams),
368 Optional.fromNullable(password).transform(GroupLinkPassword::serialize));
369 }
370
371 public DecryptedGroup getUpdatedDecryptedGroup(
372 DecryptedGroup group, byte[] signedGroupChange, GroupMasterKey groupMasterKey
373 ) {
374 try {
375 final var decryptedGroupChange = getDecryptedGroupChange(signedGroupChange, 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 var groupOperations = groupsV2Operations.forGroup(GroupSecretParams.deriveFromMasterKey(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 }