]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java
4adc413ad07a32fbb0b4bf9d4daeccb6eedbd580
[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.RecipientAddress;
18 import org.asamk.signal.manager.storage.recipients.RecipientId;
19 import org.asamk.signal.manager.storage.recipients.RecipientResolver;
20 import org.asamk.signal.manager.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.slf4j.Logger;
25 import org.slf4j.LoggerFactory;
26 import org.whispersystems.signalservice.api.util.UuidUtil;
27 import org.whispersystems.signalservice.internal.util.Hex;
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 RecipientAddress(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 groupIdV1) {
137 deleteGroup(groupIdV1);
138 }
139
140 public void deleteGroup(GroupId groupId) {
141 final Storage storage;
142 synchronized (groups) {
143 groups.remove(groupId);
144 storage = toStorageLocked();
145 }
146 saver.save(storage);
147 }
148
149 public GroupInfo getGroup(GroupId groupId) {
150 synchronized (groups) {
151 return getGroupLocked(groupId);
152 }
153 }
154
155 public GroupInfoV1 getOrCreateGroupV1(GroupIdV1 groupId) {
156 synchronized (groups) {
157 var group = getGroupLocked(groupId);
158 if (group instanceof GroupInfoV1) {
159 return (GroupInfoV1) group;
160 }
161
162 if (group == null) {
163 return new GroupInfoV1(groupId);
164 }
165
166 return null;
167 }
168 }
169
170 public List<GroupInfo> getGroups() {
171 synchronized (groups) {
172 final var groups = this.groups.values();
173 for (var group : groups) {
174 loadDecryptedGroupLocked(group);
175 }
176 return new ArrayList<>(groups);
177 }
178 }
179
180 public void mergeRecipients(final RecipientId recipientId, final RecipientId toBeMergedRecipientId) {
181 synchronized (groups) {
182 var modified = false;
183 for (var group : this.groups.values()) {
184 if (group instanceof GroupInfoV1) {
185 var groupV1 = (GroupInfoV1) group;
186 if (groupV1.isMember(toBeMergedRecipientId)) {
187 groupV1.removeMember(toBeMergedRecipientId);
188 groupV1.addMembers(List.of(recipientId));
189 modified = true;
190 }
191 }
192 }
193 if (modified) {
194 saver.save(toStorageLocked());
195 }
196 }
197 }
198
199 private GroupInfo getGroupLocked(final GroupId groupId) {
200 var group = groups.get(groupId);
201 if (group == null) {
202 if (groupId instanceof GroupIdV1) {
203 group = getGroupByV1IdLocked((GroupIdV1) groupId);
204 } else if (groupId instanceof GroupIdV2) {
205 group = getGroupV1ByV2IdLocked((GroupIdV2) groupId);
206 }
207 }
208 loadDecryptedGroupLocked(group);
209 return group;
210 }
211
212 private GroupInfo getGroupByV1IdLocked(final GroupIdV1 groupId) {
213 return groups.get(GroupUtils.getGroupIdV2(groupId));
214 }
215
216 private GroupInfoV1 getGroupV1ByV2IdLocked(GroupIdV2 groupIdV2) {
217 for (var g : groups.values()) {
218 if (g instanceof GroupInfoV1) {
219 final var gv1 = (GroupInfoV1) g;
220 if (groupIdV2.equals(gv1.getExpectedV2Id())) {
221 return gv1;
222 }
223 }
224 }
225 return null;
226 }
227
228 private void loadDecryptedGroupLocked(final GroupInfo group) {
229 if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) {
230 var groupFile = getGroupV2File(group.getGroupId());
231 if (!groupFile.exists()) {
232 groupFile = getGroupV2FileLegacy(group.getGroupId());
233 }
234 if (!groupFile.exists()) {
235 return;
236 }
237 try (var stream = new FileInputStream(groupFile)) {
238 ((GroupInfoV2) group).setGroup(DecryptedGroup.parseFrom(stream), recipientResolver);
239 } catch (IOException ignored) {
240 }
241 }
242 }
243
244 private File getGroupV2FileLegacy(final GroupId groupId) {
245 return new File(groupCachePath, Hex.toStringCondensed(groupId.serialize()));
246 }
247
248 private File getGroupV2File(final GroupId groupId) {
249 return new File(groupCachePath, groupId.toBase64().replace("/", "_"));
250 }
251
252 private Storage toStorageLocked() {
253 return new Storage(groups.values().stream().map(g -> {
254 if (g instanceof GroupInfoV1) {
255 final var g1 = (GroupInfoV1) g;
256 return new Storage.GroupV1(g1.getGroupId().toBase64(),
257 g1.getExpectedV2Id().toBase64(),
258 g1.name,
259 g1.color,
260 g1.messageExpirationTime,
261 g1.blocked,
262 g1.archived,
263 g1.members.stream()
264 .map(m -> new Storage.GroupV1.Member(m.getId(), null, null))
265 .collect(Collectors.toList()));
266 }
267
268 final var g2 = (GroupInfoV2) g;
269 return new Storage.GroupV2(g2.getGroupId().toBase64(),
270 Base64.getEncoder().encodeToString(g2.getMasterKey().serialize()),
271 g2.isBlocked());
272 }).collect(Collectors.toList()));
273 }
274
275 public static class Storage {
276
277 // @JsonSerialize(using = GroupsSerializer.class)
278 @JsonDeserialize(using = GroupsDeserializer.class)
279 public List<Storage.Group> groups;
280
281 // For deserialization
282 public Storage() {
283 }
284
285 public Storage(final List<Storage.Group> groups) {
286 this.groups = groups;
287 }
288
289 private abstract static class Group {
290
291 }
292
293 private static class GroupV1 extends Group {
294
295 public String groupId;
296 public String expectedV2Id;
297 public String name;
298 public String color;
299 public int messageExpirationTime;
300 public boolean blocked;
301 public boolean archived;
302
303 @JsonDeserialize(using = MembersDeserializer.class)
304 @JsonSerialize(using = MembersSerializer.class)
305 public List<Member> members;
306
307 // For deserialization
308 public GroupV1() {
309 }
310
311 public GroupV1(
312 final String groupId,
313 final String expectedV2Id,
314 final String name,
315 final String color,
316 final int messageExpirationTime,
317 final boolean blocked,
318 final boolean archived,
319 final List<Member> members
320 ) {
321 this.groupId = groupId;
322 this.expectedV2Id = expectedV2Id;
323 this.name = name;
324 this.color = color;
325 this.messageExpirationTime = messageExpirationTime;
326 this.blocked = blocked;
327 this.archived = archived;
328 this.members = members;
329 }
330
331 private static final class Member {
332
333 public Long recipientId;
334
335 public String uuid;
336
337 public String number;
338
339 Member(Long recipientId, final String uuid, final String number) {
340 this.recipientId = recipientId;
341 this.uuid = uuid;
342 this.number = number;
343 }
344 }
345
346 private static final class JsonRecipientAddress {
347
348 public String uuid;
349
350 public String number;
351
352 // For deserialization
353 public JsonRecipientAddress() {
354 }
355
356 JsonRecipientAddress(final String uuid, final String number) {
357 this.uuid = uuid;
358 this.number = number;
359 }
360 }
361
362 private static class MembersSerializer extends JsonSerializer<List<Member>> {
363
364 @Override
365 public void serialize(
366 final List<Member> value, final JsonGenerator jgen, final SerializerProvider provider
367 ) throws IOException {
368 jgen.writeStartArray(value.size());
369 for (var address : value) {
370 if (address.recipientId != null) {
371 jgen.writeNumber(address.recipientId);
372 } else if (address.uuid != null) {
373 jgen.writeObject(new JsonRecipientAddress(address.uuid, address.number));
374 } else {
375 jgen.writeString(address.number);
376 }
377 }
378 jgen.writeEndArray();
379 }
380 }
381
382 private static class MembersDeserializer extends JsonDeserializer<List<Member>> {
383
384 @Override
385 public List<Member> deserialize(
386 JsonParser jsonParser, DeserializationContext deserializationContext
387 ) throws IOException {
388 var addresses = new ArrayList<Member>();
389 JsonNode node = jsonParser.getCodec().readTree(jsonParser);
390 for (var n : node) {
391 if (n.isTextual()) {
392 addresses.add(new Member(null, null, n.textValue()));
393 } else if (n.isNumber()) {
394 addresses.add(new Member(n.numberValue().longValue(), null, null));
395 } else {
396 var address = jsonParser.getCodec().treeToValue(n, JsonRecipientAddress.class);
397 addresses.add(new Member(null, address.uuid, address.number));
398 }
399 }
400
401 return addresses;
402 }
403 }
404 }
405
406 private static class GroupV2 extends Group {
407
408 public String groupId;
409 public String masterKey;
410 public boolean blocked;
411
412 // For deserialization
413 private GroupV2() {
414 }
415
416 public GroupV2(final String groupId, final String masterKey, final boolean blocked) {
417 this.groupId = groupId;
418 this.masterKey = masterKey;
419 this.blocked = blocked;
420 }
421 }
422
423 }
424
425 // private static class GroupsSerializer extends JsonSerializer<List<Storage.Group>> {
426 //
427 // @Override
428 // public void serialize(
429 // final List<Storage.Group> groups, final JsonGenerator jgen, final SerializerProvider provider
430 // ) throws IOException {
431 // jgen.writeStartArray(groups.size());
432 // for (var group : groups) {
433 // if (group instanceof GroupInfoV1) {
434 // jgen.writeObject(group);
435 // } else if (group instanceof GroupInfoV2) {
436 // final var groupV2 = (GroupInfoV2) group;
437 // jgen.writeStartObject();
438 // jgen.writeStringField("groupId", groupV2.getGroupId().toBase64());
439 // jgen.writeStringField("masterKey",
440 // Base64.getEncoder().encodeToString(groupV2.getMasterKey().serialize()));
441 // jgen.writeBooleanField("blocked", groupV2.isBlocked());
442 // jgen.writeEndObject();
443 // } else {
444 // throw new AssertionError("Unknown group version");
445 // }
446 // }
447 // jgen.writeEndArray();
448 // }
449 // }
450 //
451 private static class GroupsDeserializer extends JsonDeserializer<List<Storage.Group>> {
452
453 @Override
454 public List<Storage.Group> deserialize(
455 JsonParser jsonParser, DeserializationContext deserializationContext
456 ) throws IOException {
457 var groups = new ArrayList<Storage.Group>();
458 JsonNode node = jsonParser.getCodec().readTree(jsonParser);
459 for (var n : node) {
460 Storage.Group g;
461 if (n.hasNonNull("masterKey")) {
462 // a v2 group
463 g = jsonParser.getCodec().treeToValue(n, Storage.GroupV2.class);
464 } else {
465 g = jsonParser.getCodec().treeToValue(n, Storage.GroupV1.class);
466 }
467 groups.add(g);
468 }
469
470 return groups;
471 }
472 }
473
474 public interface Saver {
475
476 void save(Storage storage);
477 }
478 }