1 package org
.asamk
.signal
.manager
.helper
;
3 import com
.google
.protobuf
.InvalidProtocolBufferException
;
5 import org
.asamk
.signal
.manager
.GroupIdV2
;
6 import org
.asamk
.signal
.manager
.GroupLinkPassword
;
7 import org
.asamk
.signal
.manager
.GroupUtils
;
8 import org
.asamk
.signal
.storage
.groups
.GroupInfoV2
;
9 import org
.asamk
.signal
.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
.signal
.zkgroup
.profiles
.ProfileKeyCredential
;
23 import org
.whispersystems
.libsignal
.util
.Pair
;
24 import org
.whispersystems
.libsignal
.util
.guava
.Optional
;
25 import org
.whispersystems
.signalservice
.api
.groupsv2
.DecryptedGroupUtil
;
26 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupCandidate
;
27 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupLinkNotActiveException
;
28 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2Api
;
29 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2AuthorizationString
;
30 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2Operations
;
31 import org
.whispersystems
.signalservice
.api
.groupsv2
.InvalidGroupStateException
;
32 import org
.whispersystems
.signalservice
.api
.groupsv2
.NotAbleToApplyGroupV2ChangeException
;
33 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
34 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
36 import java
.io
.FileInputStream
;
37 import java
.io
.IOException
;
38 import java
.io
.InputStream
;
39 import java
.util
.Collection
;
40 import java
.util
.List
;
42 import java
.util
.UUID
;
43 import java
.util
.stream
.Collectors
;
45 public class GroupHelper
{
47 private final ProfileKeyCredentialProvider profileKeyCredentialProvider
;
49 private final ProfileProvider profileProvider
;
51 private final SelfAddressProvider selfAddressProvider
;
53 private final GroupsV2Operations groupsV2Operations
;
55 private final GroupsV2Api groupsV2Api
;
57 private final GroupAuthorizationProvider groupAuthorizationProvider
;
60 final ProfileKeyCredentialProvider profileKeyCredentialProvider
,
61 final ProfileProvider profileProvider
,
62 final SelfAddressProvider selfAddressProvider
,
63 final GroupsV2Operations groupsV2Operations
,
64 final GroupsV2Api groupsV2Api
,
65 final GroupAuthorizationProvider groupAuthorizationProvider
67 this.profileKeyCredentialProvider
= profileKeyCredentialProvider
;
68 this.profileProvider
= profileProvider
;
69 this.selfAddressProvider
= selfAddressProvider
;
70 this.groupsV2Operations
= groupsV2Operations
;
71 this.groupsV2Api
= groupsV2Api
;
72 this.groupAuthorizationProvider
= groupAuthorizationProvider
;
75 public DecryptedGroup
getDecryptedGroup(final GroupSecretParams groupSecretParams
) {
77 final GroupsV2AuthorizationString groupsV2AuthorizationString
= groupAuthorizationProvider
.getAuthorizationForToday(
79 return groupsV2Api
.getGroup(groupSecretParams
, groupsV2AuthorizationString
);
80 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
81 System
.err
.println("Failed to retrieve Group V2 info, ignoring ...");
86 public DecryptedGroupJoinInfo
getDecryptedGroupJoinInfo(
87 GroupMasterKey groupMasterKey
, GroupLinkPassword password
88 ) throws IOException
, GroupLinkNotActiveException
{
89 GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
91 return groupsV2Api
.getGroupJoinInfo(groupSecretParams
,
92 Optional
.fromNullable(password
).transform(GroupLinkPassword
::serialize
),
93 groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
));
96 public GroupInfoV2
createGroupV2(
97 String name
, Collection
<SignalServiceAddress
> members
, String avatarFile
98 ) throws IOException
{
99 final byte[] avatarBytes
= readAvatarBytes(avatarFile
);
100 final GroupsV2Operations
.NewGroup newGroup
= buildNewGroupV2(name
, members
, avatarBytes
);
101 if (newGroup
== null) {
105 final GroupSecretParams groupSecretParams
= newGroup
.getGroupSecretParams();
107 final GroupsV2AuthorizationString groupAuthForToday
;
108 final DecryptedGroup decryptedGroup
;
110 groupAuthForToday
= groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
);
111 groupsV2Api
.putNewGroup(newGroup
, groupAuthForToday
);
112 decryptedGroup
= groupsV2Api
.getGroup(groupSecretParams
, groupAuthForToday
);
113 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
114 System
.err
.println("Failed to create V2 group: " + e
.getMessage());
117 if (decryptedGroup
== null) {
118 System
.err
.println("Failed to create V2 group!");
122 final GroupIdV2 groupId
= GroupUtils
.getGroupIdV2(groupSecretParams
);
123 final GroupMasterKey masterKey
= groupSecretParams
.getMasterKey();
124 GroupInfoV2 g
= new GroupInfoV2(groupId
, masterKey
);
125 g
.setGroup(decryptedGroup
);
130 private byte[] readAvatarBytes(final String avatarFile
) throws IOException
{
131 final byte[] avatarBytes
;
132 try (InputStream avatar
= avatarFile
== null ?
null : new FileInputStream(avatarFile
)) {
133 avatarBytes
= avatar
== null ?
null : IOUtils
.readFully(avatar
);
138 private GroupsV2Operations
.NewGroup
buildNewGroupV2(
139 String name
, Collection
<SignalServiceAddress
> members
, byte[] avatar
141 final ProfileKeyCredential profileKeyCredential
= profileKeyCredentialProvider
.getProfileKeyCredential(
142 selfAddressProvider
.getSelfAddress());
143 if (profileKeyCredential
== null) {
144 System
.err
.println("Cannot create a V2 group as self does not have a versioned profile");
148 if (!areMembersValid(members
)) return null;
150 GroupCandidate self
= new GroupCandidate(selfAddressProvider
.getSelfAddress().getUuid().orNull(),
151 Optional
.fromNullable(profileKeyCredential
));
152 Set
<GroupCandidate
> candidates
= members
.stream()
153 .map(member
-> new GroupCandidate(member
.getUuid().get(),
154 Optional
.fromNullable(profileKeyCredentialProvider
.getProfileKeyCredential(member
))))
155 .collect(Collectors
.toSet());
157 final GroupSecretParams groupSecretParams
= GroupSecretParams
.generate();
158 return groupsV2Operations
.createNewGroup(groupSecretParams
,
160 Optional
.fromNullable(avatar
),
167 private boolean areMembersValid(final Collection
<SignalServiceAddress
> members
) {
168 final int noUuidCapability
= members
.stream()
169 .filter(address
-> !address
.getUuid().isPresent())
170 .collect(Collectors
.toUnmodifiableSet())
172 if (noUuidCapability
> 0) {
173 System
.err
.println("Cannot create a V2 group as " + noUuidCapability
+ " members don't have a UUID.");
177 final int noGv2Capability
= members
.stream()
178 .map(profileProvider
::getProfile
)
179 .filter(profile
-> profile
!= null && !profile
.getCapabilities().gv2
)
180 .collect(Collectors
.toUnmodifiableSet())
182 if (noGv2Capability
> 0) {
183 System
.err
.println("Cannot create a V2 group as " + noGv2Capability
+ " members don't support Groups V2.");
190 public Pair
<DecryptedGroup
, GroupChange
> updateGroupV2(
191 GroupInfoV2 groupInfoV2
, String name
, String avatarFile
192 ) throws IOException
{
193 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
194 GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
196 GroupChange
.Actions
.Builder change
= name
!= null
197 ? groupOperations
.createModifyGroupTitle(name
)
198 : GroupChange
.Actions
.newBuilder();
200 if (avatarFile
!= null) {
201 final byte[] avatarBytes
= readAvatarBytes(avatarFile
);
202 String avatarCdnKey
= groupsV2Api
.uploadAvatar(avatarBytes
,
204 groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
));
205 change
.setModifyAvatar(GroupChange
.Actions
.ModifyAvatarAction
.newBuilder().setAvatar(avatarCdnKey
));
208 final Optional
<UUID
> uuid
= this.selfAddressProvider
.getSelfAddress().getUuid();
209 if (uuid
.isPresent()) {
210 change
.setSourceUuid(UuidUtil
.toByteString(uuid
.get()));
213 return commitChange(groupInfoV2
, change
);
216 public Pair
<DecryptedGroup
, GroupChange
> updateGroupV2(
217 GroupInfoV2 groupInfoV2
, Set
<SignalServiceAddress
> newMembers
218 ) throws IOException
{
219 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
220 GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
222 if (!areMembersValid(newMembers
)) return null;
224 Set
<GroupCandidate
> candidates
= newMembers
.stream()
225 .map(member
-> new GroupCandidate(member
.getUuid().get(),
226 Optional
.fromNullable(profileKeyCredentialProvider
.getProfileKeyCredential(member
))))
227 .collect(Collectors
.toSet());
229 final GroupChange
.Actions
.Builder change
= groupOperations
.createModifyGroupMembershipChange(candidates
,
230 selfAddressProvider
.getSelfAddress().getUuid().get());
232 final Optional
<UUID
> uuid
= this.selfAddressProvider
.getSelfAddress().getUuid();
233 if (uuid
.isPresent()) {
234 change
.setSourceUuid(UuidUtil
.toByteString(uuid
.get()));
237 return commitChange(groupInfoV2
, change
);
240 public Pair
<DecryptedGroup
, GroupChange
> leaveGroup(GroupInfoV2 groupInfoV2
) throws IOException
{
241 List
<DecryptedPendingMember
> pendingMembersList
= groupInfoV2
.getGroup().getPendingMembersList();
242 final UUID selfUuid
= selfAddressProvider
.getSelfAddress().getUuid().get();
243 Optional
<DecryptedPendingMember
> selfPendingMember
= DecryptedGroupUtil
.findPendingByUuid(pendingMembersList
,
246 if (selfPendingMember
.isPresent()) {
247 return revokeInvites(groupInfoV2
, Set
.of(selfPendingMember
.get()));
249 return ejectMembers(groupInfoV2
, Set
.of(selfUuid
));
253 public GroupChange
joinGroup(
254 GroupMasterKey groupMasterKey
,
255 GroupLinkPassword groupLinkPassword
,
256 DecryptedGroupJoinInfo decryptedGroupJoinInfo
257 ) throws IOException
{
258 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
259 final GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
261 final SignalServiceAddress selfAddress
= this.selfAddressProvider
.getSelfAddress();
262 final ProfileKeyCredential profileKeyCredential
= profileKeyCredentialProvider
.getProfileKeyCredential(
264 if (profileKeyCredential
== null) {
265 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
268 boolean requestToJoin
= decryptedGroupJoinInfo
.getAddFromInviteLink()
269 == AccessControl
.AccessRequired
.ADMINISTRATOR
;
270 GroupChange
.Actions
.Builder change
= requestToJoin
271 ? groupOperations
.createGroupJoinRequest(profileKeyCredential
)
272 : groupOperations
.createGroupJoinDirect(profileKeyCredential
);
274 change
.setSourceUuid(UuidUtil
.toByteString(selfAddress
.getUuid().get()));
276 return commitChange(groupSecretParams
, decryptedGroupJoinInfo
.getRevision(), change
, groupLinkPassword
);
279 public Pair
<DecryptedGroup
, GroupChange
> acceptInvite(GroupInfoV2 groupInfoV2
) throws IOException
{
280 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
281 final GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
283 final SignalServiceAddress selfAddress
= this.selfAddressProvider
.getSelfAddress();
284 final ProfileKeyCredential profileKeyCredential
= profileKeyCredentialProvider
.getProfileKeyCredential(
286 if (profileKeyCredential
== null) {
287 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
290 final GroupChange
.Actions
.Builder change
= groupOperations
.createAcceptInviteChange(profileKeyCredential
);
292 final Optional
<UUID
> uuid
= selfAddress
.getUuid();
293 if (uuid
.isPresent()) {
294 change
.setSourceUuid(UuidUtil
.toByteString(uuid
.get()));
297 return commitChange(groupInfoV2
, change
);
300 public Pair
<DecryptedGroup
, GroupChange
> revokeInvites(
301 GroupInfoV2 groupInfoV2
, Set
<DecryptedPendingMember
> pendingMembers
302 ) throws IOException
{
303 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
304 final GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
305 final Set
<UuidCiphertext
> uuidCipherTexts
= pendingMembers
.stream().map(member
-> {
307 return new UuidCiphertext(member
.getUuidCipherText().toByteArray());
308 } catch (InvalidInputException e
) {
309 throw new AssertionError(e
);
311 }).collect(Collectors
.toSet());
312 return commitChange(groupInfoV2
, groupOperations
.createRemoveInvitationChange(uuidCipherTexts
));
315 public Pair
<DecryptedGroup
, GroupChange
> ejectMembers(GroupInfoV2 groupInfoV2
, Set
<UUID
> uuids
) throws IOException
{
316 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
317 final GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
318 return commitChange(groupInfoV2
, groupOperations
.createRemoveMembersChange(uuids
));
321 private Pair
<DecryptedGroup
, GroupChange
> commitChange(
322 GroupInfoV2 groupInfoV2
, GroupChange
.Actions
.Builder change
323 ) throws IOException
{
324 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
325 final GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
326 final DecryptedGroup previousGroupState
= groupInfoV2
.getGroup();
327 final int nextRevision
= previousGroupState
.getRevision() + 1;
328 final GroupChange
.Actions changeActions
= change
.setRevision(nextRevision
).build();
329 final DecryptedGroupChange decryptedChange
;
330 final DecryptedGroup decryptedGroupState
;
333 decryptedChange
= groupOperations
.decryptChange(changeActions
,
334 selfAddressProvider
.getSelfAddress().getUuid().get());
335 decryptedGroupState
= DecryptedGroupUtil
.apply(previousGroupState
, decryptedChange
);
336 } catch (VerificationFailedException
| InvalidGroupStateException
| NotAbleToApplyGroupV2ChangeException e
) {
337 throw new IOException(e
);
340 GroupChange signedGroupChange
= groupsV2Api
.patchGroup(changeActions
,
341 groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
),
344 return new Pair
<>(decryptedGroupState
, signedGroupChange
);
347 private GroupChange
commitChange(
348 GroupSecretParams groupSecretParams
,
350 GroupChange
.Actions
.Builder change
,
351 GroupLinkPassword password
352 ) throws IOException
{
353 final int nextRevision
= currentRevision
+ 1;
354 final GroupChange
.Actions changeActions
= change
.setRevision(nextRevision
).build();
356 return groupsV2Api
.patchGroup(changeActions
,
357 groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
),
358 Optional
.fromNullable(password
).transform(GroupLinkPassword
::serialize
));
361 public DecryptedGroup
getUpdatedDecryptedGroup(
362 DecryptedGroup group
, byte[] signedGroupChange
, GroupMasterKey groupMasterKey
365 final DecryptedGroupChange decryptedGroupChange
= getDecryptedGroupChange(signedGroupChange
,
367 if (decryptedGroupChange
== null) {
370 return DecryptedGroupUtil
.apply(group
, decryptedGroupChange
);
371 } catch (NotAbleToApplyGroupV2ChangeException e
) {
376 private DecryptedGroupChange
getDecryptedGroupChange(byte[] signedGroupChange
, GroupMasterKey groupMasterKey
) {
377 if (signedGroupChange
!= null) {
378 GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(GroupSecretParams
.deriveFromMasterKey(
382 return groupOperations
.decryptChange(GroupChange
.parseFrom(signedGroupChange
), true).orNull();
383 } catch (VerificationFailedException
| InvalidGroupStateException
| InvalidProtocolBufferException e
) {