1 package org
.asamk
.signal
.manager
.helper
;
3 import com
.google
.protobuf
.InvalidProtocolBufferException
;
5 import org
.asamk
.signal
.storage
.groups
.GroupInfoV2
;
6 import org
.asamk
.signal
.util
.IOUtils
;
7 import org
.signal
.storageservice
.protos
.groups
.GroupChange
;
8 import org
.signal
.storageservice
.protos
.groups
.Member
;
9 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroup
;
10 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroupChange
;
11 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedPendingMember
;
12 import org
.signal
.zkgroup
.InvalidInputException
;
13 import org
.signal
.zkgroup
.VerificationFailedException
;
14 import org
.signal
.zkgroup
.groups
.GroupMasterKey
;
15 import org
.signal
.zkgroup
.groups
.GroupSecretParams
;
16 import org
.signal
.zkgroup
.groups
.UuidCiphertext
;
17 import org
.signal
.zkgroup
.profiles
.ProfileKeyCredential
;
18 import org
.whispersystems
.libsignal
.util
.Pair
;
19 import org
.whispersystems
.libsignal
.util
.guava
.Optional
;
20 import org
.whispersystems
.signalservice
.api
.groupsv2
.DecryptedGroupUtil
;
21 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupCandidate
;
22 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2Api
;
23 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2AuthorizationString
;
24 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2Operations
;
25 import org
.whispersystems
.signalservice
.api
.groupsv2
.InvalidGroupStateException
;
26 import org
.whispersystems
.signalservice
.api
.groupsv2
.NotAbleToApplyGroupV2ChangeException
;
27 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
28 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
30 import java
.io
.FileInputStream
;
31 import java
.io
.IOException
;
32 import java
.io
.InputStream
;
33 import java
.util
.Collection
;
34 import java
.util
.List
;
36 import java
.util
.UUID
;
37 import java
.util
.stream
.Collectors
;
39 public class GroupHelper
{
41 private final ProfileKeyCredentialProvider profileKeyCredentialProvider
;
43 private final ProfileProvider profileProvider
;
45 private final SelfAddressProvider selfAddressProvider
;
47 private final GroupsV2Operations groupsV2Operations
;
49 private final GroupsV2Api groupsV2Api
;
51 private final GroupAuthorizationProvider groupAuthorizationProvider
;
54 final ProfileKeyCredentialProvider profileKeyCredentialProvider
,
55 final ProfileProvider profileProvider
,
56 final SelfAddressProvider selfAddressProvider
,
57 final GroupsV2Operations groupsV2Operations
,
58 final GroupsV2Api groupsV2Api
,
59 final GroupAuthorizationProvider groupAuthorizationProvider
61 this.profileKeyCredentialProvider
= profileKeyCredentialProvider
;
62 this.profileProvider
= profileProvider
;
63 this.selfAddressProvider
= selfAddressProvider
;
64 this.groupsV2Operations
= groupsV2Operations
;
65 this.groupsV2Api
= groupsV2Api
;
66 this.groupAuthorizationProvider
= groupAuthorizationProvider
;
69 public GroupInfoV2
createGroupV2(
70 String name
, Collection
<SignalServiceAddress
> members
, String avatarFile
71 ) throws IOException
{
72 final byte[] avatarBytes
= readAvatarBytes(avatarFile
);
73 final GroupsV2Operations
.NewGroup newGroup
= buildNewGroupV2(name
, members
, avatarBytes
);
74 if (newGroup
== null) {
78 final GroupSecretParams groupSecretParams
= newGroup
.getGroupSecretParams();
80 final GroupsV2AuthorizationString groupAuthForToday
;
81 final DecryptedGroup decryptedGroup
;
83 groupAuthForToday
= groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
);
84 groupsV2Api
.putNewGroup(newGroup
, groupAuthForToday
);
85 decryptedGroup
= groupsV2Api
.getGroup(groupSecretParams
, groupAuthForToday
);
86 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
87 System
.err
.println("Failed to create V2 group: " + e
.getMessage());
90 if (decryptedGroup
== null) {
91 System
.err
.println("Failed to create V2 group!");
95 final byte[] groupId
= groupSecretParams
.getPublicParams().getGroupIdentifier().serialize();
96 final GroupMasterKey masterKey
= groupSecretParams
.getMasterKey();
97 GroupInfoV2 g
= new GroupInfoV2(groupId
, masterKey
);
98 g
.setGroup(decryptedGroup
);
103 private byte[] readAvatarBytes(final String avatarFile
) throws IOException
{
104 final byte[] avatarBytes
;
105 try (InputStream avatar
= avatarFile
== null ?
null : new FileInputStream(avatarFile
)) {
106 avatarBytes
= avatar
== null ?
null : IOUtils
.readFully(avatar
);
111 private GroupsV2Operations
.NewGroup
buildNewGroupV2(
112 String name
, Collection
<SignalServiceAddress
> members
, byte[] avatar
114 final ProfileKeyCredential profileKeyCredential
= profileKeyCredentialProvider
.getProfileKeyCredential(
115 selfAddressProvider
.getSelfAddress());
116 if (profileKeyCredential
== null) {
117 System
.err
.println("Cannot create a V2 group as self does not have a versioned profile");
121 if (!areMembersValid(members
)) return null;
123 GroupCandidate self
= new GroupCandidate(selfAddressProvider
.getSelfAddress().getUuid().orNull(),
124 Optional
.fromNullable(profileKeyCredential
));
125 Set
<GroupCandidate
> candidates
= members
.stream()
126 .map(member
-> new GroupCandidate(member
.getUuid().get(),
127 Optional
.fromNullable(profileKeyCredentialProvider
.getProfileKeyCredential(member
))))
128 .collect(Collectors
.toSet());
130 final GroupSecretParams groupSecretParams
= GroupSecretParams
.generate();
131 return groupsV2Operations
.createNewGroup(groupSecretParams
,
133 Optional
.fromNullable(avatar
),
140 private boolean areMembersValid(final Collection
<SignalServiceAddress
> members
) {
141 final int noUuidCapability
= members
.stream()
142 .filter(address
-> !address
.getUuid().isPresent())
143 .collect(Collectors
.toUnmodifiableSet())
145 if (noUuidCapability
> 0) {
146 System
.err
.println("Cannot create a V2 group as " + noUuidCapability
+ " members don't have a UUID.");
150 final int noGv2Capability
= members
.stream()
151 .map(profileProvider
::getProfile
)
152 .filter(profile
-> profile
!= null && !profile
.getCapabilities().gv2
)
153 .collect(Collectors
.toUnmodifiableSet())
155 if (noGv2Capability
> 0) {
156 System
.err
.println("Cannot create a V2 group as " + noGv2Capability
+ " members don't support Groups V2.");
163 public Pair
<DecryptedGroup
, GroupChange
> updateGroupV2(
164 GroupInfoV2 groupInfoV2
, String name
, String avatarFile
165 ) throws IOException
{
166 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
167 GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
169 GroupChange
.Actions
.Builder change
= name
!= null
170 ? groupOperations
.createModifyGroupTitle(name
)
171 : GroupChange
.Actions
.newBuilder();
173 if (avatarFile
!= null) {
174 final byte[] avatarBytes
= readAvatarBytes(avatarFile
);
175 String avatarCdnKey
= groupsV2Api
.uploadAvatar(avatarBytes
,
177 groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
));
178 change
.setModifyAvatar(GroupChange
.Actions
.ModifyAvatarAction
.newBuilder().setAvatar(avatarCdnKey
));
181 final Optional
<UUID
> uuid
= this.selfAddressProvider
.getSelfAddress().getUuid();
182 if (uuid
.isPresent()) {
183 change
.setSourceUuid(UuidUtil
.toByteString(uuid
.get()));
186 return commitChange(groupInfoV2
, change
);
189 public Pair
<DecryptedGroup
, GroupChange
> updateGroupV2(
190 GroupInfoV2 groupInfoV2
, Set
<SignalServiceAddress
> newMembers
191 ) throws IOException
{
192 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
193 GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
195 if (!areMembersValid(newMembers
)) return null;
197 Set
<GroupCandidate
> candidates
= newMembers
.stream()
198 .map(member
-> new GroupCandidate(member
.getUuid().get(),
199 Optional
.fromNullable(profileKeyCredentialProvider
.getProfileKeyCredential(member
))))
200 .collect(Collectors
.toSet());
202 final GroupChange
.Actions
.Builder change
= groupOperations
.createModifyGroupMembershipChange(candidates
,
203 selfAddressProvider
.getSelfAddress().getUuid().get());
205 final Optional
<UUID
> uuid
= this.selfAddressProvider
.getSelfAddress().getUuid();
206 if (uuid
.isPresent()) {
207 change
.setSourceUuid(UuidUtil
.toByteString(uuid
.get()));
210 return commitChange(groupInfoV2
, change
);
213 public Pair
<DecryptedGroup
, GroupChange
> leaveGroup(GroupInfoV2 groupInfoV2
) throws IOException
{
214 List
<DecryptedPendingMember
> pendingMembersList
= groupInfoV2
.getGroup().getPendingMembersList();
215 final UUID selfUuid
= selfAddressProvider
.getSelfAddress().getUuid().get();
216 Optional
<DecryptedPendingMember
> selfPendingMember
= DecryptedGroupUtil
.findPendingByUuid(pendingMembersList
,
219 if (selfPendingMember
.isPresent()) {
220 return revokeInvites(groupInfoV2
, Set
.of(selfPendingMember
.get()));
222 return ejectMembers(groupInfoV2
, Set
.of(selfUuid
));
226 public Pair
<DecryptedGroup
, GroupChange
> acceptInvite(GroupInfoV2 groupInfoV2
) throws IOException
{
227 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
228 final GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
230 final SignalServiceAddress selfAddress
= this.selfAddressProvider
.getSelfAddress();
231 final ProfileKeyCredential profileKeyCredential
= profileKeyCredentialProvider
.getProfileKeyCredential(
233 if (profileKeyCredential
== null) {
234 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
237 final GroupChange
.Actions
.Builder change
= groupOperations
.createAcceptInviteChange(profileKeyCredential
);
239 final Optional
<UUID
> uuid
= selfAddress
.getUuid();
240 if (uuid
.isPresent()) {
241 change
.setSourceUuid(UuidUtil
.toByteString(uuid
.get()));
244 return commitChange(groupInfoV2
, change
);
247 public Pair
<DecryptedGroup
, GroupChange
> revokeInvites(
248 GroupInfoV2 groupInfoV2
, Set
<DecryptedPendingMember
> pendingMembers
249 ) throws IOException
{
250 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
251 final GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
252 final Set
<UuidCiphertext
> uuidCipherTexts
= pendingMembers
.stream().map(member
-> {
254 return new UuidCiphertext(member
.getUuidCipherText().toByteArray());
255 } catch (InvalidInputException e
) {
256 throw new AssertionError(e
);
258 }).collect(Collectors
.toSet());
259 return commitChange(groupInfoV2
, groupOperations
.createRemoveInvitationChange(uuidCipherTexts
));
262 public Pair
<DecryptedGroup
, GroupChange
> ejectMembers(GroupInfoV2 groupInfoV2
, Set
<UUID
> uuids
) throws IOException
{
263 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
264 final GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
265 return commitChange(groupInfoV2
, groupOperations
.createRemoveMembersChange(uuids
));
268 private Pair
<DecryptedGroup
, GroupChange
> commitChange(
269 GroupInfoV2 groupInfoV2
, GroupChange
.Actions
.Builder change
270 ) throws IOException
{
271 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
272 final GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
273 final DecryptedGroup previousGroupState
= groupInfoV2
.getGroup();
274 final int nextRevision
= previousGroupState
.getRevision() + 1;
275 final GroupChange
.Actions changeActions
= change
.setRevision(nextRevision
).build();
276 final DecryptedGroupChange decryptedChange
;
277 final DecryptedGroup decryptedGroupState
;
280 decryptedChange
= groupOperations
.decryptChange(changeActions
,
281 selfAddressProvider
.getSelfAddress().getUuid().get());
282 decryptedGroupState
= DecryptedGroupUtil
.apply(previousGroupState
, decryptedChange
);
283 } catch (VerificationFailedException
| InvalidGroupStateException
| NotAbleToApplyGroupV2ChangeException e
) {
284 throw new IOException(e
);
287 GroupChange signedGroupChange
= groupsV2Api
.patchGroup(change
.build(),
288 groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
),
291 return new Pair
<>(decryptedGroupState
, signedGroupChange
);
294 public DecryptedGroup
getUpdatedDecryptedGroup(
295 DecryptedGroup group
, byte[] signedGroupChange
, GroupMasterKey groupMasterKey
298 final DecryptedGroupChange decryptedGroupChange
= getDecryptedGroupChange(signedGroupChange
,
300 if (decryptedGroupChange
== null) {
303 return DecryptedGroupUtil
.apply(group
, decryptedGroupChange
);
304 } catch (NotAbleToApplyGroupV2ChangeException e
) {
309 private DecryptedGroupChange
getDecryptedGroupChange(byte[] signedGroupChange
, GroupMasterKey groupMasterKey
) {
310 if (signedGroupChange
!= null) {
311 GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(GroupSecretParams
.deriveFromMasterKey(
315 return groupOperations
.decryptChange(GroupChange
.parseFrom(signedGroupChange
), true).orNull();
316 } catch (VerificationFailedException
| InvalidGroupStateException
| InvalidProtocolBufferException e
) {