1 package org
.asamk
.signal
.manager
.helper
;
3 import com
.google
.protobuf
.InvalidProtocolBufferException
;
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
.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
.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
;
40 import java
.io
.FileInputStream
;
41 import java
.io
.IOException
;
42 import java
.io
.InputStream
;
43 import java
.util
.Collection
;
44 import java
.util
.List
;
46 import java
.util
.UUID
;
47 import java
.util
.stream
.Collectors
;
49 public class GroupHelper
{
51 private final static Logger logger
= LoggerFactory
.getLogger(GroupHelper
.class);
53 private final ProfileKeyCredentialProvider profileKeyCredentialProvider
;
55 private final ProfileProvider profileProvider
;
57 private final SelfAddressProvider selfAddressProvider
;
59 private final GroupsV2Operations groupsV2Operations
;
61 private final GroupsV2Api groupsV2Api
;
63 private final GroupAuthorizationProvider groupAuthorizationProvider
;
66 final ProfileKeyCredentialProvider profileKeyCredentialProvider
,
67 final ProfileProvider profileProvider
,
68 final SelfAddressProvider selfAddressProvider
,
69 final GroupsV2Operations groupsV2Operations
,
70 final GroupsV2Api groupsV2Api
,
71 final GroupAuthorizationProvider groupAuthorizationProvider
73 this.profileKeyCredentialProvider
= profileKeyCredentialProvider
;
74 this.profileProvider
= profileProvider
;
75 this.selfAddressProvider
= selfAddressProvider
;
76 this.groupsV2Operations
= groupsV2Operations
;
77 this.groupsV2Api
= groupsV2Api
;
78 this.groupAuthorizationProvider
= groupAuthorizationProvider
;
81 public DecryptedGroup
getDecryptedGroup(final GroupSecretParams groupSecretParams
) {
83 final GroupsV2AuthorizationString groupsV2AuthorizationString
= groupAuthorizationProvider
.getAuthorizationForToday(
85 return groupsV2Api
.getGroup(groupSecretParams
, groupsV2AuthorizationString
);
86 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
87 logger
.warn("Failed to retrieve Group V2 info, ignoring: {}", e
.getMessage());
92 public DecryptedGroupJoinInfo
getDecryptedGroupJoinInfo(
93 GroupMasterKey groupMasterKey
, GroupLinkPassword password
94 ) throws IOException
, GroupLinkNotActiveException
{
95 GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
97 return groupsV2Api
.getGroupJoinInfo(groupSecretParams
,
98 Optional
.fromNullable(password
).transform(GroupLinkPassword
::serialize
),
99 groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
));
102 public GroupInfoV2
createGroupV2(
103 String name
, Collection
<SignalServiceAddress
> members
, File avatarFile
104 ) throws IOException
{
105 final byte[] avatarBytes
= readAvatarBytes(avatarFile
);
106 final GroupsV2Operations
.NewGroup newGroup
= buildNewGroupV2(name
, members
, avatarBytes
);
107 if (newGroup
== null) {
111 final GroupSecretParams groupSecretParams
= newGroup
.getGroupSecretParams();
113 final GroupsV2AuthorizationString groupAuthForToday
;
114 final DecryptedGroup decryptedGroup
;
116 groupAuthForToday
= groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
);
117 groupsV2Api
.putNewGroup(newGroup
, groupAuthForToday
);
118 decryptedGroup
= groupsV2Api
.getGroup(groupSecretParams
, groupAuthForToday
);
119 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
120 logger
.warn("Failed to create V2 group: {}", e
.getMessage());
123 if (decryptedGroup
== null) {
124 logger
.warn("Failed to create V2 group, unknown error!");
128 final GroupIdV2 groupId
= GroupUtils
.getGroupIdV2(groupSecretParams
);
129 final GroupMasterKey masterKey
= groupSecretParams
.getMasterKey();
130 GroupInfoV2 g
= new GroupInfoV2(groupId
, masterKey
);
131 g
.setGroup(decryptedGroup
);
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
);
144 private GroupsV2Operations
.NewGroup
buildNewGroupV2(
145 String name
, Collection
<SignalServiceAddress
> members
, byte[] avatar
147 final ProfileKeyCredential profileKeyCredential
= profileKeyCredentialProvider
.getProfileKeyCredential(
148 selfAddressProvider
.getSelfAddress());
149 if (profileKeyCredential
== null) {
150 logger
.warn("Cannot create a V2 group as self does not have a versioned profile");
154 if (!areMembersValid(members
)) return null;
156 GroupCandidate self
= new GroupCandidate(selfAddressProvider
.getSelfAddress().getUuid().orNull(),
157 Optional
.fromNullable(profileKeyCredential
));
158 Set
<GroupCandidate
> candidates
= members
.stream()
159 .map(member
-> new GroupCandidate(member
.getUuid().get(),
160 Optional
.fromNullable(profileKeyCredentialProvider
.getProfileKeyCredential(member
))))
161 .collect(Collectors
.toSet());
163 final GroupSecretParams groupSecretParams
= GroupSecretParams
.generate();
164 return groupsV2Operations
.createNewGroup(groupSecretParams
,
166 Optional
.fromNullable(avatar
),
173 private boolean areMembersValid(final Collection
<SignalServiceAddress
> members
) {
174 final Set
<String
> noUuidCapability
= members
.stream()
175 .filter(address
-> !address
.getUuid().isPresent())
176 .map(SignalServiceAddress
::getLegacyIdentifier
)
177 .collect(Collectors
.toSet());
178 if (noUuidCapability
.size() > 0) {
179 logger
.warn("Cannot create a V2 group as some members don't have a UUID: {}",
180 String
.join(", ", noUuidCapability
));
184 final Set
<SignalProfile
> noGv2Capability
= members
.stream()
185 .map(profileProvider
::getProfile
)
186 .filter(profile
-> profile
!= null && !profile
.getCapabilities().gv2
)
187 .collect(Collectors
.toSet());
188 if (noGv2Capability
.size() > 0) {
189 logger
.warn("Cannot create a V2 group as some members don't support Groups V2: {}",
190 noGv2Capability
.stream().map(SignalProfile
::getName
).collect(Collectors
.joining(", ")));
197 public Pair
<DecryptedGroup
, GroupChange
> updateGroupV2(
198 GroupInfoV2 groupInfoV2
, String name
, File avatarFile
199 ) throws IOException
{
200 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
201 GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
203 GroupChange
.Actions
.Builder change
= name
!= null
204 ? groupOperations
.createModifyGroupTitle(name
)
205 : GroupChange
.Actions
.newBuilder();
207 if (avatarFile
!= null) {
208 final byte[] avatarBytes
= readAvatarBytes(avatarFile
);
209 String avatarCdnKey
= groupsV2Api
.uploadAvatar(avatarBytes
,
211 groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
));
212 change
.setModifyAvatar(GroupChange
.Actions
.ModifyAvatarAction
.newBuilder().setAvatar(avatarCdnKey
));
215 final Optional
<UUID
> uuid
= this.selfAddressProvider
.getSelfAddress().getUuid();
216 if (uuid
.isPresent()) {
217 change
.setSourceUuid(UuidUtil
.toByteString(uuid
.get()));
220 return commitChange(groupInfoV2
, change
);
223 public Pair
<DecryptedGroup
, GroupChange
> updateGroupV2(
224 GroupInfoV2 groupInfoV2
, Set
<SignalServiceAddress
> newMembers
225 ) throws IOException
{
226 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
227 GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
229 if (!areMembersValid(newMembers
)) {
230 throw new IOException("Failed to update group");
233 Set
<GroupCandidate
> candidates
= newMembers
.stream()
234 .map(member
-> new GroupCandidate(member
.getUuid().get(),
235 Optional
.fromNullable(profileKeyCredentialProvider
.getProfileKeyCredential(member
))))
236 .collect(Collectors
.toSet());
238 final GroupChange
.Actions
.Builder change
= groupOperations
.createModifyGroupMembershipChange(candidates
,
239 selfAddressProvider
.getSelfAddress().getUuid().get());
241 final Optional
<UUID
> uuid
= this.selfAddressProvider
.getSelfAddress().getUuid();
242 if (uuid
.isPresent()) {
243 change
.setSourceUuid(UuidUtil
.toByteString(uuid
.get()));
246 return commitChange(groupInfoV2
, change
);
249 public Pair
<DecryptedGroup
, GroupChange
> leaveGroup(GroupInfoV2 groupInfoV2
) throws IOException
{
250 List
<DecryptedPendingMember
> pendingMembersList
= groupInfoV2
.getGroup().getPendingMembersList();
251 final UUID selfUuid
= selfAddressProvider
.getSelfAddress().getUuid().get();
252 Optional
<DecryptedPendingMember
> selfPendingMember
= DecryptedGroupUtil
.findPendingByUuid(pendingMembersList
,
255 if (selfPendingMember
.isPresent()) {
256 return revokeInvites(groupInfoV2
, Set
.of(selfPendingMember
.get()));
258 return ejectMembers(groupInfoV2
, Set
.of(selfUuid
));
262 public GroupChange
joinGroup(
263 GroupMasterKey groupMasterKey
,
264 GroupLinkPassword groupLinkPassword
,
265 DecryptedGroupJoinInfo decryptedGroupJoinInfo
266 ) throws IOException
{
267 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
268 final GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
270 final SignalServiceAddress selfAddress
= this.selfAddressProvider
.getSelfAddress();
271 final ProfileKeyCredential profileKeyCredential
= profileKeyCredentialProvider
.getProfileKeyCredential(
273 if (profileKeyCredential
== null) {
274 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
277 boolean requestToJoin
= decryptedGroupJoinInfo
.getAddFromInviteLink()
278 == AccessControl
.AccessRequired
.ADMINISTRATOR
;
279 GroupChange
.Actions
.Builder change
= requestToJoin
280 ? groupOperations
.createGroupJoinRequest(profileKeyCredential
)
281 : groupOperations
.createGroupJoinDirect(profileKeyCredential
);
283 change
.setSourceUuid(UuidUtil
.toByteString(selfAddress
.getUuid().get()));
285 return commitChange(groupSecretParams
, decryptedGroupJoinInfo
.getRevision(), change
, groupLinkPassword
);
288 public Pair
<DecryptedGroup
, GroupChange
> acceptInvite(GroupInfoV2 groupInfoV2
) throws IOException
{
289 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
290 final GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
292 final SignalServiceAddress selfAddress
= this.selfAddressProvider
.getSelfAddress();
293 final ProfileKeyCredential profileKeyCredential
= profileKeyCredentialProvider
.getProfileKeyCredential(
295 if (profileKeyCredential
== null) {
296 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
299 final GroupChange
.Actions
.Builder change
= groupOperations
.createAcceptInviteChange(profileKeyCredential
);
301 final Optional
<UUID
> uuid
= selfAddress
.getUuid();
302 if (uuid
.isPresent()) {
303 change
.setSourceUuid(UuidUtil
.toByteString(uuid
.get()));
306 return commitChange(groupInfoV2
, change
);
309 public Pair
<DecryptedGroup
, GroupChange
> revokeInvites(
310 GroupInfoV2 groupInfoV2
, Set
<DecryptedPendingMember
> pendingMembers
311 ) throws IOException
{
312 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
313 final GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
314 final Set
<UuidCiphertext
> uuidCipherTexts
= pendingMembers
.stream().map(member
-> {
316 return new UuidCiphertext(member
.getUuidCipherText().toByteArray());
317 } catch (InvalidInputException e
) {
318 throw new AssertionError(e
);
320 }).collect(Collectors
.toSet());
321 return commitChange(groupInfoV2
, groupOperations
.createRemoveInvitationChange(uuidCipherTexts
));
324 public Pair
<DecryptedGroup
, GroupChange
> ejectMembers(GroupInfoV2 groupInfoV2
, Set
<UUID
> uuids
) throws IOException
{
325 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
326 final GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
327 return commitChange(groupInfoV2
, groupOperations
.createRemoveMembersChange(uuids
));
330 private Pair
<DecryptedGroup
, GroupChange
> commitChange(
331 GroupInfoV2 groupInfoV2
, GroupChange
.Actions
.Builder change
332 ) throws IOException
{
333 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
334 final GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
335 final DecryptedGroup previousGroupState
= groupInfoV2
.getGroup();
336 final int nextRevision
= previousGroupState
.getRevision() + 1;
337 final GroupChange
.Actions changeActions
= change
.setRevision(nextRevision
).build();
338 final DecryptedGroupChange decryptedChange
;
339 final DecryptedGroup decryptedGroupState
;
342 decryptedChange
= groupOperations
.decryptChange(changeActions
,
343 selfAddressProvider
.getSelfAddress().getUuid().get());
344 decryptedGroupState
= DecryptedGroupUtil
.apply(previousGroupState
, decryptedChange
);
345 } catch (VerificationFailedException
| InvalidGroupStateException
| NotAbleToApplyGroupV2ChangeException e
) {
346 throw new IOException(e
);
349 GroupChange signedGroupChange
= groupsV2Api
.patchGroup(changeActions
,
350 groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
),
353 return new Pair
<>(decryptedGroupState
, signedGroupChange
);
356 private GroupChange
commitChange(
357 GroupSecretParams groupSecretParams
,
359 GroupChange
.Actions
.Builder change
,
360 GroupLinkPassword password
361 ) throws IOException
{
362 final int nextRevision
= currentRevision
+ 1;
363 final GroupChange
.Actions changeActions
= change
.setRevision(nextRevision
).build();
365 return groupsV2Api
.patchGroup(changeActions
,
366 groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
),
367 Optional
.fromNullable(password
).transform(GroupLinkPassword
::serialize
));
370 public DecryptedGroup
getUpdatedDecryptedGroup(
371 DecryptedGroup group
, byte[] signedGroupChange
, GroupMasterKey groupMasterKey
374 final DecryptedGroupChange decryptedGroupChange
= getDecryptedGroupChange(signedGroupChange
,
376 if (decryptedGroupChange
== null) {
379 return DecryptedGroupUtil
.apply(group
, decryptedGroupChange
);
380 } catch (NotAbleToApplyGroupV2ChangeException e
) {
385 private DecryptedGroupChange
getDecryptedGroupChange(byte[] signedGroupChange
, GroupMasterKey groupMasterKey
) {
386 if (signedGroupChange
!= null) {
387 GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(GroupSecretParams
.deriveFromMasterKey(
391 return groupOperations
.decryptChange(GroupChange
.parseFrom(signedGroupChange
), true).orNull();
392 } catch (VerificationFailedException
| InvalidGroupStateException
| InvalidProtocolBufferException e
) {