]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java
f24ee99cf42f07b14053e0c9a983fac99099ef7c
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / storage / groups / GroupStore.java
1 package org.asamk.signal.manager.storage.groups;
2
3 import com.fasterxml.jackson.annotation.JsonInclude;
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.SerializerProvider;
11 import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
12 import com.fasterxml.jackson.databind.annotation.JsonSerialize;
13
14 import org.asamk.signal.manager.groups.GroupId;
15 import org.asamk.signal.manager.groups.GroupIdV1;
16 import org.asamk.signal.manager.groups.GroupIdV2;
17 import org.asamk.signal.manager.groups.GroupUtils;
18 import org.asamk.signal.manager.storage.recipients.RecipientAddress;
19 import org.asamk.signal.manager.storage.recipients.RecipientId;
20 import org.asamk.signal.manager.storage.recipients.RecipientResolver;
21 import org.asamk.signal.manager.util.IOUtils;
22 import org.signal.storageservice.protos.groups.local.DecryptedGroup;
23 import org.signal.zkgroup.InvalidInputException;
24 import org.signal.zkgroup.groups.GroupMasterKey;
25 import org.slf4j.Logger;
26 import org.slf4j.LoggerFactory;
27 import org.whispersystems.signalservice.api.push.DistributionId;
28 import org.whispersystems.signalservice.api.util.UuidUtil;
29 import org.whispersystems.signalservice.internal.util.Hex;
30
31 import java.io.File;
32 import java.io.FileInputStream;
33 import java.io.FileOutputStream;
34 import java.io.IOException;
35 import java.nio.file.Files;
36 import java.util.ArrayList;
37 import java.util.Base64;
38 import java.util.HashMap;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Objects;
42 import java.util.stream.Collectors;
43
44 public class GroupStore {
45
46 private final static Logger logger = LoggerFactory.getLogger(GroupStore.class);
47
48 private final File groupCachePath;
49 private final Map<GroupId, GroupInfo> groups;
50 private final RecipientResolver recipientResolver;
51 private final Saver saver;
52
53 private GroupStore(
54 final File groupCachePath,
55 final Map<GroupId, GroupInfo> groups,
56 final RecipientResolver recipientResolver,
57 final Saver saver
58 ) {
59 this.groupCachePath = groupCachePath;
60 this.groups = groups;
61 this.recipientResolver = recipientResolver;
62 this.saver = saver;
63 }
64
65 public GroupStore(
66 final File groupCachePath, final RecipientResolver recipientResolver, final Saver saver
67 ) {
68 this.groups = new HashMap<>();
69 this.groupCachePath = groupCachePath;
70 this.recipientResolver = recipientResolver;
71 this.saver = saver;
72 }
73
74 public static GroupStore fromStorage(
75 final Storage storage,
76 final File groupCachePath,
77 final RecipientResolver recipientResolver,
78 final Saver saver
79 ) {
80 final var groups = storage.groups.stream().map(g -> {
81 if (g instanceof Storage.GroupV1 g1) {
82 final var members = g1.members.stream().map(m -> {
83 if (m.recipientId == null) {
84 return recipientResolver.resolveRecipient(new RecipientAddress(UuidUtil.parseOrNull(m.uuid),
85 m.number));
86 }
87
88 return recipientResolver.resolveRecipient(m.recipientId);
89 }).filter(Objects::nonNull).collect(Collectors.toSet());
90
91 return new GroupInfoV1(GroupIdV1.fromBase64(g1.groupId),
92 g1.expectedV2Id == null ? null : GroupIdV2.fromBase64(g1.expectedV2Id),
93 g1.name,
94 members,
95 g1.color,
96 g1.messageExpirationTime,
97 g1.blocked,
98 g1.archived);
99 }
100
101 final var g2 = (Storage.GroupV2) g;
102 var groupId = GroupIdV2.fromBase64(g2.groupId);
103 GroupMasterKey masterKey;
104 try {
105 masterKey = new GroupMasterKey(Base64.getDecoder().decode(g2.masterKey));
106 } catch (InvalidInputException | IllegalArgumentException e) {
107 throw new AssertionError("Invalid master key for group " + groupId.toBase64());
108 }
109
110 return new GroupInfoV2(groupId,
111 masterKey,
112 g2.distributionId == null ? null : DistributionId.from(g2.distributionId),
113 g2.blocked,
114 g2.permissionDenied);
115 }).collect(Collectors.toMap(GroupInfo::getGroupId, g -> g));
116
117 return new GroupStore(groupCachePath, groups, recipientResolver, saver);
118 }
119
120 public void updateGroup(GroupInfo group) {
121 final Storage storage;
122 synchronized (groups) {
123 groups.put(group.getGroupId(), group);
124 if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() != null) {
125 try {
126 IOUtils.createPrivateDirectories(groupCachePath);
127 try (var stream = new FileOutputStream(getGroupV2File(group.getGroupId()))) {
128 ((GroupInfoV2) group).getGroup().writeTo(stream);
129 }
130 final var groupFileLegacy = getGroupV2FileLegacy(group.getGroupId());
131 if (groupFileLegacy.exists()) {
132 try {
133 Files.delete(groupFileLegacy.toPath());
134 } catch (IOException e) {
135 logger.error("Failed to delete legacy group file {}: {}", groupFileLegacy, e.getMessage());
136 }
137 }
138 } catch (IOException e) {
139 logger.warn("Failed to cache group, ignoring: {}", e.getMessage());
140 }
141 }
142 storage = toStorageLocked();
143 }
144 saver.save(storage);
145 }
146
147 public void deleteGroupV1(GroupIdV1 groupIdV1) {
148 deleteGroup(groupIdV1);
149 }
150
151 public void deleteGroup(GroupId groupId) {
152 final Storage storage;
153 synchronized (groups) {
154 groups.remove(groupId);
155 storage = toStorageLocked();
156 }
157 saver.save(storage);
158 }
159
160 public GroupInfo getGroup(GroupId groupId) {
161 synchronized (groups) {
162 return getGroupLocked(groupId);
163 }
164 }
165
166 public GroupInfoV1 getOrCreateGroupV1(GroupIdV1 groupId) {
167 synchronized (groups) {
168 var group = getGroupLocked(groupId);
169 if (group instanceof GroupInfoV1) {
170 return (GroupInfoV1) group;
171 }
172
173 if (group == null) {
174 return new GroupInfoV1(groupId);
175 }
176
177 return null;
178 }
179 }
180
181 public List<GroupInfo> getGroups() {
182 synchronized (groups) {
183 final var groups = this.groups.values();
184 for (var group : groups) {
185 loadDecryptedGroupLocked(group);
186 }
187 return new ArrayList<>(groups);
188 }
189 }
190
191 public void mergeRecipients(final RecipientId recipientId, final RecipientId toBeMergedRecipientId) {
192 synchronized (groups) {
193 var modified = false;
194 for (var group : this.groups.values()) {
195 if (group instanceof GroupInfoV1 groupV1) {
196 if (groupV1.isMember(toBeMergedRecipientId)) {
197 groupV1.removeMember(toBeMergedRecipientId);
198 groupV1.addMembers(List.of(recipientId));
199 modified = true;
200 }
201 }
202 }
203 if (modified) {
204 saver.save(toStorageLocked());
205 }
206 }
207 }
208
209 private GroupInfo getGroupLocked(final GroupId groupId) {
210 var group = groups.get(groupId);
211 if (group == null) {
212 if (groupId instanceof GroupIdV1) {
213 group = getGroupByV1IdLocked((GroupIdV1) groupId);
214 } else if (groupId instanceof GroupIdV2) {
215 group = getGroupV1ByV2IdLocked((GroupIdV2) groupId);
216 }
217 }
218 loadDecryptedGroupLocked(group);
219 return group;
220 }
221
222 private GroupInfo getGroupByV1IdLocked(final GroupIdV1 groupId) {
223 return groups.get(GroupUtils.getGroupIdV2(groupId));
224 }
225
226 private GroupInfoV1 getGroupV1ByV2IdLocked(GroupIdV2 groupIdV2) {
227 for (var g : groups.values()) {
228 if (g instanceof GroupInfoV1 gv1) {
229 if (groupIdV2.equals(gv1.getExpectedV2Id())) {
230 return gv1;
231 }
232 }
233 }
234 return null;
235 }
236
237 private void loadDecryptedGroupLocked(final GroupInfo group) {
238 if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) {
239 var groupFile = getGroupV2File(group.getGroupId());
240 if (!groupFile.exists()) {
241 groupFile = getGroupV2FileLegacy(group.getGroupId());
242 }
243 if (!groupFile.exists()) {
244 return;
245 }
246 try (var stream = new FileInputStream(groupFile)) {
247 ((GroupInfoV2) group).setGroup(DecryptedGroup.parseFrom(stream), recipientResolver);
248 } catch (IOException ignored) {
249 }
250 }
251 }
252
253 private File getGroupV2FileLegacy(final GroupId groupId) {
254 return new File(groupCachePath, Hex.toStringCondensed(groupId.serialize()));
255 }
256
257 private File getGroupV2File(final GroupId groupId) {
258 return new File(groupCachePath, groupId.toBase64().replace("/", "_"));
259 }
260
261 private Storage toStorageLocked() {
262 return new Storage(groups.values().stream().map(g -> {
263 if (g instanceof GroupInfoV1 g1) {
264 return new Storage.GroupV1(g1.getGroupId().toBase64(),
265 g1.getExpectedV2Id().toBase64(),
266 g1.name,
267 g1.color,
268 g1.messageExpirationTime,
269 g1.blocked,
270 g1.archived,
271 g1.members.stream().map(m -> new Storage.GroupV1.Member(m.id(), null, null)).toList());
272 }
273
274 final var g2 = (GroupInfoV2) g;
275 return new Storage.GroupV2(g2.getGroupId().toBase64(),
276 Base64.getEncoder().encodeToString(g2.getMasterKey().serialize()),
277 g2.getDistributionId() == null ? null : g2.getDistributionId().toString(),
278 g2.isBlocked(),
279 g2.isPermissionDenied());
280 }).toList());
281 }
282
283 public record Storage(@JsonDeserialize(using = GroupsDeserializer.class) List<Record> groups) {
284
285 private record GroupV1(
286 String groupId,
287 String expectedV2Id,
288 String name,
289 String color,
290 int messageExpirationTime,
291 boolean blocked,
292 boolean archived,
293 @JsonDeserialize(using = MembersDeserializer.class) @JsonSerialize(using = MembersSerializer.class) List<Member> members
294 ) {
295
296 private record Member(Long recipientId, String uuid, String number) {}
297
298 private record JsonRecipientAddress(String uuid, String number) {}
299
300 private static class MembersSerializer extends JsonSerializer<List<Member>> {
301
302 @Override
303 public void serialize(
304 final List<Member> value, final JsonGenerator jgen, final SerializerProvider provider
305 ) throws IOException {
306 jgen.writeStartArray(null, value.size());
307 for (var address : value) {
308 if (address.recipientId != null) {
309 jgen.writeNumber(address.recipientId);
310 } else if (address.uuid != null) {
311 jgen.writeObject(new JsonRecipientAddress(address.uuid, address.number));
312 } else {
313 jgen.writeString(address.number);
314 }
315 }
316 jgen.writeEndArray();
317 }
318 }
319
320 private static class MembersDeserializer extends JsonDeserializer<List<Member>> {
321
322 @Override
323 public List<Member> deserialize(
324 JsonParser jsonParser, DeserializationContext deserializationContext
325 ) throws IOException {
326 var addresses = new ArrayList<Member>();
327 JsonNode node = jsonParser.getCodec().readTree(jsonParser);
328 for (var n : node) {
329 if (n.isTextual()) {
330 addresses.add(new Member(null, null, n.textValue()));
331 } else if (n.isNumber()) {
332 addresses.add(new Member(n.numberValue().longValue(), null, null));
333 } else {
334 var address = jsonParser.getCodec().treeToValue(n, JsonRecipientAddress.class);
335 addresses.add(new Member(null, address.uuid, address.number));
336 }
337 }
338
339 return addresses;
340 }
341 }
342 }
343
344 private record GroupV2(
345 String groupId,
346 String masterKey,
347 String distributionId,
348 @JsonInclude(JsonInclude.Include.NON_DEFAULT) boolean blocked,
349 @JsonInclude(JsonInclude.Include.NON_DEFAULT) boolean permissionDenied
350 ) {}
351 }
352
353 private static class GroupsDeserializer extends JsonDeserializer<List<Object>> {
354
355 @Override
356 public List<Object> deserialize(
357 JsonParser jsonParser, DeserializationContext deserializationContext
358 ) throws IOException {
359 var groups = new ArrayList<>();
360 JsonNode node = jsonParser.getCodec().readTree(jsonParser);
361 for (var n : node) {
362 Object g;
363 if (n.hasNonNull("masterKey")) {
364 // a v2 group
365 g = jsonParser.getCodec().treeToValue(n, Storage.GroupV2.class);
366 } else {
367 g = jsonParser.getCodec().treeToValue(n, Storage.GroupV1.class);
368 }
369 groups.add(g);
370 }
371
372 return groups;
373 }
374 }
375
376 public interface Saver {
377
378 void save(Storage storage);
379 }
380 }