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
.GroupId
;
16 import org
.asamk
.signal
.manager
.GroupIdV1
;
17 import org
.asamk
.signal
.manager
.GroupIdV2
;
18 import org
.asamk
.signal
.manager
.GroupUtils
;
19 import org
.asamk
.signal
.util
.Hex
;
20 import org
.asamk
.signal
.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
.whispersystems
.util
.Base64
;
27 import java
.io
.FileInputStream
;
28 import java
.io
.FileOutputStream
;
29 import java
.io
.IOException
;
30 import java
.util
.ArrayList
;
31 import java
.util
.Collection
;
32 import java
.util
.HashMap
;
33 import java
.util
.List
;
36 public class JsonGroupStore
{
38 private static final ObjectMapper jsonProcessor
= new ObjectMapper();
39 public File groupCachePath
;
41 @JsonProperty("groups")
42 @JsonSerialize(using
= GroupsSerializer
.class)
43 @JsonDeserialize(using
= GroupsDeserializer
.class)
44 private final Map
<GroupId
, GroupInfo
> groups
= new HashMap
<>();
46 private JsonGroupStore() {
49 public JsonGroupStore(final File groupCachePath
) {
50 this.groupCachePath
= groupCachePath
;
53 public void updateGroup(GroupInfo group
) {
54 groups
.put(group
.getGroupId(), group
);
55 if (group
instanceof GroupInfoV2
&& ((GroupInfoV2
) group
).getGroup() != null) {
57 IOUtils
.createPrivateDirectories(groupCachePath
);
58 try (FileOutputStream stream
= new FileOutputStream(getGroupFile(group
.getGroupId()))) {
59 ((GroupInfoV2
) group
).getGroup().writeTo(stream
);
61 final File groupFileLegacy
= getGroupFileLegacy(group
.getGroupId());
62 if (groupFileLegacy
.exists()) {
63 groupFileLegacy
.delete();
65 } catch (IOException e
) {
66 System
.err
.println("Failed to cache group, ignoring ...");
71 public void deleteGroup(GroupId groupId
) {
72 groups
.remove(groupId
);
75 public GroupInfo
getGroup(GroupId groupId
) {
76 GroupInfo group
= groups
.get(groupId
);
78 if (groupId
instanceof GroupIdV1
) {
79 group
= groups
.get(GroupUtils
.getGroupIdV2((GroupIdV1
) groupId
));
80 } else if (groupId
instanceof GroupIdV2
) {
81 group
= getGroupV1ByV2Id((GroupIdV2
) groupId
);
84 loadDecryptedGroup(group
);
88 private GroupInfoV1
getGroupV1ByV2Id(GroupIdV2 groupIdV2
) {
89 for (GroupInfo g
: groups
.values()) {
90 if (g
instanceof GroupInfoV1
) {
91 final GroupInfoV1 gv1
= (GroupInfoV1
) g
;
92 if (groupIdV2
.equals(gv1
.getExpectedV2Id())) {
100 private void loadDecryptedGroup(final GroupInfo group
) {
101 if (group
instanceof GroupInfoV2
&& ((GroupInfoV2
) group
).getGroup() == null) {
102 File groupFile
= getGroupFile(group
.getGroupId());
103 if (!groupFile
.exists()) {
104 groupFile
= getGroupFileLegacy(group
.getGroupId());
106 if (!groupFile
.exists()) {
109 try (FileInputStream stream
= new FileInputStream(groupFile
)) {
110 ((GroupInfoV2
) group
).setGroup(DecryptedGroup
.parseFrom(stream
));
111 } catch (IOException ignored
) {
116 private File
getGroupFileLegacy(final GroupId groupId
) {
117 return new File(groupCachePath
, Hex
.toStringCondensed(groupId
.serialize()));
120 private File
getGroupFile(final GroupId groupId
) {
121 return new File(groupCachePath
, groupId
.toBase64().replace("/", "_"));
124 public GroupInfoV1
getOrCreateGroupV1(GroupIdV1 groupId
) {
125 GroupInfo group
= getGroup(groupId
);
126 if (group
instanceof GroupInfoV1
) {
127 return (GroupInfoV1
) group
;
131 return new GroupInfoV1(groupId
);
137 public List
<GroupInfo
> getGroups() {
138 final Collection
<GroupInfo
> groups
= this.groups
.values();
139 for (GroupInfo group
: groups
) {
140 loadDecryptedGroup(group
);
142 return new ArrayList
<>(groups
);
145 private static class GroupsSerializer
extends JsonSerializer
<Map
<String
, GroupInfo
>> {
148 public void serialize(
149 final Map
<String
, GroupInfo
> value
, final JsonGenerator jgen
, final SerializerProvider provider
150 ) throws IOException
{
151 final Collection
<GroupInfo
> groups
= value
.values();
152 jgen
.writeStartArray(groups
.size());
153 for (GroupInfo group
: groups
) {
154 if (group
instanceof GroupInfoV1
) {
155 jgen
.writeObject(group
);
156 } else if (group
instanceof GroupInfoV2
) {
157 final GroupInfoV2 groupV2
= (GroupInfoV2
) group
;
158 jgen
.writeStartObject();
159 jgen
.writeStringField("groupId", groupV2
.getGroupId().toBase64());
160 jgen
.writeStringField("masterKey", Base64
.encodeBytes(groupV2
.getMasterKey().serialize()));
161 jgen
.writeBooleanField("blocked", groupV2
.isBlocked());
162 jgen
.writeEndObject();
164 throw new AssertionError("Unknown group version");
167 jgen
.writeEndArray();
171 private static class GroupsDeserializer
extends JsonDeserializer
<Map
<GroupId
, GroupInfo
>> {
174 public Map
<GroupId
, GroupInfo
> deserialize(
175 JsonParser jsonParser
, DeserializationContext deserializationContext
176 ) throws IOException
{
177 Map
<GroupId
, GroupInfo
> groups
= new HashMap
<>();
178 JsonNode node
= jsonParser
.getCodec().readTree(jsonParser
);
179 for (JsonNode n
: node
) {
181 if (n
.has("masterKey")) {
183 GroupIdV2 groupId
= GroupIdV2
.fromBase64(n
.get("groupId").asText());
185 GroupMasterKey masterKey
= new GroupMasterKey(Base64
.decode(n
.get("masterKey").asText()));
186 g
= new GroupInfoV2(groupId
, masterKey
);
187 } catch (InvalidInputException e
) {
188 throw new AssertionError("Invalid master key for group " + groupId
.toBase64());
190 g
.setBlocked(n
.get("blocked").asBoolean(false));
192 GroupInfoV1 gv1
= jsonProcessor
.treeToValue(n
, GroupInfoV1
.class);
195 groups
.put(g
.getGroupId(), g
);