1 package org
.asamk
.signal
.manager
.storage
.groups
;
3 import com
.fasterxml
.jackson
.annotation
.JsonIgnore
;
4 import com
.fasterxml
.jackson
.annotation
.JsonProperty
;
5 import com
.fasterxml
.jackson
.core
.JsonGenerator
;
6 import com
.fasterxml
.jackson
.core
.JsonParser
;
7 import com
.fasterxml
.jackson
.databind
.DeserializationContext
;
8 import com
.fasterxml
.jackson
.databind
.JsonDeserializer
;
9 import com
.fasterxml
.jackson
.databind
.JsonNode
;
10 import com
.fasterxml
.jackson
.databind
.JsonSerializer
;
11 import com
.fasterxml
.jackson
.databind
.ObjectMapper
;
12 import com
.fasterxml
.jackson
.databind
.SerializerProvider
;
13 import com
.fasterxml
.jackson
.databind
.annotation
.JsonDeserialize
;
14 import com
.fasterxml
.jackson
.databind
.annotation
.JsonSerialize
;
16 import org
.asamk
.signal
.manager
.groups
.GroupId
;
17 import org
.asamk
.signal
.manager
.groups
.GroupIdV1
;
18 import org
.asamk
.signal
.manager
.groups
.GroupIdV2
;
19 import org
.asamk
.signal
.manager
.groups
.GroupUtils
;
20 import org
.asamk
.signal
.manager
.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
.libsignal
.util
.Hex
;
29 import java
.io
.FileInputStream
;
30 import java
.io
.FileOutputStream
;
31 import java
.io
.IOException
;
32 import java
.util
.ArrayList
;
33 import java
.util
.Base64
;
34 import java
.util
.HashMap
;
35 import java
.util
.List
;
38 public class JsonGroupStore
{
40 private final static Logger logger
= LoggerFactory
.getLogger(JsonGroupStore
.class);
42 private static final ObjectMapper jsonProcessor
= new ObjectMapper();
44 public File groupCachePath
;
46 @JsonProperty("groups")
47 @JsonSerialize(using
= GroupsSerializer
.class)
48 @JsonDeserialize(using
= GroupsDeserializer
.class)
49 private final Map
<GroupId
, GroupInfo
> groups
= new HashMap
<>();
51 private JsonGroupStore() {
54 public JsonGroupStore(final File groupCachePath
) {
55 this.groupCachePath
= groupCachePath
;
58 public void updateGroup(GroupInfo group
) {
59 groups
.put(group
.getGroupId(), group
);
60 if (group
instanceof GroupInfoV2
&& ((GroupInfoV2
) group
).getGroup() != null) {
62 IOUtils
.createPrivateDirectories(groupCachePath
);
63 try (var stream
= new FileOutputStream(getGroupFile(group
.getGroupId()))) {
64 ((GroupInfoV2
) group
).getGroup().writeTo(stream
);
66 final var groupFileLegacy
= getGroupFileLegacy(group
.getGroupId());
67 if (groupFileLegacy
.exists()) {
68 groupFileLegacy
.delete();
70 } catch (IOException e
) {
71 logger
.warn("Failed to cache group, ignoring: {}", e
.getMessage());
76 public void deleteGroup(GroupId groupId
) {
77 groups
.remove(groupId
);
80 public GroupInfo
getGroup(GroupId groupId
) {
81 var group
= groups
.get(groupId
);
83 if (groupId
instanceof GroupIdV1
) {
84 group
= groups
.get(GroupUtils
.getGroupIdV2((GroupIdV1
) groupId
));
85 } else if (groupId
instanceof GroupIdV2
) {
86 group
= getGroupV1ByV2Id((GroupIdV2
) groupId
);
89 loadDecryptedGroup(group
);
93 private GroupInfoV1
getGroupV1ByV2Id(GroupIdV2 groupIdV2
) {
94 for (var g
: groups
.values()) {
95 if (g
instanceof GroupInfoV1
) {
96 final var gv1
= (GroupInfoV1
) g
;
97 if (groupIdV2
.equals(gv1
.getExpectedV2Id())) {
105 private void loadDecryptedGroup(final GroupInfo group
) {
106 if (group
instanceof GroupInfoV2
&& ((GroupInfoV2
) group
).getGroup() == null) {
107 var groupFile
= getGroupFile(group
.getGroupId());
108 if (!groupFile
.exists()) {
109 groupFile
= getGroupFileLegacy(group
.getGroupId());
111 if (!groupFile
.exists()) {
114 try (var stream
= new FileInputStream(groupFile
)) {
115 ((GroupInfoV2
) group
).setGroup(DecryptedGroup
.parseFrom(stream
));
116 } catch (IOException ignored
) {
121 private File
getGroupFileLegacy(final GroupId groupId
) {
122 return new File(groupCachePath
, Hex
.toStringCondensed(groupId
.serialize()));
125 private File
getGroupFile(final GroupId groupId
) {
126 return new File(groupCachePath
, groupId
.toBase64().replace("/", "_"));
129 public GroupInfoV1
getOrCreateGroupV1(GroupIdV1 groupId
) {
130 var group
= getGroup(groupId
);
131 if (group
instanceof GroupInfoV1
) {
132 return (GroupInfoV1
) group
;
136 return new GroupInfoV1(groupId
);
143 public List
<GroupInfo
> getGroups() {
144 final var groups
= this.groups
.values();
145 for (var group
: groups
) {
146 loadDecryptedGroup(group
);
148 return new ArrayList
<>(groups
);
151 private static class GroupsSerializer
extends JsonSerializer
<Map
<String
, GroupInfo
>> {
154 public void serialize(
155 final Map
<String
, GroupInfo
> value
, final JsonGenerator jgen
, final SerializerProvider provider
156 ) throws IOException
{
157 final var groups
= value
.values();
158 jgen
.writeStartArray(groups
.size());
159 for (var group
: groups
) {
160 if (group
instanceof GroupInfoV1
) {
161 jgen
.writeObject(group
);
162 } else if (group
instanceof GroupInfoV2
) {
163 final var groupV2
= (GroupInfoV2
) group
;
164 jgen
.writeStartObject();
165 jgen
.writeStringField("groupId", groupV2
.getGroupId().toBase64());
166 jgen
.writeStringField("masterKey",
167 Base64
.getEncoder().encodeToString(groupV2
.getMasterKey().serialize()));
168 jgen
.writeBooleanField("blocked", groupV2
.isBlocked());
169 jgen
.writeEndObject();
171 throw new AssertionError("Unknown group version");
174 jgen
.writeEndArray();
178 private static class GroupsDeserializer
extends JsonDeserializer
<Map
<GroupId
, GroupInfo
>> {
181 public Map
<GroupId
, GroupInfo
> deserialize(
182 JsonParser jsonParser
, DeserializationContext deserializationContext
183 ) throws IOException
{
184 var groups
= new HashMap
<GroupId
, GroupInfo
>();
185 JsonNode node
= jsonParser
.getCodec().readTree(jsonParser
);
188 if (n
.hasNonNull("masterKey")) {
190 var groupId
= GroupIdV2
.fromBase64(n
.get("groupId").asText());
192 var masterKey
= new GroupMasterKey(Base64
.getDecoder().decode(n
.get("masterKey").asText()));
193 g
= new GroupInfoV2(groupId
, masterKey
);
194 } catch (InvalidInputException
| IllegalArgumentException e
) {
195 throw new AssertionError("Invalid master key for group " + groupId
.toBase64());
197 g
.setBlocked(n
.get("blocked").asBoolean(false));
199 g
= jsonProcessor
.treeToValue(n
, GroupInfoV1
.class);
201 groups
.put(g
.getGroupId(), g
);