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