1 package org
.asamk
.signal
.storage
.groups
;
3 import com
.fasterxml
.jackson
.annotation
.JsonProperty
;
4 import com
.fasterxml
.jackson
.core
.JsonGenerator
;
5 import com
.fasterxml
.jackson
.core
.JsonParser
;
6 import com
.fasterxml
.jackson
.databind
.DeserializationContext
;
7 import com
.fasterxml
.jackson
.databind
.JsonDeserializer
;
8 import com
.fasterxml
.jackson
.databind
.JsonNode
;
9 import com
.fasterxml
.jackson
.databind
.JsonSerializer
;
10 import com
.fasterxml
.jackson
.databind
.ObjectMapper
;
11 import com
.fasterxml
.jackson
.databind
.SerializerProvider
;
12 import com
.fasterxml
.jackson
.databind
.annotation
.JsonDeserialize
;
13 import com
.fasterxml
.jackson
.databind
.annotation
.JsonSerialize
;
15 import org
.asamk
.signal
.util
.Hex
;
16 import org
.asamk
.signal
.util
.IOUtils
;
17 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroup
;
18 import org
.signal
.zkgroup
.InvalidInputException
;
19 import org
.signal
.zkgroup
.groups
.GroupMasterKey
;
20 import org
.whispersystems
.util
.Base64
;
23 import java
.io
.FileInputStream
;
24 import java
.io
.FileOutputStream
;
25 import java
.io
.IOException
;
26 import java
.util
.ArrayList
;
27 import java
.util
.Collection
;
28 import java
.util
.HashMap
;
29 import java
.util
.List
;
32 public class JsonGroupStore
{
34 private static final ObjectMapper jsonProcessor
= new ObjectMapper();
35 public File groupCachePath
;
37 @JsonProperty("groups")
38 @JsonSerialize(using
= GroupsSerializer
.class)
39 @JsonDeserialize(using
= GroupsDeserializer
.class)
40 private final Map
<String
, GroupInfo
> groups
= new HashMap
<>();
42 private JsonGroupStore() {
45 public JsonGroupStore(final File groupCachePath
) {
46 this.groupCachePath
= groupCachePath
;
49 public void updateGroup(GroupInfo group
) {
50 groups
.put(Base64
.encodeBytes(group
.groupId
), group
);
51 if (group
instanceof GroupInfoV2
) {
53 IOUtils
.createPrivateDirectories(groupCachePath
);
54 try (FileOutputStream stream
= new FileOutputStream(getGroupFile(group
.groupId
))) {
55 ((GroupInfoV2
) group
).getGroup().writeTo(stream
);
57 } catch (IOException e
) {
58 System
.err
.println("Failed to cache group, ignoring ...");
63 public GroupInfo
getGroup(byte[] groupId
) {
64 final GroupInfo group
= groups
.get(Base64
.encodeBytes(groupId
));
65 loadDecryptedGroup(group
);
69 private void loadDecryptedGroup(final GroupInfo group
) {
70 if (group
instanceof GroupInfoV2
&& ((GroupInfoV2
) group
).getGroup() == null) {
71 try (FileInputStream stream
= new FileInputStream(getGroupFile(group
.groupId
))) {
72 ((GroupInfoV2
) group
).setGroup(DecryptedGroup
.parseFrom(stream
));
73 } catch (IOException ignored
) {
78 private File
getGroupFile(final byte[] groupId
) {
79 return new File(groupCachePath
, Hex
.toStringCondensed(groupId
));
82 public GroupInfoV1
getOrCreateGroupV1(byte[] groupId
) {
83 GroupInfo group
= groups
.get(Base64
.encodeBytes(groupId
));
84 if (group
instanceof GroupInfoV1
) {
85 return (GroupInfoV1
) group
;
89 return new GroupInfoV1(groupId
);
95 public List
<GroupInfo
> getGroups() {
96 final Collection
<GroupInfo
> groups
= this.groups
.values();
97 for (GroupInfo group
: groups
) {
98 loadDecryptedGroup(group
);
100 return new ArrayList
<>(groups
);
103 private static class GroupsSerializer
extends JsonSerializer
<Map
<String
, GroupInfo
>> {
106 public void serialize(final Map
<String
, GroupInfo
> value
, final JsonGenerator jgen
, final SerializerProvider provider
) throws IOException
{
107 final Collection
<GroupInfo
> groups
= value
.values();
108 jgen
.writeStartArray(groups
.size());
109 for (GroupInfo group
: groups
) {
110 if (group
instanceof GroupInfoV1
) {
111 jgen
.writeObject(group
);
112 } else if (group
instanceof GroupInfoV2
) {
113 final GroupInfoV2 groupV2
= (GroupInfoV2
) group
;
114 jgen
.writeStartObject();
115 jgen
.writeStringField("groupId", Base64
.encodeBytes(groupV2
.groupId
));
116 jgen
.writeStringField("masterKey", Base64
.encodeBytes(groupV2
.getMasterKey().serialize()));
117 jgen
.writeBooleanField("blocked", groupV2
.isBlocked());
118 jgen
.writeEndObject();
120 throw new AssertionError("Unknown group version");
123 jgen
.writeEndArray();
127 private static class GroupsDeserializer
extends JsonDeserializer
<Map
<String
, GroupInfo
>> {
130 public Map
<String
, GroupInfo
> deserialize(JsonParser jsonParser
, DeserializationContext deserializationContext
) throws IOException
{
131 Map
<String
, GroupInfo
> groups
= new HashMap
<>();
132 JsonNode node
= jsonParser
.getCodec().readTree(jsonParser
);
133 for (JsonNode n
: node
) {
135 if (n
.has("masterKey")) {
137 byte[] groupId
= Base64
.decode(n
.get("groupId").asText());
139 GroupMasterKey masterKey
= new GroupMasterKey(Base64
.decode(n
.get("masterKey").asText()));
140 g
= new GroupInfoV2(groupId
, masterKey
);
141 } catch (InvalidInputException e
) {
142 throw new AssertionError("Invalid master key for group " + Base64
.encodeBytes(groupId
));
144 g
.setBlocked(n
.get("blocked").asBoolean(false));
146 g
= jsonProcessor
.treeToValue(n
, GroupInfoV1
.class);
148 groups
.put(Base64
.encodeBytes(g
.groupId
), g
);