1 package org
.asamk
.signal
.manager
.helper
;
3 import com
.google
.protobuf
.InvalidProtocolBufferException
;
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
;
39 import java
.io
.FileInputStream
;
40 import java
.io
.IOException
;
41 import java
.io
.InputStream
;
43 import java
.util
.UUID
;
44 import java
.util
.stream
.Collectors
;
46 public class GroupV2Helper
{
48 private final static Logger logger
= LoggerFactory
.getLogger(GroupV2Helper
.class);
50 private final ProfileKeyCredentialProvider profileKeyCredentialProvider
;
52 private final ProfileProvider profileProvider
;
54 private final SelfRecipientIdProvider selfRecipientIdProvider
;
56 private final GroupsV2Operations groupsV2Operations
;
58 private final GroupsV2Api groupsV2Api
;
60 private final GroupAuthorizationProvider groupAuthorizationProvider
;
62 private final SignalServiceAddressResolver addressResolver
;
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
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
;
82 public DecryptedGroup
getDecryptedGroup(final GroupSecretParams groupSecretParams
) {
84 final var groupsV2AuthorizationString
= groupAuthorizationProvider
.getAuthorizationForToday(
86 return groupsV2Api
.getGroup(groupSecretParams
, groupsV2AuthorizationString
);
87 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
88 logger
.warn("Failed to retrieve Group V2 info, ignoring: {}", e
.getMessage());
93 public DecryptedGroupJoinInfo
getDecryptedGroupJoinInfo(
94 GroupMasterKey groupMasterKey
, GroupLinkPassword password
95 ) throws IOException
, GroupLinkNotActiveException
{
96 var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
98 return groupsV2Api
.getGroupJoinInfo(groupSecretParams
,
99 Optional
.fromNullable(password
).transform(GroupLinkPassword
::serialize
),
100 groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
));
103 public Pair
<GroupInfoV2
, DecryptedGroup
> createGroup(
104 String name
, Set
<RecipientId
> members
, File avatarFile
105 ) throws IOException
{
106 final var avatarBytes
= readAvatarBytes(avatarFile
);
107 final var newGroup
= buildNewGroup(name
, members
, avatarBytes
);
108 if (newGroup
== null) {
112 final var groupSecretParams
= newGroup
.getGroupSecretParams();
114 final GroupsV2AuthorizationString groupAuthForToday
;
115 final DecryptedGroup decryptedGroup
;
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());
124 if (decryptedGroup
== null) {
125 logger
.warn("Failed to create V2 group, unknown error!");
129 final var groupId
= GroupUtils
.getGroupIdV2(groupSecretParams
);
130 final var masterKey
= groupSecretParams
.getMasterKey();
131 var g
= new GroupInfoV2(groupId
, masterKey
);
133 return new Pair
<>(g
, 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
buildNewGroup(
145 String name
, Set
<RecipientId
> members
, byte[] avatar
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");
153 if (!areMembersValid(members
)) return null;
155 var self
= new GroupCandidate(addressResolver
.resolveSignalServiceAddress(selfRecipientIdProvider
.getSelfRecipientId())
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());
163 final var groupSecretParams
= GroupSecretParams
.generate();
164 return groupsV2Operations
.createNewGroup(groupSecretParams
,
166 Optional
.fromNullable(avatar
),
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
));
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(", ")));
198 public Pair
<DecryptedGroup
, GroupChange
> updateGroup(
199 GroupInfoV2 groupInfoV2
, String name
, String description
, File avatarFile
200 ) throws IOException
{
201 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
202 var groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
204 var change
= name
!= null ? groupOperations
.createModifyGroupTitle(name
) : GroupChange
.Actions
.newBuilder();
206 if (description
!= null) {
207 change
.setModifyDescription(groupOperations
.createModifyGroupDescription(description
));
210 if (avatarFile
!= null) {
211 final var avatarBytes
= readAvatarBytes(avatarFile
);
212 var avatarCdnKey
= groupsV2Api
.uploadAvatar(avatarBytes
,
214 groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
));
215 change
.setModifyAvatar(GroupChange
.Actions
.ModifyAvatarAction
.newBuilder().setAvatar(avatarCdnKey
));
218 final var uuid
= addressResolver
.resolveSignalServiceAddress(this.selfRecipientIdProvider
.getSelfRecipientId())
220 if (uuid
.isPresent()) {
221 change
.setSourceUuid(UuidUtil
.toByteString(uuid
.get()));
224 return commitChange(groupInfoV2
, change
);
227 public Pair
<DecryptedGroup
, GroupChange
> addMembers(
228 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> newMembers
229 ) throws IOException
{
230 GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
232 if (!areMembersValid(newMembers
)) {
233 throw new IOException("Failed to update group");
236 var candidates
= newMembers
.stream()
237 .map(member
-> new GroupCandidate(addressResolver
.resolveSignalServiceAddress(member
).getUuid().get(),
238 Optional
.fromNullable(profileKeyCredentialProvider
.getProfileKeyCredential(member
))))
239 .collect(Collectors
.toSet());
241 final var uuid
= addressResolver
.resolveSignalServiceAddress(selfRecipientIdProvider
.getSelfRecipientId())
244 final var change
= groupOperations
.createModifyGroupMembershipChange(candidates
, uuid
);
246 change
.setSourceUuid(UuidUtil
.toByteString(uuid
));
248 return commitChange(groupInfoV2
, change
);
251 public Pair
<DecryptedGroup
, GroupChange
> leaveGroup(GroupInfoV2 groupInfoV2
) throws IOException
{
252 var pendingMembersList
= groupInfoV2
.getGroup().getPendingMembersList();
253 final var selfUuid
= addressResolver
.resolveSignalServiceAddress(selfRecipientIdProvider
.getSelfRecipientId())
256 var selfPendingMember
= DecryptedGroupUtil
.findPendingByUuid(pendingMembersList
, selfUuid
);
258 if (selfPendingMember
.isPresent()) {
259 return revokeInvites(groupInfoV2
, Set
.of(selfPendingMember
.get()));
261 return ejectMembers(groupInfoV2
, Set
.of(selfUuid
));
265 public Pair
<DecryptedGroup
, GroupChange
> removeMembers(
266 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> members
267 ) throws IOException
{
268 final var memberUuids
= members
.stream()
269 .map(addressResolver
::resolveSignalServiceAddress
)
270 .map(SignalServiceAddress
::getUuid
)
271 .filter(Optional
::isPresent
)
273 .collect(Collectors
.toSet());
274 return ejectMembers(groupInfoV2
, memberUuids
);
277 public Pair
<DecryptedGroup
, GroupChange
> revokeInvitedMembers(
278 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> members
279 ) throws IOException
{
280 var pendingMembersList
= groupInfoV2
.getGroup().getPendingMembersList();
281 final var memberUuids
= members
.stream()
282 .map(addressResolver
::resolveSignalServiceAddress
)
283 .map(SignalServiceAddress
::getUuid
)
284 .filter(Optional
::isPresent
)
286 .map(uuid
-> DecryptedGroupUtil
.findPendingByUuid(pendingMembersList
, uuid
))
287 .filter(Optional
::isPresent
)
289 .collect(Collectors
.toSet());
290 return revokeInvites(groupInfoV2
, memberUuids
);
293 public GroupChange
joinGroup(
294 GroupMasterKey groupMasterKey
,
295 GroupLinkPassword groupLinkPassword
,
296 DecryptedGroupJoinInfo decryptedGroupJoinInfo
297 ) throws IOException
{
298 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
299 final var groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
301 final var selfRecipientId
= this.selfRecipientIdProvider
.getSelfRecipientId();
302 final var profileKeyCredential
= profileKeyCredentialProvider
.getProfileKeyCredential(selfRecipientId
);
303 if (profileKeyCredential
== null) {
304 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
307 var requestToJoin
= decryptedGroupJoinInfo
.getAddFromInviteLink() == AccessControl
.AccessRequired
.ADMINISTRATOR
;
308 var change
= requestToJoin
309 ? groupOperations
.createGroupJoinRequest(profileKeyCredential
)
310 : groupOperations
.createGroupJoinDirect(profileKeyCredential
);
312 change
.setSourceUuid(UuidUtil
.toByteString(addressResolver
.resolveSignalServiceAddress(selfRecipientId
)
316 return commitChange(groupSecretParams
, decryptedGroupJoinInfo
.getRevision(), change
, groupLinkPassword
);
319 public Pair
<DecryptedGroup
, GroupChange
> acceptInvite(GroupInfoV2 groupInfoV2
) throws IOException
{
320 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
322 final var selfRecipientId
= this.selfRecipientIdProvider
.getSelfRecipientId();
323 final var profileKeyCredential
= profileKeyCredentialProvider
.getProfileKeyCredential(selfRecipientId
);
324 if (profileKeyCredential
== null) {
325 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
328 final var change
= groupOperations
.createAcceptInviteChange(profileKeyCredential
);
330 final var uuid
= addressResolver
.resolveSignalServiceAddress(selfRecipientId
).getUuid();
331 if (uuid
.isPresent()) {
332 change
.setSourceUuid(UuidUtil
.toByteString(uuid
.get()));
335 return commitChange(groupInfoV2
, change
);
338 public Pair
<DecryptedGroup
, GroupChange
> setMemberAdmin(
339 GroupInfoV2 groupInfoV2
, RecipientId recipientId
, boolean admin
340 ) throws IOException
{
341 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
342 final var address
= addressResolver
.resolveSignalServiceAddress(recipientId
);
343 final var newRole
= admin ? Member
.Role
.ADMINISTRATOR
: Member
.Role
.DEFAULT
;
344 final var change
= groupOperations
.createChangeMemberRole(address
.getUuid().get(), newRole
);
345 return commitChange(groupInfoV2
, change
);
348 private GroupsV2Operations
.GroupOperations
getGroupOperations(final GroupInfoV2 groupInfoV2
) {
349 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
350 return groupsV2Operations
.forGroup(groupSecretParams
);
353 private Pair
<DecryptedGroup
, GroupChange
> revokeInvites(
354 GroupInfoV2 groupInfoV2
, Set
<DecryptedPendingMember
> pendingMembers
355 ) throws IOException
{
356 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
357 final var uuidCipherTexts
= pendingMembers
.stream().map(member
-> {
359 return new UuidCiphertext(member
.getUuidCipherText().toByteArray());
360 } catch (InvalidInputException e
) {
361 throw new AssertionError(e
);
363 }).collect(Collectors
.toSet());
364 return commitChange(groupInfoV2
, groupOperations
.createRemoveInvitationChange(uuidCipherTexts
));
367 private Pair
<DecryptedGroup
, GroupChange
> ejectMembers(
368 GroupInfoV2 groupInfoV2
, Set
<UUID
> uuids
369 ) throws IOException
{
370 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
371 return commitChange(groupInfoV2
, groupOperations
.createRemoveMembersChange(uuids
));
374 private Pair
<DecryptedGroup
, GroupChange
> commitChange(
375 GroupInfoV2 groupInfoV2
, GroupChange
.Actions
.Builder change
376 ) throws IOException
{
377 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
378 final var groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
379 final var previousGroupState
= groupInfoV2
.getGroup();
380 final var nextRevision
= previousGroupState
.getRevision() + 1;
381 final var changeActions
= change
.setRevision(nextRevision
).build();
382 final DecryptedGroupChange decryptedChange
;
383 final DecryptedGroup decryptedGroupState
;
386 decryptedChange
= groupOperations
.decryptChange(changeActions
,
387 addressResolver
.resolveSignalServiceAddress(selfRecipientIdProvider
.getSelfRecipientId())
390 decryptedGroupState
= DecryptedGroupUtil
.apply(previousGroupState
, decryptedChange
);
391 } catch (VerificationFailedException
| InvalidGroupStateException
| NotAbleToApplyGroupV2ChangeException e
) {
392 throw new IOException(e
);
395 var signedGroupChange
= groupsV2Api
.patchGroup(changeActions
,
396 groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
),
399 return new Pair
<>(decryptedGroupState
, signedGroupChange
);
402 private GroupChange
commitChange(
403 GroupSecretParams groupSecretParams
,
405 GroupChange
.Actions
.Builder change
,
406 GroupLinkPassword password
407 ) throws IOException
{
408 final var nextRevision
= currentRevision
+ 1;
409 final var changeActions
= change
.setRevision(nextRevision
).build();
411 return groupsV2Api
.patchGroup(changeActions
,
412 groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
),
413 Optional
.fromNullable(password
).transform(GroupLinkPassword
::serialize
));
416 public DecryptedGroup
getUpdatedDecryptedGroup(
417 DecryptedGroup group
, byte[] signedGroupChange
, GroupMasterKey groupMasterKey
420 final var decryptedGroupChange
= getDecryptedGroupChange(signedGroupChange
, groupMasterKey
);
421 if (decryptedGroupChange
== null) {
424 return DecryptedGroupUtil
.apply(group
, decryptedGroupChange
);
425 } catch (NotAbleToApplyGroupV2ChangeException e
) {
430 private DecryptedGroupChange
getDecryptedGroupChange(byte[] signedGroupChange
, GroupMasterKey groupMasterKey
) {
431 if (signedGroupChange
!= null) {
432 var groupOperations
= groupsV2Operations
.forGroup(GroupSecretParams
.deriveFromMasterKey(groupMasterKey
));
435 return groupOperations
.decryptChange(GroupChange
.parseFrom(signedGroupChange
), true).orNull();
436 } catch (VerificationFailedException
| InvalidGroupStateException
| InvalidProtocolBufferException e
) {