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 } catch (IOException e
) {
62 System
.err
.println("Failed to cache group, ignoring ...");
67 public void deleteGroup(GroupId groupId
) {
68 groups
.remove(groupId
);
71 public GroupInfo
getGroup(GroupId groupId
) {
72 GroupInfo group
= groups
.get(groupId
);
74 if (groupId
instanceof GroupIdV1
) {
75 group
= groups
.get(GroupUtils
.getGroupIdV2((GroupIdV1
) groupId
));
76 } else if (groupId
instanceof GroupIdV2
) {
77 group
= getGroupV1ByV2Id((GroupIdV2
) groupId
);
80 loadDecryptedGroup(group
);
84 private GroupInfoV1
getGroupV1ByV2Id(GroupIdV2 groupIdV2
) {
85 for (GroupInfo g
: groups
.values()) {
86 if (g
instanceof GroupInfoV1
) {
87 final GroupInfoV1 gv1
= (GroupInfoV1
) g
;
88 if (groupIdV2
.equals(gv1
.getExpectedV2Id())) {
96 private void loadDecryptedGroup(final GroupInfo group
) {
97 if (group
instanceof GroupInfoV2
&& ((GroupInfoV2
) group
).getGroup() == null) {
98 try (FileInputStream stream
= new FileInputStream(getGroupFile(group
.getGroupId()))) {
99 ((GroupInfoV2
) group
).setGroup(DecryptedGroup
.parseFrom(stream
));
100 } catch (IOException ignored
) {
105 private File
getGroupFile(final GroupId groupId
) {
106 return new File(groupCachePath
, Hex
.toStringCondensed(groupId
.serialize()));
109 public GroupInfoV1
getOrCreateGroupV1(GroupIdV1 groupId
) {
110 GroupInfo group
= getGroup(groupId
);
111 if (group
instanceof GroupInfoV1
) {
112 return (GroupInfoV1
) group
;
116 return new GroupInfoV1(groupId
);
122 public List
<GroupInfo
> getGroups() {
123 final Collection
<GroupInfo
> groups
= this.groups
.values();
124 for (GroupInfo group
: groups
) {
125 loadDecryptedGroup(group
);
127 return new ArrayList
<>(groups
);
130 private static class GroupsSerializer
extends JsonSerializer
<Map
<String
, GroupInfo
>> {
133 public void serialize(
134 final Map
<String
, GroupInfo
> value
, final JsonGenerator jgen
, final SerializerProvider provider
135 ) throws IOException
{
136 final Collection
<GroupInfo
> groups
= value
.values();
137 jgen
.writeStartArray(groups
.size());
138 for (GroupInfo group
: groups
) {
139 if (group
instanceof GroupInfoV1
) {
140 jgen
.writeObject(group
);
141 } else if (group
instanceof GroupInfoV2
) {
142 final GroupInfoV2 groupV2
= (GroupInfoV2
) group
;
143 jgen
.writeStartObject();
144 jgen
.writeStringField("groupId", groupV2
.getGroupId().toBase64());
145 jgen
.writeStringField("masterKey", Base64
.encodeBytes(groupV2
.getMasterKey().serialize()));
146 jgen
.writeBooleanField("blocked", groupV2
.isBlocked());
147 jgen
.writeEndObject();
149 throw new AssertionError("Unknown group version");
152 jgen
.writeEndArray();
156 private static class GroupsDeserializer
extends JsonDeserializer
<Map
<GroupId
, GroupInfo
>> {
159 public Map
<GroupId
, GroupInfo
> deserialize(
160 JsonParser jsonParser
, DeserializationContext deserializationContext
161 ) throws IOException
{
162 Map
<GroupId
, GroupInfo
> groups
= new HashMap
<>();
163 JsonNode node
= jsonParser
.getCodec().readTree(jsonParser
);
164 for (JsonNode n
: node
) {
166 if (n
.has("masterKey")) {
168 GroupIdV2 groupId
= GroupIdV2
.fromBase64(n
.get("groupId").asText());
170 GroupMasterKey masterKey
= new GroupMasterKey(Base64
.decode(n
.get("masterKey").asText()));
171 g
= new GroupInfoV2(groupId
, masterKey
);
172 } catch (InvalidInputException e
) {
173 throw new AssertionError("Invalid master key for group " + groupId
.toBase64());
175 g
.setBlocked(n
.get("blocked").asBoolean(false));
177 GroupInfoV1 gv1
= jsonProcessor
.treeToValue(n
, GroupInfoV1
.class);
180 groups
.put(g
.getGroupId(), g
);