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
.manager
.GroupUtils
;
16 import org
.asamk
.signal
.util
.Hex
;
17 import org
.asamk
.signal
.util
.IOUtils
;
18 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroup
;
19 import org
.signal
.zkgroup
.InvalidInputException
;
20 import org
.signal
.zkgroup
.groups
.GroupMasterKey
;
21 import org
.whispersystems
.util
.Base64
;
24 import java
.io
.FileInputStream
;
25 import java
.io
.FileOutputStream
;
26 import java
.io
.IOException
;
27 import java
.util
.ArrayList
;
28 import java
.util
.Arrays
;
29 import java
.util
.Collection
;
30 import java
.util
.HashMap
;
31 import java
.util
.List
;
34 public class JsonGroupStore
{
36 private static final ObjectMapper jsonProcessor
= new ObjectMapper();
37 public File groupCachePath
;
39 @JsonProperty("groups")
40 @JsonSerialize(using
= GroupsSerializer
.class)
41 @JsonDeserialize(using
= GroupsDeserializer
.class)
42 private final Map
<String
, GroupInfo
> groups
= new HashMap
<>();
44 private JsonGroupStore() {
47 public JsonGroupStore(final File groupCachePath
) {
48 this.groupCachePath
= groupCachePath
;
51 public void updateGroup(GroupInfo group
) {
52 groups
.put(Base64
.encodeBytes(group
.groupId
), group
);
53 if (group
instanceof GroupInfoV2
&& ((GroupInfoV2
) group
).getGroup() != null) {
55 IOUtils
.createPrivateDirectories(groupCachePath
);
56 try (FileOutputStream stream
= new FileOutputStream(getGroupFile(group
.groupId
))) {
57 ((GroupInfoV2
) group
).getGroup().writeTo(stream
);
59 } catch (IOException e
) {
60 System
.err
.println("Failed to cache group, ignoring ...");
65 public void deleteGroup(byte[] groupId
) {
66 groups
.remove(Base64
.encodeBytes(groupId
));
69 public GroupInfo
getGroup(byte[] groupId
) {
70 final GroupInfo group
= groups
.get(Base64
.encodeBytes(groupId
));
71 if (group
== null & groupId
.length
== 16) {
72 return getGroupByV1Id(groupId
);
74 loadDecryptedGroup(group
);
78 public GroupInfo
getGroupByV1Id(byte[] groupIdV1
) {
79 GroupInfo group
= groups
.get(Base64
.encodeBytes(groupIdV1
));
81 group
= groups
.get(Base64
.encodeBytes(GroupUtils
.getGroupId(GroupUtils
.deriveV2MigrationMasterKey(groupIdV1
))));
83 loadDecryptedGroup(group
);
87 public GroupInfo
getGroupByV2Id(byte[] groupIdV2
) {
88 GroupInfo group
= groups
.get(Base64
.encodeBytes(groupIdV2
));
90 for (GroupInfo g
: groups
.values()) {
91 if (g
instanceof GroupInfoV1
&& Arrays
.equals(groupIdV2
, ((GroupInfoV1
) g
).expectedV2Id
)) {
97 loadDecryptedGroup(group
);
101 private void loadDecryptedGroup(final GroupInfo group
) {
102 if (group
instanceof GroupInfoV2
&& ((GroupInfoV2
) group
).getGroup() == null) {
103 try (FileInputStream stream
= new FileInputStream(getGroupFile(group
.groupId
))) {
104 ((GroupInfoV2
) group
).setGroup(DecryptedGroup
.parseFrom(stream
));
105 } catch (IOException ignored
) {
110 private File
getGroupFile(final byte[] groupId
) {
111 return new File(groupCachePath
, Hex
.toStringCondensed(groupId
));
114 public GroupInfoV1
getOrCreateGroupV1(byte[] groupId
) {
115 GroupInfo group
= groups
.get(Base64
.encodeBytes(groupId
));
116 if (group
instanceof GroupInfoV1
) {
117 return (GroupInfoV1
) group
;
121 return new GroupInfoV1(groupId
);
127 public List
<GroupInfo
> getGroups() {
128 final Collection
<GroupInfo
> groups
= this.groups
.values();
129 for (GroupInfo group
: groups
) {
130 loadDecryptedGroup(group
);
132 return new ArrayList
<>(groups
);
135 private static class GroupsSerializer
extends JsonSerializer
<Map
<String
, GroupInfo
>> {
138 public void serialize(
139 final Map
<String
, GroupInfo
> value
, final JsonGenerator jgen
, final SerializerProvider provider
140 ) throws IOException
{
141 final Collection
<GroupInfo
> groups
= value
.values();
142 jgen
.writeStartArray(groups
.size());
143 for (GroupInfo group
: groups
) {
144 if (group
instanceof GroupInfoV1
) {
145 jgen
.writeObject(group
);
146 } else if (group
instanceof GroupInfoV2
) {
147 final GroupInfoV2 groupV2
= (GroupInfoV2
) group
;
148 jgen
.writeStartObject();
149 jgen
.writeStringField("groupId", Base64
.encodeBytes(groupV2
.groupId
));
150 jgen
.writeStringField("masterKey", Base64
.encodeBytes(groupV2
.getMasterKey().serialize()));
151 jgen
.writeBooleanField("blocked", groupV2
.isBlocked());
152 jgen
.writeEndObject();
154 throw new AssertionError("Unknown group version");
157 jgen
.writeEndArray();
161 private static class GroupsDeserializer
extends JsonDeserializer
<Map
<String
, GroupInfo
>> {
164 public Map
<String
, GroupInfo
> deserialize(
165 JsonParser jsonParser
, DeserializationContext deserializationContext
166 ) throws IOException
{
167 Map
<String
, GroupInfo
> groups
= new HashMap
<>();
168 JsonNode node
= jsonParser
.getCodec().readTree(jsonParser
);
169 for (JsonNode n
: node
) {
171 if (n
.has("masterKey")) {
173 byte[] groupId
= Base64
.decode(n
.get("groupId").asText());
175 GroupMasterKey masterKey
= new GroupMasterKey(Base64
.decode(n
.get("masterKey").asText()));
176 g
= new GroupInfoV2(groupId
, masterKey
);
177 } catch (InvalidInputException e
) {
178 throw new AssertionError("Invalid master key for group " + Base64
.encodeBytes(groupId
));
180 g
.setBlocked(n
.get("blocked").asBoolean(false));
182 GroupInfoV1 gv1
= jsonProcessor
.treeToValue(n
, GroupInfoV1
.class);
183 if (gv1
.expectedV2Id
== null) {
184 gv1
.expectedV2Id
= GroupUtils
.getGroupId(GroupUtils
.deriveV2MigrationMasterKey(gv1
.groupId
));
188 groups
.put(Base64
.encodeBytes(g
.groupId
), g
);