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
.profiles
.SignalProfile
;
9 import org
.asamk
.signal
.manager
.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
.slf4j
.Logger
;
23 import org
.slf4j
.LoggerFactory
;
24 import org
.whispersystems
.libsignal
.util
.Pair
;
25 import org
.whispersystems
.libsignal
.util
.guava
.Optional
;
26 import org
.whispersystems
.signalservice
.api
.groupsv2
.DecryptedGroupUtil
;
27 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupCandidate
;
28 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupLinkNotActiveException
;
29 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2Api
;
30 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2AuthorizationString
;
31 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2Operations
;
32 import org
.whispersystems
.signalservice
.api
.groupsv2
.InvalidGroupStateException
;
33 import org
.whispersystems
.signalservice
.api
.groupsv2
.NotAbleToApplyGroupV2ChangeException
;
34 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
35 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
38 import java
.io
.FileInputStream
;
39 import java
.io
.IOException
;
40 import java
.io
.InputStream
;
41 import java
.util
.Collection
;
43 import java
.util
.UUID
;
44 import java
.util
.stream
.Collectors
;
46 public class GroupHelper
{
48 private final static Logger logger
= LoggerFactory
.getLogger(GroupHelper
.class);
50 private final ProfileKeyCredentialProvider profileKeyCredentialProvider
;
52 private final ProfileProvider profileProvider
;
54 private final SelfAddressProvider selfAddressProvider
;
56 private final GroupsV2Operations groupsV2Operations
;
58 private final GroupsV2Api groupsV2Api
;
60 private final GroupAuthorizationProvider groupAuthorizationProvider
;
63 final ProfileKeyCredentialProvider profileKeyCredentialProvider
,
64 final ProfileProvider profileProvider
,
65 final SelfAddressProvider selfAddressProvider
,
66 final GroupsV2Operations groupsV2Operations
,
67 final GroupsV2Api groupsV2Api
,
68 final GroupAuthorizationProvider groupAuthorizationProvider
70 this.profileKeyCredentialProvider
= profileKeyCredentialProvider
;
71 this.profileProvider
= profileProvider
;
72 this.selfAddressProvider
= selfAddressProvider
;
73 this.groupsV2Operations
= groupsV2Operations
;
74 this.groupsV2Api
= groupsV2Api
;
75 this.groupAuthorizationProvider
= groupAuthorizationProvider
;
78 public DecryptedGroup
getDecryptedGroup(final GroupSecretParams groupSecretParams
) {
80 final var groupsV2AuthorizationString
= groupAuthorizationProvider
.getAuthorizationForToday(
82 return groupsV2Api
.getGroup(groupSecretParams
, groupsV2AuthorizationString
);
83 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
84 logger
.warn("Failed to retrieve Group V2 info, ignoring: {}", e
.getMessage());
89 public DecryptedGroupJoinInfo
getDecryptedGroupJoinInfo(
90 GroupMasterKey groupMasterKey
, GroupLinkPassword password
91 ) throws IOException
, GroupLinkNotActiveException
{
92 var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
94 return groupsV2Api
.getGroupJoinInfo(groupSecretParams
,
95 Optional
.fromNullable(password
).transform(GroupLinkPassword
::serialize
),
96 groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
));
99 public GroupInfoV2
createGroupV2(
100 String name
, Collection
<SignalServiceAddress
> members
, File avatarFile
101 ) throws IOException
{
102 final var avatarBytes
= readAvatarBytes(avatarFile
);
103 final var newGroup
= buildNewGroupV2(name
, members
, avatarBytes
);
104 if (newGroup
== null) {
108 final var groupSecretParams
= newGroup
.getGroupSecretParams();
110 final GroupsV2AuthorizationString groupAuthForToday
;
111 final DecryptedGroup decryptedGroup
;
113 groupAuthForToday
= groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
);
114 groupsV2Api
.putNewGroup(newGroup
, groupAuthForToday
);
115 decryptedGroup
= groupsV2Api
.getGroup(groupSecretParams
, groupAuthForToday
);
116 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
117 logger
.warn("Failed to create V2 group: {}", e
.getMessage());
120 if (decryptedGroup
== null) {
121 logger
.warn("Failed to create V2 group, unknown error!");
125 final var groupId
= GroupUtils
.getGroupIdV2(groupSecretParams
);
126 final var masterKey
= groupSecretParams
.getMasterKey();
127 var g
= new GroupInfoV2(groupId
, masterKey
);
128 g
.setGroup(decryptedGroup
);
133 private byte[] readAvatarBytes(final File avatarFile
) throws IOException
{
134 final byte[] avatarBytes
;
135 try (InputStream avatar
= avatarFile
== null ?
null : new FileInputStream(avatarFile
)) {
136 avatarBytes
= avatar
== null ?
null : IOUtils
.readFully(avatar
);
141 private GroupsV2Operations
.NewGroup
buildNewGroupV2(
142 String name
, Collection
<SignalServiceAddress
> members
, byte[] avatar
144 final var profileKeyCredential
= profileKeyCredentialProvider
.getProfileKeyCredential(selfAddressProvider
.getSelfAddress());
145 if (profileKeyCredential
== null) {
146 logger
.warn("Cannot create a V2 group as self does not have a versioned profile");
150 if (!areMembersValid(members
)) return null;
152 var self
= new GroupCandidate(selfAddressProvider
.getSelfAddress().getUuid().orNull(),
153 Optional
.fromNullable(profileKeyCredential
));
154 var candidates
= members
.stream()
155 .map(member
-> new GroupCandidate(member
.getUuid().get(),
156 Optional
.fromNullable(profileKeyCredentialProvider
.getProfileKeyCredential(member
))))
157 .collect(Collectors
.toSet());
159 final var groupSecretParams
= GroupSecretParams
.generate();
160 return groupsV2Operations
.createNewGroup(groupSecretParams
,
162 Optional
.fromNullable(avatar
),
169 private boolean areMembersValid(final Collection
<SignalServiceAddress
> members
) {
170 final var noUuidCapability
= members
.stream()
171 .filter(address
-> !address
.getUuid().isPresent())
172 .map(SignalServiceAddress
::getLegacyIdentifier
)
173 .collect(Collectors
.toSet());
174 if (noUuidCapability
.size() > 0) {
175 logger
.warn("Cannot create a V2 group as some members don't have a UUID: {}",
176 String
.join(", ", noUuidCapability
));
180 final var noGv2Capability
= members
.stream()
181 .map(profileProvider
::getProfile
)
182 .filter(profile
-> profile
!= null && !profile
.getCapabilities().gv2
)
183 .collect(Collectors
.toSet());
184 if (noGv2Capability
.size() > 0) {
185 logger
.warn("Cannot create a V2 group as some members don't support Groups V2: {}",
186 noGv2Capability
.stream().map(SignalProfile
::getName
).collect(Collectors
.joining(", ")));
193 public Pair
<DecryptedGroup
, GroupChange
> updateGroupV2(
194 GroupInfoV2 groupInfoV2
, String name
, File avatarFile
195 ) throws IOException
{
196 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
197 var groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
199 var change
= name
!= null ? groupOperations
.createModifyGroupTitle(name
) : GroupChange
.Actions
.newBuilder();
201 if (avatarFile
!= null) {
202 final var avatarBytes
= readAvatarBytes(avatarFile
);
203 var avatarCdnKey
= groupsV2Api
.uploadAvatar(avatarBytes
,
205 groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
));
206 change
.setModifyAvatar(GroupChange
.Actions
.ModifyAvatarAction
.newBuilder().setAvatar(avatarCdnKey
));
209 final var uuid
= this.selfAddressProvider
.getSelfAddress().getUuid();
210 if (uuid
.isPresent()) {
211 change
.setSourceUuid(UuidUtil
.toByteString(uuid
.get()));
214 return commitChange(groupInfoV2
, change
);
217 public Pair
<DecryptedGroup
, GroupChange
> updateGroupV2(
218 GroupInfoV2 groupInfoV2
, Set
<SignalServiceAddress
> newMembers
219 ) throws IOException
{
220 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
221 var groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
223 if (!areMembersValid(newMembers
)) {
224 throw new IOException("Failed to update group");
227 var candidates
= newMembers
.stream()
228 .map(member
-> new GroupCandidate(member
.getUuid().get(),
229 Optional
.fromNullable(profileKeyCredentialProvider
.getProfileKeyCredential(member
))))
230 .collect(Collectors
.toSet());
232 final var change
= groupOperations
.createModifyGroupMembershipChange(candidates
,
233 selfAddressProvider
.getSelfAddress().getUuid().get());
235 final var uuid
= this.selfAddressProvider
.getSelfAddress().getUuid();
236 if (uuid
.isPresent()) {
237 change
.setSourceUuid(UuidUtil
.toByteString(uuid
.get()));
240 return commitChange(groupInfoV2
, change
);
243 public Pair
<DecryptedGroup
, GroupChange
> leaveGroup(GroupInfoV2 groupInfoV2
) throws IOException
{
244 var pendingMembersList
= groupInfoV2
.getGroup().getPendingMembersList();
245 final var selfUuid
= selfAddressProvider
.getSelfAddress().getUuid().get();
246 var selfPendingMember
= DecryptedGroupUtil
.findPendingByUuid(pendingMembersList
, selfUuid
);
248 if (selfPendingMember
.isPresent()) {
249 return revokeInvites(groupInfoV2
, Set
.of(selfPendingMember
.get()));
251 return ejectMembers(groupInfoV2
, Set
.of(selfUuid
));
255 public GroupChange
joinGroup(
256 GroupMasterKey groupMasterKey
,
257 GroupLinkPassword groupLinkPassword
,
258 DecryptedGroupJoinInfo decryptedGroupJoinInfo
259 ) throws IOException
{
260 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
261 final var groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
263 final var selfAddress
= this.selfAddressProvider
.getSelfAddress();
264 final var profileKeyCredential
= profileKeyCredentialProvider
.getProfileKeyCredential(selfAddress
);
265 if (profileKeyCredential
== null) {
266 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
269 var requestToJoin
= decryptedGroupJoinInfo
.getAddFromInviteLink() == AccessControl
.AccessRequired
.ADMINISTRATOR
;
270 var 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 var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
281 final var groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
283 final var selfAddress
= this.selfAddressProvider
.getSelfAddress();
284 final var profileKeyCredential
= profileKeyCredentialProvider
.getProfileKeyCredential(selfAddress
);
285 if (profileKeyCredential
== null) {
286 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
289 final var change
= groupOperations
.createAcceptInviteChange(profileKeyCredential
);
291 final var uuid
= selfAddress
.getUuid();
292 if (uuid
.isPresent()) {
293 change
.setSourceUuid(UuidUtil
.toByteString(uuid
.get()));
296 return commitChange(groupInfoV2
, change
);
299 public Pair
<DecryptedGroup
, GroupChange
> revokeInvites(
300 GroupInfoV2 groupInfoV2
, Set
<DecryptedPendingMember
> pendingMembers
301 ) throws IOException
{
302 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
303 final var groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
304 final var uuidCipherTexts
= pendingMembers
.stream().map(member
-> {
306 return new UuidCiphertext(member
.getUuidCipherText().toByteArray());
307 } catch (InvalidInputException e
) {
308 throw new AssertionError(e
);
310 }).collect(Collectors
.toSet());
311 return commitChange(groupInfoV2
, groupOperations
.createRemoveInvitationChange(uuidCipherTexts
));
314 public Pair
<DecryptedGroup
, GroupChange
> ejectMembers(GroupInfoV2 groupInfoV2
, Set
<UUID
> uuids
) throws IOException
{
315 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
316 final var groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
317 return commitChange(groupInfoV2
, groupOperations
.createRemoveMembersChange(uuids
));
320 private Pair
<DecryptedGroup
, GroupChange
> commitChange(
321 GroupInfoV2 groupInfoV2
, GroupChange
.Actions
.Builder change
322 ) throws IOException
{
323 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
324 final var groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
325 final var previousGroupState
= groupInfoV2
.getGroup();
326 final var nextRevision
= previousGroupState
.getRevision() + 1;
327 final var changeActions
= change
.setRevision(nextRevision
).build();
328 final DecryptedGroupChange decryptedChange
;
329 final DecryptedGroup decryptedGroupState
;
332 decryptedChange
= groupOperations
.decryptChange(changeActions
,
333 selfAddressProvider
.getSelfAddress().getUuid().get());
334 decryptedGroupState
= DecryptedGroupUtil
.apply(previousGroupState
, decryptedChange
);
335 } catch (VerificationFailedException
| InvalidGroupStateException
| NotAbleToApplyGroupV2ChangeException e
) {
336 throw new IOException(e
);
339 var signedGroupChange
= groupsV2Api
.patchGroup(changeActions
,
340 groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
),
343 return new Pair
<>(decryptedGroupState
, signedGroupChange
);
346 private GroupChange
commitChange(
347 GroupSecretParams groupSecretParams
,
349 GroupChange
.Actions
.Builder change
,
350 GroupLinkPassword password
351 ) throws IOException
{
352 final var nextRevision
= currentRevision
+ 1;
353 final var changeActions
= change
.setRevision(nextRevision
).build();
355 return groupsV2Api
.patchGroup(changeActions
,
356 groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
),
357 Optional
.fromNullable(password
).transform(GroupLinkPassword
::serialize
));
360 public DecryptedGroup
getUpdatedDecryptedGroup(
361 DecryptedGroup group
, byte[] signedGroupChange
, GroupMasterKey groupMasterKey
364 final var decryptedGroupChange
= getDecryptedGroupChange(signedGroupChange
, groupMasterKey
);
365 if (decryptedGroupChange
== null) {
368 return DecryptedGroupUtil
.apply(group
, decryptedGroupChange
);
369 } catch (NotAbleToApplyGroupV2ChangeException e
) {
374 private DecryptedGroupChange
getDecryptedGroupChange(byte[] signedGroupChange
, GroupMasterKey groupMasterKey
) {
375 if (signedGroupChange
!= null) {
376 var groupOperations
= groupsV2Operations
.forGroup(GroupSecretParams
.deriveFromMasterKey(groupMasterKey
));
379 return groupOperations
.decryptChange(GroupChange
.parseFrom(signedGroupChange
), true).orNull();
380 } catch (VerificationFailedException
| InvalidGroupStateException
| InvalidProtocolBufferException e
) {