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
.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
;
39 import java
.io
.FileInputStream
;
40 import java
.io
.IOException
;
41 import java
.io
.InputStream
;
42 import java
.util
.Collection
;
43 import java
.util
.List
;
45 import java
.util
.UUID
;
46 import java
.util
.stream
.Collectors
;
48 public class GroupHelper
{
50 final static Logger logger
= LoggerFactory
.getLogger(GroupHelper
.class);
52 private final ProfileKeyCredentialProvider profileKeyCredentialProvider
;
54 private final ProfileProvider profileProvider
;
56 private final SelfAddressProvider selfAddressProvider
;
58 private final GroupsV2Operations groupsV2Operations
;
60 private final GroupsV2Api groupsV2Api
;
62 private final GroupAuthorizationProvider groupAuthorizationProvider
;
65 final ProfileKeyCredentialProvider profileKeyCredentialProvider
,
66 final ProfileProvider profileProvider
,
67 final SelfAddressProvider selfAddressProvider
,
68 final GroupsV2Operations groupsV2Operations
,
69 final GroupsV2Api groupsV2Api
,
70 final GroupAuthorizationProvider groupAuthorizationProvider
72 this.profileKeyCredentialProvider
= profileKeyCredentialProvider
;
73 this.profileProvider
= profileProvider
;
74 this.selfAddressProvider
= selfAddressProvider
;
75 this.groupsV2Operations
= groupsV2Operations
;
76 this.groupsV2Api
= groupsV2Api
;
77 this.groupAuthorizationProvider
= groupAuthorizationProvider
;
80 public DecryptedGroup
getDecryptedGroup(final GroupSecretParams groupSecretParams
) {
82 final GroupsV2AuthorizationString groupsV2AuthorizationString
= groupAuthorizationProvider
.getAuthorizationForToday(
84 return groupsV2Api
.getGroup(groupSecretParams
, groupsV2AuthorizationString
);
85 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
86 logger
.warn("Failed to retrieve Group V2 info, ignoring: {}", e
.getMessage());
91 public DecryptedGroupJoinInfo
getDecryptedGroupJoinInfo(
92 GroupMasterKey groupMasterKey
, GroupLinkPassword password
93 ) throws IOException
, GroupLinkNotActiveException
{
94 GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
96 return groupsV2Api
.getGroupJoinInfo(groupSecretParams
,
97 Optional
.fromNullable(password
).transform(GroupLinkPassword
::serialize
),
98 groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
));
101 public GroupInfoV2
createGroupV2(
102 String name
, Collection
<SignalServiceAddress
> members
, String avatarFile
103 ) throws IOException
{
104 final byte[] avatarBytes
= readAvatarBytes(avatarFile
);
105 final GroupsV2Operations
.NewGroup newGroup
= buildNewGroupV2(name
, members
, avatarBytes
);
106 if (newGroup
== null) {
110 final GroupSecretParams groupSecretParams
= newGroup
.getGroupSecretParams();
112 final GroupsV2AuthorizationString groupAuthForToday
;
113 final DecryptedGroup decryptedGroup
;
115 groupAuthForToday
= groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
);
116 groupsV2Api
.putNewGroup(newGroup
, groupAuthForToday
);
117 decryptedGroup
= groupsV2Api
.getGroup(groupSecretParams
, groupAuthForToday
);
118 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
119 logger
.warn("Failed to create V2 group: {}", e
.getMessage());
122 if (decryptedGroup
== null) {
123 logger
.warn("Failed to create V2 group, unknown error!");
127 final GroupIdV2 groupId
= GroupUtils
.getGroupIdV2(groupSecretParams
);
128 final GroupMasterKey masterKey
= groupSecretParams
.getMasterKey();
129 GroupInfoV2 g
= new GroupInfoV2(groupId
, masterKey
);
130 g
.setGroup(decryptedGroup
);
135 private byte[] readAvatarBytes(final String avatarFile
) throws IOException
{
136 final byte[] avatarBytes
;
137 try (InputStream avatar
= avatarFile
== null ?
null : new FileInputStream(avatarFile
)) {
138 avatarBytes
= avatar
== null ?
null : IOUtils
.readFully(avatar
);
143 private GroupsV2Operations
.NewGroup
buildNewGroupV2(
144 String name
, Collection
<SignalServiceAddress
> members
, byte[] avatar
146 final ProfileKeyCredential profileKeyCredential
= profileKeyCredentialProvider
.getProfileKeyCredential(
147 selfAddressProvider
.getSelfAddress());
148 if (profileKeyCredential
== null) {
149 logger
.warn("Cannot create a V2 group as self does not have a versioned profile");
153 if (!areMembersValid(members
)) return null;
155 GroupCandidate self
= new GroupCandidate(selfAddressProvider
.getSelfAddress().getUuid().orNull(),
156 Optional
.fromNullable(profileKeyCredential
));
157 Set
<GroupCandidate
> candidates
= members
.stream()
158 .map(member
-> new GroupCandidate(member
.getUuid().get(),
159 Optional
.fromNullable(profileKeyCredentialProvider
.getProfileKeyCredential(member
))))
160 .collect(Collectors
.toSet());
162 final GroupSecretParams groupSecretParams
= GroupSecretParams
.generate();
163 return groupsV2Operations
.createNewGroup(groupSecretParams
,
165 Optional
.fromNullable(avatar
),
172 private boolean areMembersValid(final Collection
<SignalServiceAddress
> members
) {
173 final Set
<String
> noUuidCapability
= members
.stream()
174 .filter(address
-> !address
.getUuid().isPresent())
175 .map(SignalServiceAddress
::getLegacyIdentifier
)
176 .collect(Collectors
.toSet());
177 if (noUuidCapability
.size() > 0) {
178 logger
.warn("Cannot create a V2 group as some members don't have a UUID: {}",
179 String
.join(", ", noUuidCapability
));
183 final Set
<SignalProfile
> noGv2Capability
= members
.stream()
184 .map(profileProvider
::getProfile
)
185 .filter(profile
-> profile
!= null && !profile
.getCapabilities().gv2
)
186 .collect(Collectors
.toSet());
187 if (noGv2Capability
.size() > 0) {
188 logger
.warn("Cannot create a V2 group as some members don't support Groups V2: {}",
189 noGv2Capability
.stream().map(SignalProfile
::getName
).collect(Collectors
.joining(", ")));
196 public Pair
<DecryptedGroup
, GroupChange
> updateGroupV2(
197 GroupInfoV2 groupInfoV2
, String name
, String avatarFile
198 ) throws IOException
{
199 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
200 GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
202 GroupChange
.Actions
.Builder change
= name
!= null
203 ? groupOperations
.createModifyGroupTitle(name
)
204 : GroupChange
.Actions
.newBuilder();
206 if (avatarFile
!= null) {
207 final byte[] avatarBytes
= readAvatarBytes(avatarFile
);
208 String avatarCdnKey
= groupsV2Api
.uploadAvatar(avatarBytes
,
210 groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
));
211 change
.setModifyAvatar(GroupChange
.Actions
.ModifyAvatarAction
.newBuilder().setAvatar(avatarCdnKey
));
214 final Optional
<UUID
> uuid
= this.selfAddressProvider
.getSelfAddress().getUuid();
215 if (uuid
.isPresent()) {
216 change
.setSourceUuid(UuidUtil
.toByteString(uuid
.get()));
219 return commitChange(groupInfoV2
, change
);
222 public Pair
<DecryptedGroup
, GroupChange
> updateGroupV2(
223 GroupInfoV2 groupInfoV2
, Set
<SignalServiceAddress
> newMembers
224 ) throws IOException
{
225 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
226 GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
228 if (!areMembersValid(newMembers
)) {
229 throw new IOException("Failed to update group");
232 Set
<GroupCandidate
> candidates
= newMembers
.stream()
233 .map(member
-> new GroupCandidate(member
.getUuid().get(),
234 Optional
.fromNullable(profileKeyCredentialProvider
.getProfileKeyCredential(member
))))
235 .collect(Collectors
.toSet());
237 final GroupChange
.Actions
.Builder change
= groupOperations
.createModifyGroupMembershipChange(candidates
,
238 selfAddressProvider
.getSelfAddress().getUuid().get());
240 final Optional
<UUID
> uuid
= this.selfAddressProvider
.getSelfAddress().getUuid();
241 if (uuid
.isPresent()) {
242 change
.setSourceUuid(UuidUtil
.toByteString(uuid
.get()));
245 return commitChange(groupInfoV2
, change
);
248 public Pair
<DecryptedGroup
, GroupChange
> leaveGroup(GroupInfoV2 groupInfoV2
) throws IOException
{
249 List
<DecryptedPendingMember
> pendingMembersList
= groupInfoV2
.getGroup().getPendingMembersList();
250 final UUID selfUuid
= selfAddressProvider
.getSelfAddress().getUuid().get();
251 Optional
<DecryptedPendingMember
> selfPendingMember
= DecryptedGroupUtil
.findPendingByUuid(pendingMembersList
,
254 if (selfPendingMember
.isPresent()) {
255 return revokeInvites(groupInfoV2
, Set
.of(selfPendingMember
.get()));
257 return ejectMembers(groupInfoV2
, Set
.of(selfUuid
));
261 public GroupChange
joinGroup(
262 GroupMasterKey groupMasterKey
,
263 GroupLinkPassword groupLinkPassword
,
264 DecryptedGroupJoinInfo decryptedGroupJoinInfo
265 ) throws IOException
{
266 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
267 final GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
269 final SignalServiceAddress selfAddress
= this.selfAddressProvider
.getSelfAddress();
270 final ProfileKeyCredential profileKeyCredential
= profileKeyCredentialProvider
.getProfileKeyCredential(
272 if (profileKeyCredential
== null) {
273 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
276 boolean requestToJoin
= decryptedGroupJoinInfo
.getAddFromInviteLink()
277 == AccessControl
.AccessRequired
.ADMINISTRATOR
;
278 GroupChange
.Actions
.Builder change
= requestToJoin
279 ? groupOperations
.createGroupJoinRequest(profileKeyCredential
)
280 : groupOperations
.createGroupJoinDirect(profileKeyCredential
);
282 change
.setSourceUuid(UuidUtil
.toByteString(selfAddress
.getUuid().get()));
284 return commitChange(groupSecretParams
, decryptedGroupJoinInfo
.getRevision(), change
, groupLinkPassword
);
287 public Pair
<DecryptedGroup
, GroupChange
> acceptInvite(GroupInfoV2 groupInfoV2
) throws IOException
{
288 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
289 final GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
291 final SignalServiceAddress selfAddress
= this.selfAddressProvider
.getSelfAddress();
292 final ProfileKeyCredential profileKeyCredential
= profileKeyCredentialProvider
.getProfileKeyCredential(
294 if (profileKeyCredential
== null) {
295 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
298 final GroupChange
.Actions
.Builder change
= groupOperations
.createAcceptInviteChange(profileKeyCredential
);
300 final Optional
<UUID
> uuid
= selfAddress
.getUuid();
301 if (uuid
.isPresent()) {
302 change
.setSourceUuid(UuidUtil
.toByteString(uuid
.get()));
305 return commitChange(groupInfoV2
, change
);
308 public Pair
<DecryptedGroup
, GroupChange
> revokeInvites(
309 GroupInfoV2 groupInfoV2
, Set
<DecryptedPendingMember
> pendingMembers
310 ) throws IOException
{
311 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
312 final GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
313 final Set
<UuidCiphertext
> uuidCipherTexts
= pendingMembers
.stream().map(member
-> {
315 return new UuidCiphertext(member
.getUuidCipherText().toByteArray());
316 } catch (InvalidInputException e
) {
317 throw new AssertionError(e
);
319 }).collect(Collectors
.toSet());
320 return commitChange(groupInfoV2
, groupOperations
.createRemoveInvitationChange(uuidCipherTexts
));
323 public Pair
<DecryptedGroup
, GroupChange
> ejectMembers(GroupInfoV2 groupInfoV2
, Set
<UUID
> uuids
) throws IOException
{
324 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
325 final GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
326 return commitChange(groupInfoV2
, groupOperations
.createRemoveMembersChange(uuids
));
329 private Pair
<DecryptedGroup
, GroupChange
> commitChange(
330 GroupInfoV2 groupInfoV2
, GroupChange
.Actions
.Builder change
331 ) throws IOException
{
332 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
333 final GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
334 final DecryptedGroup previousGroupState
= groupInfoV2
.getGroup();
335 final int nextRevision
= previousGroupState
.getRevision() + 1;
336 final GroupChange
.Actions changeActions
= change
.setRevision(nextRevision
).build();
337 final DecryptedGroupChange decryptedChange
;
338 final DecryptedGroup decryptedGroupState
;
341 decryptedChange
= groupOperations
.decryptChange(changeActions
,
342 selfAddressProvider
.getSelfAddress().getUuid().get());
343 decryptedGroupState
= DecryptedGroupUtil
.apply(previousGroupState
, decryptedChange
);
344 } catch (VerificationFailedException
| InvalidGroupStateException
| NotAbleToApplyGroupV2ChangeException e
) {
345 throw new IOException(e
);
348 GroupChange signedGroupChange
= groupsV2Api
.patchGroup(changeActions
,
349 groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
),
352 return new Pair
<>(decryptedGroupState
, signedGroupChange
);
355 private GroupChange
commitChange(
356 GroupSecretParams groupSecretParams
,
358 GroupChange
.Actions
.Builder change
,
359 GroupLinkPassword password
360 ) throws IOException
{
361 final int nextRevision
= currentRevision
+ 1;
362 final GroupChange
.Actions changeActions
= change
.setRevision(nextRevision
).build();
364 return groupsV2Api
.patchGroup(changeActions
,
365 groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
),
366 Optional
.fromNullable(password
).transform(GroupLinkPassword
::serialize
));
369 public DecryptedGroup
getUpdatedDecryptedGroup(
370 DecryptedGroup group
, byte[] signedGroupChange
, GroupMasterKey groupMasterKey
373 final DecryptedGroupChange decryptedGroupChange
= getDecryptedGroupChange(signedGroupChange
,
375 if (decryptedGroupChange
== null) {
378 return DecryptedGroupUtil
.apply(group
, decryptedGroupChange
);
379 } catch (NotAbleToApplyGroupV2ChangeException e
) {
384 private DecryptedGroupChange
getDecryptedGroupChange(byte[] signedGroupChange
, GroupMasterKey groupMasterKey
) {
385 if (signedGroupChange
!= null) {
386 GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(GroupSecretParams
.deriveFromMasterKey(
390 return groupOperations
.decryptChange(GroupChange
.parseFrom(signedGroupChange
), true).orNull();
391 } catch (VerificationFailedException
| InvalidGroupStateException
| InvalidProtocolBufferException e
) {