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