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
.zkgroup
.VerificationFailedException
;
12 import org
.signal
.zkgroup
.groups
.GroupMasterKey
;
13 import org
.signal
.zkgroup
.groups
.GroupSecretParams
;
14 import org
.signal
.zkgroup
.profiles
.ProfileKeyCredential
;
15 import org
.whispersystems
.libsignal
.util
.Pair
;
16 import org
.whispersystems
.libsignal
.util
.guava
.Optional
;
17 import org
.whispersystems
.signalservice
.api
.groupsv2
.DecryptedGroupUtil
;
18 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupCandidate
;
19 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2Api
;
20 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2AuthorizationString
;
21 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2Operations
;
22 import org
.whispersystems
.signalservice
.api
.groupsv2
.InvalidGroupStateException
;
23 import org
.whispersystems
.signalservice
.api
.groupsv2
.NotAbleToApplyGroupV2ChangeException
;
24 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
25 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
27 import java
.io
.FileInputStream
;
28 import java
.io
.IOException
;
29 import java
.io
.InputStream
;
30 import java
.util
.Collection
;
32 import java
.util
.UUID
;
33 import java
.util
.stream
.Collectors
;
35 public class GroupHelper
{
37 private final ProfileKeyCredentialProvider profileKeyCredentialProvider
;
39 private final ProfileProvider profileProvider
;
41 private final SelfAddressProvider selfAddressProvider
;
43 private final GroupsV2Operations groupsV2Operations
;
45 private final GroupsV2Api groupsV2Api
;
47 private final GroupAuthorizationProvider groupAuthorizationProvider
;
50 final ProfileKeyCredentialProvider profileKeyCredentialProvider
,
51 final ProfileProvider profileProvider
,
52 final SelfAddressProvider selfAddressProvider
,
53 final GroupsV2Operations groupsV2Operations
,
54 final GroupsV2Api groupsV2Api
,
55 final GroupAuthorizationProvider groupAuthorizationProvider
57 this.profileKeyCredentialProvider
= profileKeyCredentialProvider
;
58 this.profileProvider
= profileProvider
;
59 this.selfAddressProvider
= selfAddressProvider
;
60 this.groupsV2Operations
= groupsV2Operations
;
61 this.groupsV2Api
= groupsV2Api
;
62 this.groupAuthorizationProvider
= groupAuthorizationProvider
;
65 public GroupInfoV2
createGroupV2(
66 String name
, Collection
<SignalServiceAddress
> members
, String avatarFile
67 ) throws IOException
{
68 final byte[] avatarBytes
= readAvatarBytes(avatarFile
);
69 final GroupsV2Operations
.NewGroup newGroup
= buildNewGroupV2(name
, members
, avatarBytes
);
70 if (newGroup
== null) {
74 final GroupSecretParams groupSecretParams
= newGroup
.getGroupSecretParams();
76 final GroupsV2AuthorizationString groupAuthForToday
;
77 final DecryptedGroup decryptedGroup
;
79 groupAuthForToday
= groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
);
80 groupsV2Api
.putNewGroup(newGroup
, groupAuthForToday
);
81 decryptedGroup
= groupsV2Api
.getGroup(groupSecretParams
, groupAuthForToday
);
82 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
83 System
.err
.println("Failed to create V2 group: " + e
.getMessage());
86 if (decryptedGroup
== null) {
87 System
.err
.println("Failed to create V2 group!");
91 final byte[] groupId
= groupSecretParams
.getPublicParams().getGroupIdentifier().serialize();
92 final GroupMasterKey masterKey
= groupSecretParams
.getMasterKey();
93 GroupInfoV2 g
= new GroupInfoV2(groupId
, masterKey
);
94 g
.setGroup(decryptedGroup
);
99 private byte[] readAvatarBytes(final String avatarFile
) throws IOException
{
100 final byte[] avatarBytes
;
101 try (InputStream avatar
= avatarFile
== null ?
null : new FileInputStream(avatarFile
)) {
102 avatarBytes
= avatar
== null ?
null : IOUtils
.readFully(avatar
);
107 private GroupsV2Operations
.NewGroup
buildNewGroupV2(
108 String name
, Collection
<SignalServiceAddress
> members
, byte[] avatar
110 final ProfileKeyCredential profileKeyCredential
= profileKeyCredentialProvider
.getProfileKeyCredential(
111 selfAddressProvider
.getSelfAddress());
112 if (profileKeyCredential
== null) {
113 System
.err
.println("Cannot create a V2 group as self does not have a versioned profile");
117 final int noUuidCapability
= members
.stream()
118 .filter(address
-> !address
.getUuid().isPresent())
119 .collect(Collectors
.toUnmodifiableSet())
121 if (noUuidCapability
> 0) {
122 System
.err
.println("Cannot create a V2 group as " + noUuidCapability
+ " members don't have a UUID.");
126 final int noGv2Capability
= members
.stream()
127 .map(profileProvider
::getProfile
)
128 .filter(profile
-> !profile
.getCapabilities().gv2
)
129 .collect(Collectors
.toUnmodifiableSet())
131 if (noGv2Capability
> 0) {
132 System
.err
.println("Cannot create a V2 group as " + noGv2Capability
+ " members don't support Groups V2.");
136 GroupCandidate self
= new GroupCandidate(selfAddressProvider
.getSelfAddress().getUuid().orNull(),
137 Optional
.fromNullable(profileKeyCredential
));
138 Set
<GroupCandidate
> candidates
= members
.stream()
139 .map(member
-> new GroupCandidate(member
.getUuid().get(),
140 Optional
.fromNullable(profileKeyCredentialProvider
.getProfileKeyCredential(member
))))
141 .collect(Collectors
.toSet());
143 final GroupSecretParams groupSecretParams
= GroupSecretParams
.generate();
144 return groupsV2Operations
.createNewGroup(groupSecretParams
,
146 Optional
.fromNullable(avatar
),
153 public Pair
<DecryptedGroup
, GroupChange
> updateGroupV2(
154 GroupInfoV2 groupInfoV2
, String name
, String avatarFile
155 ) throws IOException
{
156 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
157 GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
159 GroupChange
.Actions
.Builder change
= name
!= null
160 ? groupOperations
.createModifyGroupTitle(name
)
161 : GroupChange
.Actions
.newBuilder();
163 if (avatarFile
!= null) {
164 final byte[] avatarBytes
= readAvatarBytes(avatarFile
);
165 String avatarCdnKey
= groupsV2Api
.uploadAvatar(avatarBytes
,
167 groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
));
168 change
.setModifyAvatar(GroupChange
.Actions
.ModifyAvatarAction
.newBuilder().setAvatar(avatarCdnKey
));
171 final Optional
<UUID
> uuid
= this.selfAddressProvider
.getSelfAddress().getUuid();
172 if (uuid
.isPresent()) {
173 change
.setSourceUuid(UuidUtil
.toByteString(uuid
.get()));
176 return commitChange(groupInfoV2
, change
);
179 public Pair
<DecryptedGroup
, GroupChange
> updateGroupV2(
180 GroupInfoV2 groupInfoV2
, Set
<SignalServiceAddress
> newMembers
181 ) throws IOException
{
182 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
183 GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
185 Set
<GroupCandidate
> candidates
= newMembers
.stream()
186 .map(member
-> new GroupCandidate(member
.getUuid().get(),
187 Optional
.fromNullable(profileKeyCredentialProvider
.getProfileKeyCredential(member
))))
188 .collect(Collectors
.toSet());
190 final GroupChange
.Actions
.Builder change
= groupOperations
.createModifyGroupMembershipChange(candidates
,
191 selfAddressProvider
.getSelfAddress().getUuid().get());
193 final Optional
<UUID
> uuid
= this.selfAddressProvider
.getSelfAddress().getUuid();
194 if (uuid
.isPresent()) {
195 change
.setSourceUuid(UuidUtil
.toByteString(uuid
.get()));
198 return commitChange(groupInfoV2
, change
);
201 private Pair
<DecryptedGroup
, GroupChange
> commitChange(
202 GroupInfoV2 groupInfoV2
, GroupChange
.Actions
.Builder change
203 ) throws IOException
{
204 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
205 final GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
206 final DecryptedGroup previousGroupState
= groupInfoV2
.getGroup();
207 final int nextRevision
= previousGroupState
.getRevision() + 1;
208 final GroupChange
.Actions changeActions
= change
.setRevision(nextRevision
).build();
209 final DecryptedGroupChange decryptedChange
;
210 final DecryptedGroup decryptedGroupState
;
213 decryptedChange
= groupOperations
.decryptChange(changeActions
,
214 selfAddressProvider
.getSelfAddress().getUuid().get());
215 decryptedGroupState
= DecryptedGroupUtil
.apply(previousGroupState
, decryptedChange
);
216 } catch (VerificationFailedException
| InvalidGroupStateException
| NotAbleToApplyGroupV2ChangeException e
) {
217 throw new IOException(e
);
220 GroupChange signedGroupChange
= groupsV2Api
.patchGroup(change
.build(),
221 groupAuthorizationProvider
.getAuthorizationForToday(groupSecretParams
),
224 return new Pair
<>(decryptedGroupState
, signedGroupChange
);
227 public DecryptedGroup
getUpdatedDecryptedGroup(
228 DecryptedGroup group
, byte[] signedGroupChange
, GroupMasterKey groupMasterKey
231 final DecryptedGroupChange decryptedGroupChange
= getDecryptedGroupChange(signedGroupChange
,
233 if (decryptedGroupChange
== null) {
236 return DecryptedGroupUtil
.apply(group
, decryptedGroupChange
);
237 } catch (NotAbleToApplyGroupV2ChangeException e
) {
242 private DecryptedGroupChange
getDecryptedGroupChange(byte[] signedGroupChange
, GroupMasterKey groupMasterKey
) {
243 if (signedGroupChange
!= null) {
244 GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(GroupSecretParams
.deriveFromMasterKey(
248 return groupOperations
.decryptChange(GroupChange
.parseFrom(signedGroupChange
), true).orNull();
249 } catch (VerificationFailedException
| InvalidGroupStateException
| InvalidProtocolBufferException e
) {