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
.util
.UuidUtil
;
27 import org
.whispersystems
.signalservice
.internal
.util
.Hex
;
30 import java
.io
.FileInputStream
;
31 import java
.io
.FileOutputStream
;
32 import java
.io
.IOException
;
33 import java
.util
.ArrayList
;
34 import java
.util
.Base64
;
35 import java
.util
.HashMap
;
36 import java
.util
.List
;
38 import java
.util
.stream
.Collectors
;
40 public class GroupStore
{
42 private final static Logger logger
= LoggerFactory
.getLogger(GroupStore
.class);
44 private final File groupCachePath
;
45 private final Map
<GroupId
, GroupInfo
> groups
;
46 private final RecipientResolver recipientResolver
;
47 private final Saver saver
;
50 final File groupCachePath
,
51 final Map
<GroupId
, GroupInfo
> groups
,
52 final RecipientResolver recipientResolver
,
55 this.groupCachePath
= groupCachePath
;
57 this.recipientResolver
= recipientResolver
;
62 final File groupCachePath
, final RecipientResolver recipientResolver
, final Saver saver
64 this.groups
= new HashMap
<>();
65 this.groupCachePath
= groupCachePath
;
66 this.recipientResolver
= recipientResolver
;
70 public static GroupStore
fromStorage(
71 final Storage storage
,
72 final File groupCachePath
,
73 final RecipientResolver recipientResolver
,
76 final var groups
= storage
.groups
.stream().map(g
-> {
77 if (g
instanceof Storage
.GroupV1
) {
78 final var g1
= (Storage
.GroupV1
) g
;
79 final var members
= g1
.members
.stream().map(m
-> {
80 if (m
.recipientId
== null) {
81 return recipientResolver
.resolveRecipient(new RecipientAddress(UuidUtil
.parseOrNull(m
.uuid
),
85 return RecipientId
.of(m
.recipientId
);
86 }).collect(Collectors
.toSet());
88 return new GroupInfoV1(GroupIdV1
.fromBase64(g1
.groupId
),
89 g1
.expectedV2Id
== null ?
null : GroupIdV2
.fromBase64(g1
.expectedV2Id
),
93 g1
.messageExpirationTime
,
98 final var g2
= (Storage
.GroupV2
) g
;
99 var groupId
= GroupIdV2
.fromBase64(g2
.groupId
);
100 GroupMasterKey masterKey
;
102 masterKey
= new GroupMasterKey(Base64
.getDecoder().decode(g2
.masterKey
));
103 } catch (InvalidInputException
| IllegalArgumentException e
) {
104 throw new AssertionError("Invalid master key for group " + groupId
.toBase64());
107 return new GroupInfoV2(groupId
, masterKey
, g2
.blocked
);
108 }).collect(Collectors
.toMap(GroupInfo
::getGroupId
, g
-> g
));
110 return new GroupStore(groupCachePath
, groups
, recipientResolver
, saver
);
113 public void updateGroup(GroupInfo group
) {
114 final Storage storage
;
115 synchronized (groups
) {
116 groups
.put(group
.getGroupId(), group
);
117 if (group
instanceof GroupInfoV2
&& ((GroupInfoV2
) group
).getGroup() != null) {
119 IOUtils
.createPrivateDirectories(groupCachePath
);
120 try (var stream
= new FileOutputStream(getGroupV2File(group
.getGroupId()))) {
121 ((GroupInfoV2
) group
).getGroup().writeTo(stream
);
123 final var groupFileLegacy
= getGroupV2FileLegacy(group
.getGroupId());
124 if (groupFileLegacy
.exists()) {
125 groupFileLegacy
.delete();
127 } catch (IOException e
) {
128 logger
.warn("Failed to cache group, ignoring: {}", e
.getMessage());
131 storage
= toStorageLocked();
136 public void deleteGroupV1(GroupIdV1 groupIdV1
) {
137 deleteGroup(groupIdV1
);
140 public void deleteGroup(GroupId groupId
) {
141 final Storage storage
;
142 synchronized (groups
) {
143 groups
.remove(groupId
);
144 storage
= toStorageLocked();
149 public GroupInfo
getGroup(GroupId groupId
) {
150 synchronized (groups
) {
151 return getGroupLocked(groupId
);
155 public GroupInfoV1
getOrCreateGroupV1(GroupIdV1 groupId
) {
156 synchronized (groups
) {
157 var group
= getGroupLocked(groupId
);
158 if (group
instanceof GroupInfoV1
) {
159 return (GroupInfoV1
) group
;
163 return new GroupInfoV1(groupId
);
170 public List
<GroupInfo
> getGroups() {
171 synchronized (groups
) {
172 final var groups
= this.groups
.values();
173 for (var group
: groups
) {
174 loadDecryptedGroupLocked(group
);
176 return new ArrayList
<>(groups
);
180 public void mergeRecipients(final RecipientId recipientId
, final RecipientId toBeMergedRecipientId
) {
181 synchronized (groups
) {
182 var modified
= false;
183 for (var group
: this.groups
.values()) {
184 if (group
instanceof GroupInfoV1
) {
185 var groupV1
= (GroupInfoV1
) group
;
186 if (groupV1
.isMember(toBeMergedRecipientId
)) {
187 groupV1
.removeMember(toBeMergedRecipientId
);
188 groupV1
.addMembers(List
.of(recipientId
));
194 saver
.save(toStorageLocked());
199 private GroupInfo
getGroupLocked(final GroupId groupId
) {
200 var group
= groups
.get(groupId
);
202 if (groupId
instanceof GroupIdV1
) {
203 group
= getGroupByV1IdLocked((GroupIdV1
) groupId
);
204 } else if (groupId
instanceof GroupIdV2
) {
205 group
= getGroupV1ByV2IdLocked((GroupIdV2
) groupId
);
208 loadDecryptedGroupLocked(group
);
212 private GroupInfo
getGroupByV1IdLocked(final GroupIdV1 groupId
) {
213 return groups
.get(GroupUtils
.getGroupIdV2(groupId
));
216 private GroupInfoV1
getGroupV1ByV2IdLocked(GroupIdV2 groupIdV2
) {
217 for (var g
: groups
.values()) {
218 if (g
instanceof GroupInfoV1
) {
219 final var gv1
= (GroupInfoV1
) g
;
220 if (groupIdV2
.equals(gv1
.getExpectedV2Id())) {
228 private void loadDecryptedGroupLocked(final GroupInfo group
) {
229 if (group
instanceof GroupInfoV2
&& ((GroupInfoV2
) group
).getGroup() == null) {
230 var groupFile
= getGroupV2File(group
.getGroupId());
231 if (!groupFile
.exists()) {
232 groupFile
= getGroupV2FileLegacy(group
.getGroupId());
234 if (!groupFile
.exists()) {
237 try (var stream
= new FileInputStream(groupFile
)) {
238 ((GroupInfoV2
) group
).setGroup(DecryptedGroup
.parseFrom(stream
), recipientResolver
);
239 } catch (IOException ignored
) {
244 private File
getGroupV2FileLegacy(final GroupId groupId
) {
245 return new File(groupCachePath
, Hex
.toStringCondensed(groupId
.serialize()));
248 private File
getGroupV2File(final GroupId groupId
) {
249 return new File(groupCachePath
, groupId
.toBase64().replace("/", "_"));
252 private Storage
toStorageLocked() {
253 return new Storage(groups
.values().stream().map(g
-> {
254 if (g
instanceof GroupInfoV1
) {
255 final var g1
= (GroupInfoV1
) g
;
256 return new Storage
.GroupV1(g1
.getGroupId().toBase64(),
257 g1
.getExpectedV2Id().toBase64(),
260 g1
.messageExpirationTime
,
264 .map(m
-> new Storage
.GroupV1
.Member(m
.getId(), null, null))
265 .collect(Collectors
.toList()));
268 final var g2
= (GroupInfoV2
) g
;
269 return new Storage
.GroupV2(g2
.getGroupId().toBase64(),
270 Base64
.getEncoder().encodeToString(g2
.getMasterKey().serialize()),
272 }).collect(Collectors
.toList()));
275 public static class Storage
{
277 // @JsonSerialize(using = GroupsSerializer.class)
278 @JsonDeserialize(using
= GroupsDeserializer
.class)
279 public List
<Storage
.Group
> groups
;
281 // For deserialization
285 public Storage(final List
<Storage
.Group
> groups
) {
286 this.groups
= groups
;
289 private abstract static class Group
{
293 private static class GroupV1
extends Group
{
295 public String groupId
;
296 public String expectedV2Id
;
299 public int messageExpirationTime
;
300 public boolean blocked
;
301 public boolean archived
;
303 @JsonDeserialize(using
= MembersDeserializer
.class)
304 @JsonSerialize(using
= MembersSerializer
.class)
305 public List
<Member
> members
;
307 // For deserialization
312 final String groupId
,
313 final String expectedV2Id
,
316 final int messageExpirationTime
,
317 final boolean blocked
,
318 final boolean archived
,
319 final List
<Member
> members
321 this.groupId
= groupId
;
322 this.expectedV2Id
= expectedV2Id
;
325 this.messageExpirationTime
= messageExpirationTime
;
326 this.blocked
= blocked
;
327 this.archived
= archived
;
328 this.members
= members
;
331 private static final class Member
{
333 public Long recipientId
;
337 public String number
;
339 Member(Long recipientId
, final String uuid
, final String number
) {
340 this.recipientId
= recipientId
;
342 this.number
= number
;
346 private static final class JsonRecipientAddress
{
350 public String number
;
352 // For deserialization
353 public JsonRecipientAddress() {
356 JsonRecipientAddress(final String uuid
, final String number
) {
358 this.number
= number
;
362 private static class MembersSerializer
extends JsonSerializer
<List
<Member
>> {
365 public void serialize(
366 final List
<Member
> value
, final JsonGenerator jgen
, final SerializerProvider provider
367 ) throws IOException
{
368 jgen
.writeStartArray(value
.size());
369 for (var address
: value
) {
370 if (address
.recipientId
!= null) {
371 jgen
.writeNumber(address
.recipientId
);
372 } else if (address
.uuid
!= null) {
373 jgen
.writeObject(new JsonRecipientAddress(address
.uuid
, address
.number
));
375 jgen
.writeString(address
.number
);
378 jgen
.writeEndArray();
382 private static class MembersDeserializer
extends JsonDeserializer
<List
<Member
>> {
385 public List
<Member
> deserialize(
386 JsonParser jsonParser
, DeserializationContext deserializationContext
387 ) throws IOException
{
388 var addresses
= new ArrayList
<Member
>();
389 JsonNode node
= jsonParser
.getCodec().readTree(jsonParser
);
392 addresses
.add(new Member(null, null, n
.textValue()));
393 } else if (n
.isNumber()) {
394 addresses
.add(new Member(n
.numberValue().longValue(), null, null));
396 var address
= jsonParser
.getCodec().treeToValue(n
, JsonRecipientAddress
.class);
397 addresses
.add(new Member(null, address
.uuid
, address
.number
));
406 private static class GroupV2
extends Group
{
408 public String groupId
;
409 public String masterKey
;
410 public boolean blocked
;
412 // For deserialization
416 public GroupV2(final String groupId
, final String masterKey
, final boolean blocked
) {
417 this.groupId
= groupId
;
418 this.masterKey
= masterKey
;
419 this.blocked
= blocked
;
425 // private static class GroupsSerializer extends JsonSerializer<List<Storage.Group>> {
428 // public void serialize(
429 // final List<Storage.Group> groups, final JsonGenerator jgen, final SerializerProvider provider
430 // ) throws IOException {
431 // jgen.writeStartArray(groups.size());
432 // for (var group : groups) {
433 // if (group instanceof GroupInfoV1) {
434 // jgen.writeObject(group);
435 // } else if (group instanceof GroupInfoV2) {
436 // final var groupV2 = (GroupInfoV2) group;
437 // jgen.writeStartObject();
438 // jgen.writeStringField("groupId", groupV2.getGroupId().toBase64());
439 // jgen.writeStringField("masterKey",
440 // Base64.getEncoder().encodeToString(groupV2.getMasterKey().serialize()));
441 // jgen.writeBooleanField("blocked", groupV2.isBlocked());
442 // jgen.writeEndObject();
444 // throw new AssertionError("Unknown group version");
447 // jgen.writeEndArray();
451 private static class GroupsDeserializer
extends JsonDeserializer
<List
<Storage
.Group
>> {
454 public List
<Storage
.Group
> deserialize(
455 JsonParser jsonParser
, DeserializationContext deserializationContext
456 ) throws IOException
{
457 var groups
= new ArrayList
<Storage
.Group
>();
458 JsonNode node
= jsonParser
.getCodec().readTree(jsonParser
);
461 if (n
.hasNonNull("masterKey")) {
463 g
= jsonParser
.getCodec().treeToValue(n
, Storage
.GroupV2
.class);
465 g
= jsonParser
.getCodec().treeToValue(n
, Storage
.GroupV1
.class);
474 public interface Saver
{
476 void save(Storage storage
);