]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/groups/JsonGroupStore.java
8e37895a679fb4cefe5c1b5e36f656e68f482de7
[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.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;
14
15 import org.asamk.signal.manager.groups.GroupId;
16 import org.asamk.signal.manager.groups.GroupIdV1;
17 import org.asamk.signal.manager.groups.GroupIdV2;
18 import org.asamk.signal.manager.groups.GroupUtils;
19 import org.asamk.signal.manager.util.IOUtils;
20 import org.signal.storageservice.protos.groups.local.DecryptedGroup;
21 import org.signal.zkgroup.InvalidInputException;
22 import org.signal.zkgroup.groups.GroupMasterKey;
23 import org.slf4j.Logger;
24 import org.slf4j.LoggerFactory;
25 import org.whispersystems.libsignal.util.Hex;
26
27 import java.io.File;
28 import java.io.FileInputStream;
29 import java.io.FileOutputStream;
30 import java.io.IOException;
31 import java.util.ArrayList;
32 import java.util.Base64;
33 import java.util.HashMap;
34 import java.util.List;
35 import java.util.Map;
36
37 public class JsonGroupStore {
38
39 private final static Logger logger = LoggerFactory.getLogger(JsonGroupStore.class);
40
41 private static final ObjectMapper jsonProcessor = new ObjectMapper();
42 public File groupCachePath;
43
44 @JsonProperty("groups")
45 @JsonSerialize(using = GroupsSerializer.class)
46 @JsonDeserialize(using = GroupsDeserializer.class)
47 private final Map<GroupId, GroupInfo> groups = new HashMap<>();
48
49 private JsonGroupStore() {
50 }
51
52 public JsonGroupStore(final File groupCachePath) {
53 this.groupCachePath = groupCachePath;
54 }
55
56 public void updateGroup(GroupInfo group) {
57 groups.put(group.getGroupId(), group);
58 if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() != null) {
59 try {
60 IOUtils.createPrivateDirectories(groupCachePath);
61 try (var stream = new FileOutputStream(getGroupFile(group.getGroupId()))) {
62 ((GroupInfoV2) group).getGroup().writeTo(stream);
63 }
64 final var groupFileLegacy = getGroupFileLegacy(group.getGroupId());
65 if (groupFileLegacy.exists()) {
66 groupFileLegacy.delete();
67 }
68 } catch (IOException e) {
69 logger.warn("Failed to cache group, ignoring: {}", e.getMessage());
70 }
71 }
72 }
73
74 public void deleteGroup(GroupId groupId) {
75 groups.remove(groupId);
76 }
77
78 public GroupInfo getGroup(GroupId groupId) {
79 var group = groups.get(groupId);
80 if (group == null) {
81 if (groupId instanceof GroupIdV1) {
82 group = groups.get(GroupUtils.getGroupIdV2((GroupIdV1) groupId));
83 } else if (groupId instanceof GroupIdV2) {
84 group = getGroupV1ByV2Id((GroupIdV2) groupId);
85 }
86 }
87 loadDecryptedGroup(group);
88 return group;
89 }
90
91 private GroupInfoV1 getGroupV1ByV2Id(GroupIdV2 groupIdV2) {
92 for (var g : groups.values()) {
93 if (g instanceof GroupInfoV1) {
94 final var gv1 = (GroupInfoV1) g;
95 if (groupIdV2.equals(gv1.getExpectedV2Id())) {
96 return gv1;
97 }
98 }
99 }
100 return null;
101 }
102
103 private void loadDecryptedGroup(final GroupInfo group) {
104 if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) {
105 var groupFile = getGroupFile(group.getGroupId());
106 if (!groupFile.exists()) {
107 groupFile = getGroupFileLegacy(group.getGroupId());
108 }
109 if (!groupFile.exists()) {
110 return;
111 }
112 try (var stream = new FileInputStream(groupFile)) {
113 ((GroupInfoV2) group).setGroup(DecryptedGroup.parseFrom(stream));
114 } catch (IOException ignored) {
115 }
116 }
117 }
118
119 private File getGroupFileLegacy(final GroupId groupId) {
120 return new File(groupCachePath, Hex.toStringCondensed(groupId.serialize()));
121 }
122
123 private File getGroupFile(final GroupId groupId) {
124 return new File(groupCachePath, groupId.toBase64().replace("/", "_"));
125 }
126
127 public GroupInfoV1 getOrCreateGroupV1(GroupIdV1 groupId) {
128 var group = getGroup(groupId);
129 if (group instanceof GroupInfoV1) {
130 return (GroupInfoV1) group;
131 }
132
133 if (group == null) {
134 return new GroupInfoV1(groupId);
135 }
136
137 return null;
138 }
139
140 public List<GroupInfo> getGroups() {
141 final var groups = this.groups.values();
142 for (var group : groups) {
143 loadDecryptedGroup(group);
144 }
145 return new ArrayList<>(groups);
146 }
147
148 private static class GroupsSerializer extends JsonSerializer<Map<String, GroupInfo>> {
149
150 @Override
151 public void serialize(
152 final Map<String, GroupInfo> value, final JsonGenerator jgen, final SerializerProvider provider
153 ) throws IOException {
154 final var groups = value.values();
155 jgen.writeStartArray(groups.size());
156 for (var group : groups) {
157 if (group instanceof GroupInfoV1) {
158 jgen.writeObject(group);
159 } else if (group instanceof GroupInfoV2) {
160 final var groupV2 = (GroupInfoV2) group;
161 jgen.writeStartObject();
162 jgen.writeStringField("groupId", groupV2.getGroupId().toBase64());
163 jgen.writeStringField("masterKey",
164 Base64.getEncoder().encodeToString(groupV2.getMasterKey().serialize()));
165 jgen.writeBooleanField("blocked", groupV2.isBlocked());
166 jgen.writeEndObject();
167 } else {
168 throw new AssertionError("Unknown group version");
169 }
170 }
171 jgen.writeEndArray();
172 }
173 }
174
175 private static class GroupsDeserializer extends JsonDeserializer<Map<GroupId, GroupInfo>> {
176
177 @Override
178 public Map<GroupId, GroupInfo> deserialize(
179 JsonParser jsonParser, DeserializationContext deserializationContext
180 ) throws IOException {
181 var groups = new HashMap<GroupId, GroupInfo>();
182 JsonNode node = jsonParser.getCodec().readTree(jsonParser);
183 for (var n : node) {
184 GroupInfo g;
185 if (n.hasNonNull("masterKey")) {
186 // a v2 group
187 var groupId = GroupIdV2.fromBase64(n.get("groupId").asText());
188 try {
189 var masterKey = new GroupMasterKey(Base64.getDecoder().decode(n.get("masterKey").asText()));
190 g = new GroupInfoV2(groupId, masterKey);
191 } catch (InvalidInputException | IllegalArgumentException e) {
192 throw new AssertionError("Invalid master key for group " + groupId.toBase64());
193 }
194 g.setBlocked(n.get("blocked").asBoolean(false));
195 } else {
196 g = jsonProcessor.treeToValue(n, GroupInfoV1.class);
197 }
198 groups.put(g.getGroupId(), g);
199 }
200
201 return groups;
202 }
203 }
204 }