]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/manager/helper/GroupHelper.java
Get UUIDs for unknown numbers from server
[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.storage.groups.GroupInfoV2;
6 import org.asamk.signal.util.IOUtils;
7 import org.signal.storageservice.protos.groups.GroupChange;
8 import org.signal.storageservice.protos.groups.Member;
9 import org.signal.storageservice.protos.groups.local.DecryptedGroup;
10 import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
11 import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
12 import org.signal.zkgroup.InvalidInputException;
13 import org.signal.zkgroup.VerificationFailedException;
14 import org.signal.zkgroup.groups.GroupMasterKey;
15 import org.signal.zkgroup.groups.GroupSecretParams;
16 import org.signal.zkgroup.groups.UuidCiphertext;
17 import org.signal.zkgroup.profiles.ProfileKeyCredential;
18 import org.whispersystems.libsignal.util.Pair;
19 import org.whispersystems.libsignal.util.guava.Optional;
20 import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
21 import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
22 import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
23 import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
24 import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
25 import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
26 import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException;
27 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
28 import org.whispersystems.signalservice.api.util.UuidUtil;
29
30 import java.io.FileInputStream;
31 import java.io.IOException;
32 import java.io.InputStream;
33 import java.util.Collection;
34 import java.util.List;
35 import java.util.Set;
36 import java.util.UUID;
37 import java.util.stream.Collectors;
38
39 public class GroupHelper {
40
41 private final ProfileKeyCredentialProvider profileKeyCredentialProvider;
42
43 private final ProfileProvider profileProvider;
44
45 private final SelfAddressProvider selfAddressProvider;
46
47 private final GroupsV2Operations groupsV2Operations;
48
49 private final GroupsV2Api groupsV2Api;
50
51 private final GroupAuthorizationProvider groupAuthorizationProvider;
52
53 public GroupHelper(
54 final ProfileKeyCredentialProvider profileKeyCredentialProvider,
55 final ProfileProvider profileProvider,
56 final SelfAddressProvider selfAddressProvider,
57 final GroupsV2Operations groupsV2Operations,
58 final GroupsV2Api groupsV2Api,
59 final GroupAuthorizationProvider groupAuthorizationProvider
60 ) {
61 this.profileKeyCredentialProvider = profileKeyCredentialProvider;
62 this.profileProvider = profileProvider;
63 this.selfAddressProvider = selfAddressProvider;
64 this.groupsV2Operations = groupsV2Operations;
65 this.groupsV2Api = groupsV2Api;
66 this.groupAuthorizationProvider = groupAuthorizationProvider;
67 }
68
69 public GroupInfoV2 createGroupV2(
70 String name, Collection<SignalServiceAddress> members, String avatarFile
71 ) throws IOException {
72 final byte[] avatarBytes = readAvatarBytes(avatarFile);
73 final GroupsV2Operations.NewGroup newGroup = buildNewGroupV2(name, members, avatarBytes);
74 if (newGroup == null) {
75 return null;
76 }
77
78 final GroupSecretParams groupSecretParams = newGroup.getGroupSecretParams();
79
80 final GroupsV2AuthorizationString groupAuthForToday;
81 final DecryptedGroup decryptedGroup;
82 try {
83 groupAuthForToday = groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams);
84 groupsV2Api.putNewGroup(newGroup, groupAuthForToday);
85 decryptedGroup = groupsV2Api.getGroup(groupSecretParams, groupAuthForToday);
86 } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
87 System.err.println("Failed to create V2 group: " + e.getMessage());
88 return null;
89 }
90 if (decryptedGroup == null) {
91 System.err.println("Failed to create V2 group!");
92 return null;
93 }
94
95 final byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize();
96 final GroupMasterKey masterKey = groupSecretParams.getMasterKey();
97 GroupInfoV2 g = new GroupInfoV2(groupId, masterKey);
98 g.setGroup(decryptedGroup);
99
100 return g;
101 }
102
103 private byte[] readAvatarBytes(final String avatarFile) throws IOException {
104 final byte[] avatarBytes;
105 try (InputStream avatar = avatarFile == null ? null : new FileInputStream(avatarFile)) {
106 avatarBytes = avatar == null ? null : IOUtils.readFully(avatar);
107 }
108 return avatarBytes;
109 }
110
111 private GroupsV2Operations.NewGroup buildNewGroupV2(
112 String name, Collection<SignalServiceAddress> members, byte[] avatar
113 ) {
114 final ProfileKeyCredential profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(
115 selfAddressProvider.getSelfAddress());
116 if (profileKeyCredential == null) {
117 System.err.println("Cannot create a V2 group as self does not have a versioned profile");
118 return null;
119 }
120
121 final int noUuidCapability = members.stream()
122 .filter(address -> !address.getUuid().isPresent())
123 .collect(Collectors.toUnmodifiableSet())
124 .size();
125 if (noUuidCapability > 0) {
126 System.err.println("Cannot create a V2 group as " + noUuidCapability + " members don't have a UUID.");
127 return null;
128 }
129
130 final int noGv2Capability = members.stream()
131 .map(profileProvider::getProfile)
132 .filter(profile -> !profile.getCapabilities().gv2)
133 .collect(Collectors.toUnmodifiableSet())
134 .size();
135 if (noGv2Capability > 0) {
136 System.err.println("Cannot create a V2 group as " + noGv2Capability + " members don't support Groups V2.");
137 return null;
138 }
139
140 GroupCandidate self = new GroupCandidate(selfAddressProvider.getSelfAddress().getUuid().orNull(),
141 Optional.fromNullable(profileKeyCredential));
142 Set<GroupCandidate> candidates = members.stream()
143 .map(member -> new GroupCandidate(member.getUuid().get(),
144 Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member))))
145 .collect(Collectors.toSet());
146
147 final GroupSecretParams groupSecretParams = GroupSecretParams.generate();
148 return groupsV2Operations.createNewGroup(groupSecretParams,
149 name,
150 Optional.fromNullable(avatar),
151 self,
152 candidates,
153 Member.Role.DEFAULT,
154 0);
155 }
156
157 public Pair<DecryptedGroup, GroupChange> updateGroupV2(
158 GroupInfoV2 groupInfoV2, String name, String avatarFile
159 ) throws IOException {
160 final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
161 GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
162
163 GroupChange.Actions.Builder change = name != null
164 ? groupOperations.createModifyGroupTitle(name)
165 : GroupChange.Actions.newBuilder();
166
167 if (avatarFile != null) {
168 final byte[] avatarBytes = readAvatarBytes(avatarFile);
169 String avatarCdnKey = groupsV2Api.uploadAvatar(avatarBytes,
170 groupSecretParams,
171 groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams));
172 change.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder().setAvatar(avatarCdnKey));
173 }
174
175 final Optional<UUID> uuid = this.selfAddressProvider.getSelfAddress().getUuid();
176 if (uuid.isPresent()) {
177 change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
178 }
179
180 return commitChange(groupInfoV2, change);
181 }
182
183 public Pair<DecryptedGroup, GroupChange> updateGroupV2(
184 GroupInfoV2 groupInfoV2, Set<SignalServiceAddress> newMembers
185 ) throws IOException {
186 final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
187 GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
188
189 Set<GroupCandidate> candidates = newMembers.stream()
190 .map(member -> new GroupCandidate(member.getUuid().get(),
191 Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member))))
192 .collect(Collectors.toSet());
193
194 final GroupChange.Actions.Builder change = groupOperations.createModifyGroupMembershipChange(candidates,
195 selfAddressProvider.getSelfAddress().getUuid().get());
196
197 final Optional<UUID> uuid = this.selfAddressProvider.getSelfAddress().getUuid();
198 if (uuid.isPresent()) {
199 change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
200 }
201
202 return commitChange(groupInfoV2, change);
203 }
204
205 public Pair<DecryptedGroup, GroupChange> leaveGroup(GroupInfoV2 groupInfoV2) throws IOException {
206 List<DecryptedPendingMember> pendingMembersList = groupInfoV2.getGroup().getPendingMembersList();
207 final UUID selfUuid = selfAddressProvider.getSelfAddress().getUuid().get();
208 Optional<DecryptedPendingMember> selfPendingMember = DecryptedGroupUtil.findPendingByUuid(pendingMembersList,
209 selfUuid);
210
211 if (selfPendingMember.isPresent()) {
212 return revokeInvites(groupInfoV2, Set.of(selfPendingMember.get()));
213 } else {
214 return ejectMembers(groupInfoV2, Set.of(selfUuid));
215 }
216 }
217
218 public Pair<DecryptedGroup, GroupChange> revokeInvites(
219 GroupInfoV2 groupInfoV2, Set<DecryptedPendingMember> pendingMembers
220 ) throws IOException {
221 final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
222 final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
223 final Set<UuidCiphertext> uuidCipherTexts = pendingMembers.stream().map(member -> {
224 try {
225 return new UuidCiphertext(member.getUuidCipherText().toByteArray());
226 } catch (InvalidInputException e) {
227 throw new AssertionError(e);
228 }
229 }).collect(Collectors.toSet());
230 return commitChange(groupInfoV2, groupOperations.createRemoveInvitationChange(uuidCipherTexts));
231 }
232
233 public Pair<DecryptedGroup, GroupChange> ejectMembers(GroupInfoV2 groupInfoV2, Set<UUID> uuids) throws IOException {
234 final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
235 final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
236 return commitChange(groupInfoV2, groupOperations.createRemoveMembersChange(uuids));
237 }
238
239 private Pair<DecryptedGroup, GroupChange> commitChange(
240 GroupInfoV2 groupInfoV2, GroupChange.Actions.Builder change
241 ) throws IOException {
242 final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
243 final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
244 final DecryptedGroup previousGroupState = groupInfoV2.getGroup();
245 final int nextRevision = previousGroupState.getRevision() + 1;
246 final GroupChange.Actions changeActions = change.setRevision(nextRevision).build();
247 final DecryptedGroupChange decryptedChange;
248 final DecryptedGroup decryptedGroupState;
249
250 try {
251 decryptedChange = groupOperations.decryptChange(changeActions,
252 selfAddressProvider.getSelfAddress().getUuid().get());
253 decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange);
254 } catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
255 throw new IOException(e);
256 }
257
258 GroupChange signedGroupChange = groupsV2Api.patchGroup(change.build(),
259 groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams),
260 Optional.absent());
261
262 return new Pair<>(decryptedGroupState, signedGroupChange);
263 }
264
265 public DecryptedGroup getUpdatedDecryptedGroup(
266 DecryptedGroup group, byte[] signedGroupChange, GroupMasterKey groupMasterKey
267 ) {
268 try {
269 final DecryptedGroupChange decryptedGroupChange = getDecryptedGroupChange(signedGroupChange,
270 groupMasterKey);
271 if (decryptedGroupChange == null) {
272 return null;
273 }
274 return DecryptedGroupUtil.apply(group, decryptedGroupChange);
275 } catch (NotAbleToApplyGroupV2ChangeException e) {
276 return null;
277 }
278 }
279
280 private DecryptedGroupChange getDecryptedGroupChange(byte[] signedGroupChange, GroupMasterKey groupMasterKey) {
281 if (signedGroupChange != null) {
282 GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(GroupSecretParams.deriveFromMasterKey(
283 groupMasterKey));
284
285 try {
286 return groupOperations.decryptChange(GroupChange.parseFrom(signedGroupChange), true).orNull();
287 } catch (VerificationFailedException | InvalidGroupStateException | InvalidProtocolBufferException e) {
288 return null;
289 }
290 }
291
292 return null;
293 }
294 }