]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/groups/JsonGroupStore.java
4bd19ea123a44788507771fd551546f3fa301bca
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / storage / groups / JsonGroupStore.java
1 package org.asamk.signal.manager.storage.groups;
2
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;
15
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;
27
28 import java.io.File;
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;
36 import java.util.Map;
37
38 public class JsonGroupStore {
39
40 private final static Logger logger = LoggerFactory.getLogger(JsonGroupStore.class);
41
42 private static final ObjectMapper jsonProcessor = new ObjectMapper();
43 @JsonIgnore
44 public File groupCachePath;
45
46 @JsonProperty("groups")
47 @JsonSerialize(using = GroupsSerializer.class)
48 @JsonDeserialize(using = GroupsDeserializer.class)
49 private final Map<GroupId, GroupInfo> groups = new HashMap<>();
50
51 private JsonGroupStore() {
52 }
53
54 public JsonGroupStore(final File groupCachePath) {
55 this.groupCachePath = groupCachePath;
56 }
57
58 public void updateGroup(GroupInfo group) {
59 groups.put(group.getGroupId(), group);
60 if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() != null) {
61 try {
62 IOUtils.createPrivateDirectories(groupCachePath);
63 try (var stream = new FileOutputStream(getGroupFile(group.getGroupId()))) {
64 ((GroupInfoV2) group).getGroup().writeTo(stream);
65 }
66 final var groupFileLegacy = getGroupFileLegacy(group.getGroupId());
67 if (groupFileLegacy.exists()) {
68 groupFileLegacy.delete();
69 }
70 } catch (IOException e) {
71 logger.warn("Failed to cache group, ignoring: {}", e.getMessage());
72 }
73 }
74 }
75
76 public void deleteGroup(GroupId groupId) {
77 groups.remove(groupId);
78 }
79
80 public GroupInfo getGroup(GroupId groupId) {
81 var group = groups.get(groupId);
82 if (group == null) {
83 if (groupId instanceof GroupIdV1) {
84 group = groups.get(GroupUtils.getGroupIdV2((GroupIdV1) groupId));
85 } else if (groupId instanceof GroupIdV2) {
86 group = getGroupV1ByV2Id((GroupIdV2) groupId);
87 }
88 }
89 loadDecryptedGroup(group);
90 return group;
91 }
92
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())) {
98 return gv1;
99 }
100 }
101 }
102 return null;
103 }
104
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());
110 }
111 if (!groupFile.exists()) {
112 return;
113 }
114 try (var stream = new FileInputStream(groupFile)) {
115 ((GroupInfoV2) group).setGroup(DecryptedGroup.parseFrom(stream));
116 } catch (IOException ignored) {
117 }
118 }
119 }
120
121 private File getGroupFileLegacy(final GroupId groupId) {
122 return new File(groupCachePath, Hex.toStringCondensed(groupId.serialize()));
123 }
124
125 private File getGroupFile(final GroupId groupId) {
126 return new File(groupCachePath, groupId.toBase64().replace("/", "_"));
127 }
128
129 public GroupInfoV1 getOrCreateGroupV1(GroupIdV1 groupId) {
130 var group = getGroup(groupId);
131 if (group instanceof GroupInfoV1) {
132 return (GroupInfoV1) group;
133 }
134
135 if (group == null) {
136 return new GroupInfoV1(groupId);
137 }
138
139 return null;
140 }
141
142 @JsonIgnore
143 public List<GroupInfo> getGroups() {
144 final var groups = this.groups.values();
145 for (var group : groups) {
146 loadDecryptedGroup(group);
147 }
148 return new ArrayList<>(groups);
149 }
150
151 private static class GroupsSerializer extends JsonSerializer<Map<String, GroupInfo>> {
152
153 @Override
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();
170 } else {
171 throw new AssertionError("Unknown group version");
172 }
173 }
174 jgen.writeEndArray();
175 }
176 }
177
178 private static class GroupsDeserializer extends JsonDeserializer<Map<GroupId, GroupInfo>> {
179
180 @Override
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);
186 for (var n : node) {
187 GroupInfo g;
188 if (n.hasNonNull("masterKey")) {
189 // a v2 group
190 var groupId = GroupIdV2.fromBase64(n.get("groupId").asText());
191 try {
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());
196 }
197 g.setBlocked(n.get("blocked").asBoolean(false));
198 } else {
199 g = jsonProcessor.treeToValue(n, GroupInfoV1.class);
200 }
201 groups.put(g.getGroupId(), g);
202 }
203
204 return groups;
205 }
206 }
207 }