1 package org
.asamk
.signal
.manager
.storage
.groups
;
3 import com
.fasterxml
.jackson
.core
.JsonGenerator
;
4 import com
.fasterxml
.jackson
.core
.JsonParser
;
5 import com
.fasterxml
.jackson
.databind
.DeserializationContext
;
6 import com
.fasterxml
.jackson
.databind
.JsonDeserializer
;
7 import com
.fasterxml
.jackson
.databind
.JsonNode
;
8 import com
.fasterxml
.jackson
.databind
.JsonSerializer
;
9 import com
.fasterxml
.jackson
.databind
.SerializerProvider
;
10 import com
.fasterxml
.jackson
.databind
.annotation
.JsonDeserialize
;
11 import com
.fasterxml
.jackson
.databind
.annotation
.JsonSerialize
;
13 import org
.asamk
.signal
.manager
.groups
.GroupId
;
14 import org
.asamk
.signal
.manager
.groups
.GroupIdV1
;
15 import org
.asamk
.signal
.manager
.groups
.GroupIdV2
;
16 import org
.asamk
.signal
.manager
.groups
.GroupUtils
;
17 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientAddress
;
18 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientId
;
19 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientResolver
;
20 import org
.asamk
.signal
.manager
.util
.IOUtils
;
21 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroup
;
22 import org
.signal
.zkgroup
.InvalidInputException
;
23 import org
.signal
.zkgroup
.groups
.GroupMasterKey
;
24 import org
.slf4j
.Logger
;
25 import org
.slf4j
.LoggerFactory
;
26 import org
.whispersystems
.signalservice
.api
.push
.DistributionId
;
27 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
28 import org
.whispersystems
.signalservice
.internal
.util
.Hex
;
31 import java
.io
.FileInputStream
;
32 import java
.io
.FileOutputStream
;
33 import java
.io
.IOException
;
34 import java
.nio
.file
.Files
;
35 import java
.util
.ArrayList
;
36 import java
.util
.Base64
;
37 import java
.util
.HashMap
;
38 import java
.util
.List
;
40 import java
.util
.Objects
;
41 import java
.util
.stream
.Collectors
;
43 public class GroupStore
{
45 private final static Logger logger
= LoggerFactory
.getLogger(GroupStore
.class);
47 private final File groupCachePath
;
48 private final Map
<GroupId
, GroupInfo
> groups
;
49 private final RecipientResolver recipientResolver
;
50 private final Saver saver
;
53 final File groupCachePath
,
54 final Map
<GroupId
, GroupInfo
> groups
,
55 final RecipientResolver recipientResolver
,
58 this.groupCachePath
= groupCachePath
;
60 this.recipientResolver
= recipientResolver
;
65 final File groupCachePath
, final RecipientResolver recipientResolver
, final Saver saver
67 this.groups
= new HashMap
<>();
68 this.groupCachePath
= groupCachePath
;
69 this.recipientResolver
= recipientResolver
;
73 public static GroupStore
fromStorage(
74 final Storage storage
,
75 final File groupCachePath
,
76 final RecipientResolver recipientResolver
,
79 final var groups
= storage
.groups
.stream().map(g
-> {
80 if (g
instanceof Storage
.GroupV1 g1
) {
81 final var members
= g1
.members
.stream().map(m
-> {
82 if (m
.recipientId
== null) {
83 return recipientResolver
.resolveRecipient(new RecipientAddress(UuidUtil
.parseOrNull(m
.uuid
),
87 return recipientResolver
.resolveRecipient(m
.recipientId
);
88 }).filter(Objects
::nonNull
).collect(Collectors
.toSet());
90 return new GroupInfoV1(GroupIdV1
.fromBase64(g1
.groupId
),
91 g1
.expectedV2Id
== null ?
null : GroupIdV2
.fromBase64(g1
.expectedV2Id
),
95 g1
.messageExpirationTime
,
100 final var g2
= (Storage
.GroupV2
) g
;
101 var groupId
= GroupIdV2
.fromBase64(g2
.groupId
);
102 GroupMasterKey masterKey
;
104 masterKey
= new GroupMasterKey(Base64
.getDecoder().decode(g2
.masterKey
));
105 } catch (InvalidInputException
| IllegalArgumentException e
) {
106 throw new AssertionError("Invalid master key for group " + groupId
.toBase64());
109 return new GroupInfoV2(groupId
,
111 g2
.distributionId
== null ?
null : DistributionId
.from(g2
.distributionId
),
113 g2
.permissionDenied
);
114 }).collect(Collectors
.toMap(GroupInfo
::getGroupId
, g
-> g
));
116 return new GroupStore(groupCachePath
, groups
, recipientResolver
, saver
);
119 public void updateGroup(GroupInfo group
) {
120 final Storage storage
;
121 synchronized (groups
) {
122 groups
.put(group
.getGroupId(), group
);
123 if (group
instanceof GroupInfoV2
&& ((GroupInfoV2
) group
).getGroup() != null) {
125 IOUtils
.createPrivateDirectories(groupCachePath
);
126 try (var stream
= new FileOutputStream(getGroupV2File(group
.getGroupId()))) {
127 ((GroupInfoV2
) group
).getGroup().writeTo(stream
);
129 final var groupFileLegacy
= getGroupV2FileLegacy(group
.getGroupId());
130 if (groupFileLegacy
.exists()) {
132 Files
.delete(groupFileLegacy
.toPath());
133 } catch (IOException e
) {
134 logger
.error("Failed to delete legacy group file {}: {}", groupFileLegacy
, e
.getMessage());
137 } catch (IOException e
) {
138 logger
.warn("Failed to cache group, ignoring: {}", e
.getMessage());
141 storage
= toStorageLocked();
146 public void deleteGroupV1(GroupIdV1 groupIdV1
) {
147 deleteGroup(groupIdV1
);
150 public void deleteGroup(GroupId groupId
) {
151 final Storage storage
;
152 synchronized (groups
) {
153 groups
.remove(groupId
);
154 storage
= toStorageLocked();
159 public GroupInfo
getGroup(GroupId groupId
) {
160 synchronized (groups
) {
161 return getGroupLocked(groupId
);
165 public GroupInfoV1
getOrCreateGroupV1(GroupIdV1 groupId
) {
166 synchronized (groups
) {
167 var group
= getGroupLocked(groupId
);
168 if (group
instanceof GroupInfoV1
) {
169 return (GroupInfoV1
) group
;
173 return new GroupInfoV1(groupId
);
180 public List
<GroupInfo
> getGroups() {
181 synchronized (groups
) {
182 final var groups
= this.groups
.values();
183 for (var group
: groups
) {
184 loadDecryptedGroupLocked(group
);
186 return new ArrayList
<>(groups
);
190 public void mergeRecipients(final RecipientId recipientId
, final RecipientId toBeMergedRecipientId
) {
191 synchronized (groups
) {
192 var modified
= false;
193 for (var group
: this.groups
.values()) {
194 if (group
instanceof GroupInfoV1 groupV1
) {
195 if (groupV1
.isMember(toBeMergedRecipientId
)) {
196 groupV1
.removeMember(toBeMergedRecipientId
);
197 groupV1
.addMembers(List
.of(recipientId
));
203 saver
.save(toStorageLocked());
208 private GroupInfo
getGroupLocked(final GroupId groupId
) {
209 var group
= groups
.get(groupId
);
211 if (groupId
instanceof GroupIdV1
) {
212 group
= getGroupByV1IdLocked((GroupIdV1
) groupId
);
213 } else if (groupId
instanceof GroupIdV2
) {
214 group
= getGroupV1ByV2IdLocked((GroupIdV2
) groupId
);
217 loadDecryptedGroupLocked(group
);
221 private GroupInfo
getGroupByV1IdLocked(final GroupIdV1 groupId
) {
222 return groups
.get(GroupUtils
.getGroupIdV2(groupId
));
225 private GroupInfoV1
getGroupV1ByV2IdLocked(GroupIdV2 groupIdV2
) {
226 for (var g
: groups
.values()) {
227 if (g
instanceof GroupInfoV1 gv1
) {
228 if (groupIdV2
.equals(gv1
.getExpectedV2Id())) {
236 private void loadDecryptedGroupLocked(final GroupInfo group
) {
237 if (group
instanceof GroupInfoV2
&& ((GroupInfoV2
) group
).getGroup() == null) {
238 var groupFile
= getGroupV2File(group
.getGroupId());
239 if (!groupFile
.exists()) {
240 groupFile
= getGroupV2FileLegacy(group
.getGroupId());
242 if (!groupFile
.exists()) {
245 try (var stream
= new FileInputStream(groupFile
)) {
246 ((GroupInfoV2
) group
).setGroup(DecryptedGroup
.parseFrom(stream
), recipientResolver
);
247 } catch (IOException ignored
) {
252 private File
getGroupV2FileLegacy(final GroupId groupId
) {
253 return new File(groupCachePath
, Hex
.toStringCondensed(groupId
.serialize()));
256 private File
getGroupV2File(final GroupId groupId
) {
257 return new File(groupCachePath
, groupId
.toBase64().replace("/", "_"));
260 private Storage
toStorageLocked() {
261 return new Storage(groups
.values().stream().map(g
-> {
262 if (g
instanceof GroupInfoV1 g1
) {
263 return new Storage
.GroupV1(g1
.getGroupId().toBase64(),
264 g1
.getExpectedV2Id().toBase64(),
267 g1
.messageExpirationTime
,
270 g1
.members
.stream().map(m
-> new Storage
.GroupV1
.Member(m
.id(), null, null)).toList());
273 final var g2
= (GroupInfoV2
) g
;
274 return new Storage
.GroupV2(g2
.getGroupId().toBase64(),
275 Base64
.getEncoder().encodeToString(g2
.getMasterKey().serialize()),
276 g2
.getDistributionId() == null ?
null : g2
.getDistributionId().toString(),
278 g2
.isPermissionDenied());
282 public record Storage(@JsonDeserialize(using
= GroupsDeserializer
.class) List
<Record
> groups
) {
284 private record GroupV1(
289 int messageExpirationTime
,
292 @JsonDeserialize(using
= MembersDeserializer
.class) @JsonSerialize(using
= MembersSerializer
.class) List
<Member
> members
295 private record Member(Long recipientId
, String uuid
, String number
) {}
297 private record JsonRecipientAddress(String uuid
, String number
) {}
299 private static class MembersSerializer
extends JsonSerializer
<List
<Member
>> {
302 public void serialize(
303 final List
<Member
> value
, final JsonGenerator jgen
, final SerializerProvider provider
304 ) throws IOException
{
305 jgen
.writeStartArray(null, value
.size());
306 for (var address
: value
) {
307 if (address
.recipientId
!= null) {
308 jgen
.writeNumber(address
.recipientId
);
309 } else if (address
.uuid
!= null) {
310 jgen
.writeObject(new JsonRecipientAddress(address
.uuid
, address
.number
));
312 jgen
.writeString(address
.number
);
315 jgen
.writeEndArray();
319 private static class MembersDeserializer
extends JsonDeserializer
<List
<Member
>> {
322 public List
<Member
> deserialize(
323 JsonParser jsonParser
, DeserializationContext deserializationContext
324 ) throws IOException
{
325 var addresses
= new ArrayList
<Member
>();
326 JsonNode node
= jsonParser
.getCodec().readTree(jsonParser
);
329 addresses
.add(new Member(null, null, n
.textValue()));
330 } else if (n
.isNumber()) {
331 addresses
.add(new Member(n
.numberValue().longValue(), null, null));
333 var address
= jsonParser
.getCodec().treeToValue(n
, JsonRecipientAddress
.class);
334 addresses
.add(new Member(null, address
.uuid
, address
.number
));
343 private record GroupV2(
344 String groupId
, String masterKey
, String distributionId
, boolean blocked
, boolean permissionDenied
348 private static class GroupsDeserializer
extends JsonDeserializer
<List
<Object
>> {
351 public List
<Object
> deserialize(
352 JsonParser jsonParser
, DeserializationContext deserializationContext
353 ) throws IOException
{
354 var groups
= new ArrayList
<>();
355 JsonNode node
= jsonParser
.getCodec().readTree(jsonParser
);
358 if (n
.hasNonNull("masterKey")) {
360 g
= jsonParser
.getCodec().treeToValue(n
, Storage
.GroupV2
.class);
362 g
= jsonParser
.getCodec().treeToValue(n
, Storage
.GroupV1
.class);
371 public interface Saver
{
373 void save(Storage storage
);