]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java
19fc7564614fe450948a27ecc47f67b6b84d1e1b
[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.core.JsonGenerator;
4 import com.fasterxml.jackson.core.JsonParser;
5 import com.fasterxml.jackson.databind.DeserializationContext;
6 import com.fasterxml.jackson.databind.JsonDeserializer;
7 import com.fasterxml.jackson.databind.JsonNode;
8 import com.fasterxml.jackson.databind.JsonSerializer;
9 import com.fasterxml.jackson.databind.SerializerProvider;
10 import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
11 import com.fasterxml.jackson.databind.annotation.JsonSerialize;
12
13 import org.asamk.signal.manager.groups.GroupId;
14 import org.asamk.signal.manager.groups.GroupIdV1;
15 import org.asamk.signal.manager.groups.GroupIdV2;
16 import org.asamk.signal.manager.groups.GroupUtils;
17 import org.asamk.signal.manager.storage.recipients.RecipientId;
18 import org.asamk.signal.manager.storage.recipients.RecipientResolver;
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 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
27 import org.whispersystems.signalservice.api.util.UuidUtil;
28
29 import java.io.File;
30 import java.io.FileInputStream;
31 import java.io.FileOutputStream;
32 import java.io.IOException;
33 import java.util.ArrayList;
34 import java.util.Base64;
35 import java.util.HashMap;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.stream.Collectors;
39
40 public class GroupStore {
41
42 private final static Logger logger = LoggerFactory.getLogger(GroupStore.class);
43
44 private final File groupCachePath;
45 private final Map<GroupId, GroupInfo> groups;
46 private final RecipientResolver recipientResolver;
47 private final Saver saver;
48
49 private GroupStore(
50 final File groupCachePath,
51 final Map<GroupId, GroupInfo> groups,
52 final RecipientResolver recipientResolver,
53 final Saver saver
54 ) {
55 this.groupCachePath = groupCachePath;
56 this.groups = groups;
57 this.recipientResolver = recipientResolver;
58 this.saver = saver;
59 }
60
61 public GroupStore(
62 final File groupCachePath, final RecipientResolver recipientResolver, final Saver saver
63 ) {
64 this.groups = new HashMap<>();
65 this.groupCachePath = groupCachePath;
66 this.recipientResolver = recipientResolver;
67 this.saver = saver;
68 }
69
70 public static GroupStore fromStorage(
71 final Storage storage,
72 final File groupCachePath,
73 final RecipientResolver recipientResolver,
74 final Saver saver
75 ) {
76 final var groups = storage.groups.stream().map(g -> {
77 if (g instanceof Storage.GroupV1) {
78 final var g1 = (Storage.GroupV1) g;
79 final var members = g1.members.stream().map(m -> {
80 if (m.recipientId == null) {
81 return recipientResolver.resolveRecipient(new SignalServiceAddress(UuidUtil.parseOrNull(m.uuid),
82 m.number));
83 }
84
85 return RecipientId.of(m.recipientId);
86 }).collect(Collectors.toSet());
87
88 return new GroupInfoV1(GroupIdV1.fromBase64(g1.groupId),
89 g1.expectedV2Id == null ? null : GroupIdV2.fromBase64(g1.expectedV2Id),
90 g1.name,
91 members,
92 g1.color,
93 g1.messageExpirationTime,
94 g1.blocked,
95 g1.archived);
96 }
97
98 final var g2 = (Storage.GroupV2) g;
99 var groupId = GroupIdV2.fromBase64(g2.groupId);
100 GroupMasterKey masterKey;
101 try {
102 masterKey = new GroupMasterKey(Base64.getDecoder().decode(g2.masterKey));
103 } catch (InvalidInputException | IllegalArgumentException e) {
104 throw new AssertionError("Invalid master key for group " + groupId.toBase64());
105 }
106
107 return new GroupInfoV2(groupId, masterKey, g2.blocked);
108 }).collect(Collectors.toMap(GroupInfo::getGroupId, g -> g));
109
110 return new GroupStore(groupCachePath, groups, recipientResolver, saver);
111 }
112
113 public void updateGroup(GroupInfo group) {
114 final Storage storage;
115 synchronized (groups) {
116 groups.put(group.getGroupId(), group);
117 if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() != null) {
118 try {
119 IOUtils.createPrivateDirectories(groupCachePath);
120 try (var stream = new FileOutputStream(getGroupV2File(group.getGroupId()))) {
121 ((GroupInfoV2) group).getGroup().writeTo(stream);
122 }
123 final var groupFileLegacy = getGroupV2FileLegacy(group.getGroupId());
124 if (groupFileLegacy.exists()) {
125 groupFileLegacy.delete();
126 }
127 } catch (IOException e) {
128 logger.warn("Failed to cache group, ignoring: {}", e.getMessage());
129 }
130 }
131 storage = toStorageLocked();
132 }
133 saver.save(storage);
134 }
135
136 public void deleteGroupV1(GroupIdV1 groupId) {
137 final Storage storage;
138 synchronized (groups) {
139 groups.remove(groupId);
140 storage = toStorageLocked();
141 }
142 saver.save(storage);
143 }
144
145 public GroupInfo getGroup(GroupId groupId) {
146 synchronized (groups) {
147 return getGroupLocked(groupId);
148 }
149 }
150
151 public GroupInfoV1 getOrCreateGroupV1(GroupIdV1 groupId) {
152 synchronized (groups) {
153 var group = getGroupLocked(groupId);
154 if (group instanceof GroupInfoV1) {
155 return (GroupInfoV1) group;
156 }
157
158 if (group == null) {
159 return new GroupInfoV1(groupId);
160 }
161
162 return null;
163 }
164 }
165
166 public List<GroupInfo> getGroups() {
167 synchronized (groups) {
168 final var groups = this.groups.values();
169 for (var group : groups) {
170 loadDecryptedGroupLocked(group);
171 }
172 return new ArrayList<>(groups);
173 }
174 }
175
176 public void mergeRecipients(final RecipientId recipientId, final RecipientId toBeMergedRecipientId) {
177 synchronized (groups) {
178 var modified = false;
179 for (var group : this.groups.values()) {
180 if (group instanceof GroupInfoV1) {
181 var groupV1 = (GroupInfoV1) group;
182 if (groupV1.isMember(toBeMergedRecipientId)) {
183 groupV1.removeMember(toBeMergedRecipientId);
184 groupV1.addMembers(List.of(recipientId));
185 modified = true;
186 }
187 }
188 }
189 if (modified) {
190 saver.save(toStorageLocked());
191 }
192 }
193 }
194
195 private GroupInfo getGroupLocked(final GroupId groupId) {
196 var group = groups.get(groupId);
197 if (group == null) {
198 if (groupId instanceof GroupIdV1) {
199 group = getGroupByV1IdLocked((GroupIdV1) groupId);
200 } else if (groupId instanceof GroupIdV2) {
201 group = getGroupV1ByV2IdLocked((GroupIdV2) groupId);
202 }
203 }
204 loadDecryptedGroupLocked(group);
205 return group;
206 }
207
208 private GroupInfo getGroupByV1IdLocked(final GroupIdV1 groupId) {
209 return groups.get(GroupUtils.getGroupIdV2(groupId));
210 }
211
212 private GroupInfoV1 getGroupV1ByV2IdLocked(GroupIdV2 groupIdV2) {
213 for (var g : groups.values()) {
214 if (g instanceof GroupInfoV1) {
215 final var gv1 = (GroupInfoV1) g;
216 if (groupIdV2.equals(gv1.getExpectedV2Id())) {
217 return gv1;
218 }
219 }
220 }
221 return null;
222 }
223
224 private void loadDecryptedGroupLocked(final GroupInfo group) {
225 if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) {
226 var groupFile = getGroupV2File(group.getGroupId());
227 if (!groupFile.exists()) {
228 groupFile = getGroupV2FileLegacy(group.getGroupId());
229 }
230 if (!groupFile.exists()) {
231 return;
232 }
233 try (var stream = new FileInputStream(groupFile)) {
234 ((GroupInfoV2) group).setGroup(DecryptedGroup.parseFrom(stream), recipientResolver);
235 } catch (IOException ignored) {
236 }
237 }
238 }
239
240 private File getGroupV2FileLegacy(final GroupId groupId) {
241 return new File(groupCachePath, Hex.toStringCondensed(groupId.serialize()));
242 }
243
244 private File getGroupV2File(final GroupId groupId) {
245 return new File(groupCachePath, groupId.toBase64().replace("/", "_"));
246 }
247
248 private Storage toStorageLocked() {
249 return new Storage(groups.values().stream().map(g -> {
250 if (g instanceof GroupInfoV1) {
251 final var g1 = (GroupInfoV1) g;
252 return new Storage.GroupV1(g1.getGroupId().toBase64(),
253 g1.getExpectedV2Id().toBase64(),
254 g1.name,
255 g1.color,
256 g1.messageExpirationTime,
257 g1.blocked,
258 g1.archived,
259 g1.members.stream()
260 .map(m -> new Storage.GroupV1.Member(m.getId(), null, null))
261 .collect(Collectors.toList()));
262 }
263
264 final var g2 = (GroupInfoV2) g;
265 return new Storage.GroupV2(g2.getGroupId().toBase64(),
266 Base64.getEncoder().encodeToString(g2.getMasterKey().serialize()),
267 g2.isBlocked());
268 }).collect(Collectors.toList()));
269 }
270
271 public static class Storage {
272
273 // @JsonSerialize(using = GroupsSerializer.class)
274 @JsonDeserialize(using = GroupsDeserializer.class)
275 public List<Storage.Group> groups;
276
277 // For deserialization
278 public Storage() {
279 }
280
281 public Storage(final List<Storage.Group> groups) {
282 this.groups = groups;
283 }
284
285 private abstract static class Group {
286
287 }
288
289 private static class GroupV1 extends Group {
290
291 public String groupId;
292 public String expectedV2Id;
293 public String name;
294 public String color;
295 public int messageExpirationTime;
296 public boolean blocked;
297 public boolean archived;
298
299 @JsonDeserialize(using = MembersDeserializer.class)
300 @JsonSerialize(using = MembersSerializer.class)
301 public List<Member> members;
302
303 // For deserialization
304 public GroupV1() {
305 }
306
307 public GroupV1(
308 final String groupId,
309 final String expectedV2Id,
310 final String name,
311 final String color,
312 final int messageExpirationTime,
313 final boolean blocked,
314 final boolean archived,
315 final List<Member> members
316 ) {
317 this.groupId = groupId;
318 this.expectedV2Id = expectedV2Id;
319 this.name = name;
320 this.color = color;
321 this.messageExpirationTime = messageExpirationTime;
322 this.blocked = blocked;
323 this.archived = archived;
324 this.members = members;
325 }
326
327 private static final class Member {
328
329 public Long recipientId;
330
331 public String uuid;
332
333 public String number;
334
335 Member(Long recipientId, final String uuid, final String number) {
336 this.recipientId = recipientId;
337 this.uuid = uuid;
338 this.number = number;
339 }
340 }
341
342 private static final class JsonSignalServiceAddress {
343
344 public String uuid;
345
346 public String number;
347
348 // For deserialization
349 public JsonSignalServiceAddress() {
350 }
351
352 JsonSignalServiceAddress(final String uuid, final String number) {
353 this.uuid = uuid;
354 this.number = number;
355 }
356 }
357
358 private static class MembersSerializer extends JsonSerializer<List<Member>> {
359
360 @Override
361 public void serialize(
362 final List<Member> value, final JsonGenerator jgen, final SerializerProvider provider
363 ) throws IOException {
364 jgen.writeStartArray(value.size());
365 for (var address : value) {
366 if (address.recipientId != null) {
367 jgen.writeNumber(address.recipientId);
368 } else if (address.uuid != null) {
369 jgen.writeObject(new JsonSignalServiceAddress(address.uuid, address.number));
370 } else {
371 jgen.writeString(address.number);
372 }
373 }
374 jgen.writeEndArray();
375 }
376 }
377
378 private static class MembersDeserializer extends JsonDeserializer<List<Member>> {
379
380 @Override
381 public List<Member> deserialize(
382 JsonParser jsonParser, DeserializationContext deserializationContext
383 ) throws IOException {
384 var addresses = new ArrayList<Member>();
385 JsonNode node = jsonParser.getCodec().readTree(jsonParser);
386 for (var n : node) {
387 if (n.isTextual()) {
388 addresses.add(new Member(null, null, n.textValue()));
389 } else if (n.isNumber()) {
390 addresses.add(new Member(n.numberValue().longValue(), null, null));
391 } else {
392 var address = jsonParser.getCodec().treeToValue(n, JsonSignalServiceAddress.class);
393 addresses.add(new Member(null, address.uuid, address.number));
394 }
395 }
396
397 return addresses;
398 }
399 }
400 }
401
402 private static class GroupV2 extends Group {
403
404 public String groupId;
405 public String masterKey;
406 public boolean blocked;
407
408 // For deserialization
409 private GroupV2() {
410 }
411
412 public GroupV2(final String groupId, final String masterKey, final boolean blocked) {
413 this.groupId = groupId;
414 this.masterKey = masterKey;
415 this.blocked = blocked;
416 }
417 }
418
419 }
420
421 // private static class GroupsSerializer extends JsonSerializer<List<Storage.Group>> {
422 //
423 // @Override
424 // public void serialize(
425 // final List<Storage.Group> groups, final JsonGenerator jgen, final SerializerProvider provider
426 // ) throws IOException {
427 // jgen.writeStartArray(groups.size());
428 // for (var group : groups) {
429 // if (group instanceof GroupInfoV1) {
430 // jgen.writeObject(group);
431 // } else if (group instanceof GroupInfoV2) {
432 // final var groupV2 = (GroupInfoV2) group;
433 // jgen.writeStartObject();
434 // jgen.writeStringField("groupId", groupV2.getGroupId().toBase64());
435 // jgen.writeStringField("masterKey",
436 // Base64.getEncoder().encodeToString(groupV2.getMasterKey().serialize()));
437 // jgen.writeBooleanField("blocked", groupV2.isBlocked());
438 // jgen.writeEndObject();
439 // } else {
440 // throw new AssertionError("Unknown group version");
441 // }
442 // }
443 // jgen.writeEndArray();
444 // }
445 // }
446 //
447 private static class GroupsDeserializer extends JsonDeserializer<List<Storage.Group>> {
448
449 @Override
450 public List<Storage.Group> deserialize(
451 JsonParser jsonParser, DeserializationContext deserializationContext
452 ) throws IOException {
453 var groups = new ArrayList<Storage.Group>();
454 JsonNode node = jsonParser.getCodec().readTree(jsonParser);
455 for (var n : node) {
456 Storage.Group g;
457 if (n.hasNonNull("masterKey")) {
458 // a v2 group
459 g = jsonParser.getCodec().treeToValue(n, Storage.GroupV2.class);
460 } else {
461 g = jsonParser.getCodec().treeToValue(n, Storage.GroupV1.class);
462 }
463 groups.add(g);
464 }
465
466 return groups;
467 }
468 }
469
470 public interface Saver {
471
472 void save(Storage storage);
473 }
474 }