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
.slf4j
.Logger
;
25 import org
.slf4j
.LoggerFactory
;
26 import org
.whispersystems
.util
.Base64
;
29 import java
.io
.FileInputStream
;
30 import java
.io
.FileOutputStream
;
31 import java
.io
.IOException
;
32 import java
.util
.ArrayList
;
33 import java
.util
.Collection
;
34 import java
.util
.HashMap
;
35 import java
.util
.List
;
38 public class JsonGroupStore
{
40 final static Logger logger
= LoggerFactory
.getLogger(JsonGroupStore
.class);
42 private static final ObjectMapper jsonProcessor
= new ObjectMapper();
43 public File groupCachePath
;
45 @JsonProperty("groups")
46 @JsonSerialize(using
= GroupsSerializer
.class)
47 @JsonDeserialize(using
= GroupsDeserializer
.class)
48 private final Map
<GroupId
, GroupInfo
> groups
= new HashMap
<>();
50 private JsonGroupStore() {
53 public JsonGroupStore(final File groupCachePath
) {
54 this.groupCachePath
= groupCachePath
;
57 public void updateGroup(GroupInfo group
) {
58 groups
.put(group
.getGroupId(), group
);
59 if (group
instanceof GroupInfoV2
&& ((GroupInfoV2
) group
).getGroup() != null) {
61 IOUtils
.createPrivateDirectories(groupCachePath
);
62 try (FileOutputStream stream
= new FileOutputStream(getGroupFile(group
.getGroupId()))) {
63 ((GroupInfoV2
) group
).getGroup().writeTo(stream
);
65 final File groupFileLegacy
= getGroupFileLegacy(group
.getGroupId());
66 if (groupFileLegacy
.exists()) {
67 groupFileLegacy
.delete();
69 } catch (IOException e
) {
70 logger
.warn("Failed to cache group, ignoring: {}", e
.getMessage());
75 public void deleteGroup(GroupId groupId
) {
76 groups
.remove(groupId
);
79 public GroupInfo
getGroup(GroupId groupId
) {
80 GroupInfo group
= groups
.get(groupId
);
82 if (groupId
instanceof GroupIdV1
) {
83 group
= groups
.get(GroupUtils
.getGroupIdV2((GroupIdV1
) groupId
));
84 } else if (groupId
instanceof GroupIdV2
) {
85 group
= getGroupV1ByV2Id((GroupIdV2
) groupId
);
88 loadDecryptedGroup(group
);
92 private GroupInfoV1
getGroupV1ByV2Id(GroupIdV2 groupIdV2
) {
93 for (GroupInfo g
: groups
.values()) {
94 if (g
instanceof GroupInfoV1
) {
95 final GroupInfoV1 gv1
= (GroupInfoV1
) g
;
96 if (groupIdV2
.equals(gv1
.getExpectedV2Id())) {
104 private void loadDecryptedGroup(final GroupInfo group
) {
105 if (group
instanceof GroupInfoV2
&& ((GroupInfoV2
) group
).getGroup() == null) {
106 File groupFile
= getGroupFile(group
.getGroupId());
107 if (!groupFile
.exists()) {
108 groupFile
= getGroupFileLegacy(group
.getGroupId());
110 if (!groupFile
.exists()) {
113 try (FileInputStream stream
= new FileInputStream(groupFile
)) {
114 ((GroupInfoV2
) group
).setGroup(DecryptedGroup
.parseFrom(stream
));
115 } catch (IOException ignored
) {
120 private File
getGroupFileLegacy(final GroupId groupId
) {
121 return new File(groupCachePath
, Hex
.toStringCondensed(groupId
.serialize()));
124 private File
getGroupFile(final GroupId groupId
) {
125 return new File(groupCachePath
, groupId
.toBase64().replace("/", "_"));
128 public GroupInfoV1
getOrCreateGroupV1(GroupIdV1 groupId
) {
129 GroupInfo group
= getGroup(groupId
);
130 if (group
instanceof GroupInfoV1
) {
131 return (GroupInfoV1
) group
;
135 return new GroupInfoV1(groupId
);
141 public List
<GroupInfo
> getGroups() {
142 final Collection
<GroupInfo
> groups
= this.groups
.values();
143 for (GroupInfo group
: groups
) {
144 loadDecryptedGroup(group
);
146 return new ArrayList
<>(groups
);
149 private static class GroupsSerializer
extends JsonSerializer
<Map
<String
, GroupInfo
>> {
152 public void serialize(
153 final Map
<String
, GroupInfo
> value
, final JsonGenerator jgen
, final SerializerProvider provider
154 ) throws IOException
{
155 final Collection
<GroupInfo
> groups
= value
.values();
156 jgen
.writeStartArray(groups
.size());
157 for (GroupInfo group
: groups
) {
158 if (group
instanceof GroupInfoV1
) {
159 jgen
.writeObject(group
);
160 } else if (group
instanceof GroupInfoV2
) {
161 final GroupInfoV2 groupV2
= (GroupInfoV2
) group
;
162 jgen
.writeStartObject();
163 jgen
.writeStringField("groupId", groupV2
.getGroupId().toBase64());
164 jgen
.writeStringField("masterKey", Base64
.encodeBytes(groupV2
.getMasterKey().serialize()));
165 jgen
.writeBooleanField("blocked", groupV2
.isBlocked());
166 jgen
.writeEndObject();
168 throw new AssertionError("Unknown group version");
171 jgen
.writeEndArray();
175 private static class GroupsDeserializer
extends JsonDeserializer
<Map
<GroupId
, GroupInfo
>> {
178 public Map
<GroupId
, GroupInfo
> deserialize(
179 JsonParser jsonParser
, DeserializationContext deserializationContext
180 ) throws IOException
{
181 Map
<GroupId
, GroupInfo
> groups
= new HashMap
<>();
182 JsonNode node
= jsonParser
.getCodec().readTree(jsonParser
);
183 for (JsonNode n
: node
) {
185 if (n
.has("masterKey")) {
187 GroupIdV2 groupId
= GroupIdV2
.fromBase64(n
.get("groupId").asText());
189 GroupMasterKey masterKey
= new GroupMasterKey(Base64
.decode(n
.get("masterKey").asText()));
190 g
= new GroupInfoV2(groupId
, masterKey
);
191 } catch (InvalidInputException e
) {
192 throw new AssertionError("Invalid master key for group " + groupId
.toBase64());
194 g
.setBlocked(n
.get("blocked").asBoolean(false));
196 GroupInfoV1 gv1
= jsonProcessor
.treeToValue(n
, GroupInfoV1
.class);
199 groups
.put(g
.getGroupId(), g
);