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 final int noUuidCapability
= members
.stream()
122 .filter(address
-> !address
.getUuid().isPresent())
123 .collect(Collectors
.toUnmodifiableSet())
125 if (noUuidCapability
> 0) {
126 System
.err
.println("Cannot create a V2 group as " + noUuidCapability
+ " members don't have a UUID.");
130 final int noGv2Capability
= members
.stream()
131 .map(profileProvider
::getProfile
)
132 .filter(profile
-> !profile
.getCapabilities().gv2
)
133 .collect(Collectors
.toUnmodifiableSet())
135 if (noGv2Capability
> 0) {
136 System
.err
.println("Cannot create a V2 group as " + noGv2Capability
+ " members don't support Groups V2.");
140 GroupCandidate self
= new GroupCandidate(selfAddressProvider
.getSelfAddress().getUuid().orNull(),
141 Optional
.fromNullable(profileKeyCredential
));
142 Set
<GroupCandidate
> candidates
= members
.stream()
143 .map(member
-> new GroupCandidate(member
.getUuid().get(),
144 Optional
.fromNullable(profileKeyCredentialProvider
.getProfileKeyCredential(member
))))
145 .collect(Collectors
.toSet());
147 final GroupSecretParams groupSecretParams
= GroupSecretParams
.generate();
148 return groupsV2Operations
.createNewGroup(groupSecretParams
,
150 Optional
.fromNullable(avatar
),
157 public Pair
<DecryptedGroup
, GroupChange
> updateGroupV2(
158 GroupInfoV2 groupInfoV2
, String name
, String avatarFile
159 ) throws IOException
{
160 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
161 GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
163 GroupChange
.Actions
.Builder change
= name
!= null
164 ? groupOperations
.createModifyGroupTitle(name
)
165 : GroupChange
.Actions
.newBuilder();
167 if (avatarFile
!= null) {
168 final byte[] avatarBytes
= readAvatarBytes(avatarFile
);
169 String avatarCdnKey
= groupsV2Api
.uploadAvatar(avatarBytes
,
171 groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
));
172 change
.setModifyAvatar(GroupChange
.Actions
.ModifyAvatarAction
.newBuilder().setAvatar(avatarCdnKey
));
175 final Optional
<UUID
> uuid
= this.selfAddressProvider
.getSelfAddress().getUuid();
176 if (uuid
.isPresent()) {
177 change
.setSourceUuid(UuidUtil
.toByteString(uuid
.get()));
180 return commitChange(groupInfoV2
, change
);
183 public Pair
<DecryptedGroup
, GroupChange
> updateGroupV2(
184 GroupInfoV2 groupInfoV2
, Set
<SignalServiceAddress
> newMembers
185 ) throws IOException
{
186 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
187 GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
189 Set
<GroupCandidate
> candidates
= newMembers
.stream()
190 .map(member
-> new GroupCandidate(member
.getUuid().get(),
191 Optional
.fromNullable(profileKeyCredentialProvider
.getProfileKeyCredential(member
))))
192 .collect(Collectors
.toSet());
194 final GroupChange
.Actions
.Builder change
= groupOperations
.createModifyGroupMembershipChange(candidates
,
195 selfAddressProvider
.getSelfAddress().getUuid().get());
197 final Optional
<UUID
> uuid
= this.selfAddressProvider
.getSelfAddress().getUuid();
198 if (uuid
.isPresent()) {
199 change
.setSourceUuid(UuidUtil
.toByteString(uuid
.get()));
202 return commitChange(groupInfoV2
, change
);
205 public Pair
<DecryptedGroup
, GroupChange
> leaveGroup(GroupInfoV2 groupInfoV2
) throws IOException
{
206 List
<DecryptedPendingMember
> pendingMembersList
= groupInfoV2
.getGroup().getPendingMembersList();
207 final UUID selfUuid
= selfAddressProvider
.getSelfAddress().getUuid().get();
208 Optional
<DecryptedPendingMember
> selfPendingMember
= DecryptedGroupUtil
.findPendingByUuid(pendingMembersList
,
211 if (selfPendingMember
.isPresent()) {
212 return revokeInvites(groupInfoV2
, Set
.of(selfPendingMember
.get()));
214 return ejectMembers(groupInfoV2
, Set
.of(selfUuid
));
218 public Pair
<DecryptedGroup
, GroupChange
> revokeInvites(
219 GroupInfoV2 groupInfoV2
, Set
<DecryptedPendingMember
> pendingMembers
220 ) throws IOException
{
221 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
222 final GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
223 final Set
<UuidCiphertext
> uuidCipherTexts
= pendingMembers
.stream().map(member
-> {
225 return new UuidCiphertext(member
.getUuidCipherText().toByteArray());
226 } catch (InvalidInputException e
) {
227 throw new AssertionError(e
);
229 }).collect(Collectors
.toSet());
230 return commitChange(groupInfoV2
, groupOperations
.createRemoveInvitationChange(uuidCipherTexts
));
233 public Pair
<DecryptedGroup
, GroupChange
> ejectMembers(GroupInfoV2 groupInfoV2
, Set
<UUID
> uuids
) throws IOException
{
234 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
235 final GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
236 return commitChange(groupInfoV2
, groupOperations
.createRemoveMembersChange(uuids
));
239 private Pair
<DecryptedGroup
, GroupChange
> commitChange(
240 GroupInfoV2 groupInfoV2
, GroupChange
.Actions
.Builder change
241 ) throws IOException
{
242 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
243 final GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
244 final DecryptedGroup previousGroupState
= groupInfoV2
.getGroup();
245 final int nextRevision
= previousGroupState
.getRevision() + 1;
246 final GroupChange
.Actions changeActions
= change
.setRevision(nextRevision
).build();
247 final DecryptedGroupChange decryptedChange
;
248 final DecryptedGroup decryptedGroupState
;
251 decryptedChange
= groupOperations
.decryptChange(changeActions
,
252 selfAddressProvider
.getSelfAddress().getUuid().get());
253 decryptedGroupState
= DecryptedGroupUtil
.apply(previousGroupState
, decryptedChange
);
254 } catch (VerificationFailedException
| InvalidGroupStateException
| NotAbleToApplyGroupV2ChangeException e
) {
255 throw new IOException(e
);
258 GroupChange signedGroupChange
= groupsV2Api
.patchGroup(change
.build(),
259 groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
),
262 return new Pair
<>(decryptedGroupState
, signedGroupChange
);
265 public DecryptedGroup
getUpdatedDecryptedGroup(
266 DecryptedGroup group
, byte[] signedGroupChange
, GroupMasterKey groupMasterKey
269 final DecryptedGroupChange decryptedGroupChange
= getDecryptedGroupChange(signedGroupChange
,
271 if (decryptedGroupChange
== null) {
274 return DecryptedGroupUtil
.apply(group
, decryptedGroupChange
);
275 } catch (NotAbleToApplyGroupV2ChangeException e
) {
280 private DecryptedGroupChange
getDecryptedGroupChange(byte[] signedGroupChange
, GroupMasterKey groupMasterKey
) {
281 if (signedGroupChange
!= null) {
282 GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(GroupSecretParams
.deriveFromMasterKey(
286 return groupOperations
.decryptChange(GroupChange
.parseFrom(signedGroupChange
), true).orNull();
287 } catch (VerificationFailedException
| InvalidGroupStateException
| InvalidProtocolBufferException e
) {