]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java
6dd327e85e5fdeb03e25723a5cc773881287e39f
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / storage / recipients / RecipientStore.java
1 package org.asamk.signal.manager.storage.recipients;
2
3 import com.fasterxml.jackson.databind.ObjectMapper;
4
5 import org.asamk.signal.manager.api.Pair;
6 import org.asamk.signal.manager.api.UnregisteredRecipientException;
7 import org.asamk.signal.manager.storage.Utils;
8 import org.asamk.signal.manager.storage.contacts.ContactsStore;
9 import org.asamk.signal.manager.storage.profiles.ProfileStore;
10 import org.signal.zkgroup.InvalidInputException;
11 import org.signal.zkgroup.profiles.ProfileKey;
12 import org.signal.zkgroup.profiles.ProfileKeyCredential;
13 import org.slf4j.Logger;
14 import org.slf4j.LoggerFactory;
15 import org.whispersystems.signalservice.api.push.ACI;
16 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
17 import org.whispersystems.signalservice.api.util.UuidUtil;
18
19 import java.io.ByteArrayInputStream;
20 import java.io.ByteArrayOutputStream;
21 import java.io.File;
22 import java.io.FileInputStream;
23 import java.io.FileNotFoundException;
24 import java.io.FileOutputStream;
25 import java.io.IOException;
26 import java.util.ArrayList;
27 import java.util.Base64;
28 import java.util.HashMap;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Objects;
32 import java.util.Optional;
33 import java.util.Set;
34 import java.util.UUID;
35 import java.util.function.Supplier;
36 import java.util.stream.Collectors;
37
38 public class RecipientStore implements RecipientResolver, ContactsStore, ProfileStore {
39
40 private final static Logger logger = LoggerFactory.getLogger(RecipientStore.class);
41
42 private final ObjectMapper objectMapper;
43 private final File file;
44 private final RecipientMergeHandler recipientMergeHandler;
45
46 private final Map<RecipientId, Recipient> recipients;
47 private final Map<Long, Long> recipientsMerged = new HashMap<>();
48
49 private long lastId;
50 private boolean isBulkUpdating;
51
52 public static RecipientStore load(File file, RecipientMergeHandler recipientMergeHandler) throws IOException {
53 final var objectMapper = Utils.createStorageObjectMapper();
54 try (var inputStream = new FileInputStream(file)) {
55 final var storage = objectMapper.readValue(inputStream, Storage.class);
56
57 final var recipientStore = new RecipientStore(objectMapper,
58 file,
59 recipientMergeHandler,
60 new HashMap<>(),
61 storage.lastId);
62 final var recipients = storage.recipients.stream().map(r -> {
63 final var recipientId = new RecipientId(r.id, recipientStore);
64 final var address = new RecipientAddress(Optional.ofNullable(r.uuid).map(UuidUtil::parseOrThrow),
65 Optional.ofNullable(r.number));
66
67 Contact contact = null;
68 if (r.contact != null) {
69 contact = new Contact(r.contact.name,
70 r.contact.color,
71 r.contact.messageExpirationTime,
72 r.contact.blocked,
73 r.contact.archived);
74 }
75
76 ProfileKey profileKey = null;
77 if (r.profileKey != null) {
78 try {
79 profileKey = new ProfileKey(Base64.getDecoder().decode(r.profileKey));
80 } catch (InvalidInputException ignored) {
81 }
82 }
83
84 ProfileKeyCredential profileKeyCredential = null;
85 if (r.profileKeyCredential != null) {
86 try {
87 profileKeyCredential = new ProfileKeyCredential(Base64.getDecoder()
88 .decode(r.profileKeyCredential));
89 } catch (Throwable ignored) {
90 }
91 }
92
93 Profile profile = null;
94 if (r.profile != null) {
95 profile = new Profile(r.profile.lastUpdateTimestamp,
96 r.profile.givenName,
97 r.profile.familyName,
98 r.profile.about,
99 r.profile.aboutEmoji,
100 r.profile.avatarUrlPath,
101 Profile.UnidentifiedAccessMode.valueOfOrUnknown(r.profile.unidentifiedAccessMode),
102 r.profile.capabilities.stream()
103 .map(Profile.Capability::valueOfOrNull)
104 .filter(Objects::nonNull)
105 .collect(Collectors.toSet()));
106 }
107
108 return new Recipient(recipientId, address, contact, profileKey, profileKeyCredential, profile);
109 }).collect(Collectors.toMap(Recipient::getRecipientId, r -> r));
110
111 recipientStore.addRecipients(recipients);
112
113 return recipientStore;
114 } catch (FileNotFoundException e) {
115 logger.trace("Creating new recipient store.");
116 return new RecipientStore(objectMapper, file, recipientMergeHandler, new HashMap<>(), 0);
117 }
118 }
119
120 private RecipientStore(
121 final ObjectMapper objectMapper,
122 final File file,
123 final RecipientMergeHandler recipientMergeHandler,
124 final Map<RecipientId, Recipient> recipients,
125 final long lastId
126 ) {
127 this.objectMapper = objectMapper;
128 this.file = file;
129 this.recipientMergeHandler = recipientMergeHandler;
130 this.recipients = recipients;
131 this.lastId = lastId;
132 }
133
134 public boolean isBulkUpdating() {
135 return isBulkUpdating;
136 }
137
138 public void setBulkUpdating(final boolean bulkUpdating) {
139 isBulkUpdating = bulkUpdating;
140 if (!bulkUpdating) {
141 synchronized (recipients) {
142 saveLocked();
143 }
144 }
145 }
146
147 public RecipientAddress resolveRecipientAddress(RecipientId recipientId) {
148 synchronized (recipients) {
149 return getRecipient(recipientId).getAddress();
150 }
151 }
152
153 public Recipient getRecipient(RecipientId recipientId) {
154 synchronized (recipients) {
155 return recipients.get(recipientId);
156 }
157 }
158
159 @Override
160 public RecipientId resolveRecipient(ACI aci) {
161 return resolveRecipient(new RecipientAddress(aci.uuid()), false);
162 }
163
164 @Override
165 public RecipientId resolveRecipient(final long recipientId) {
166 final var recipient = getRecipient(new RecipientId(recipientId, this));
167 return recipient == null ? null : recipient.getRecipientId();
168 }
169
170 @Override
171 public RecipientId resolveRecipient(final String identifier) {
172 return resolveRecipient(Utils.getRecipientAddressFromIdentifier(identifier), false);
173 }
174
175 public RecipientId resolveRecipient(
176 final String number, Supplier<ACI> aciSupplier
177 ) throws UnregisteredRecipientException {
178 final Optional<Recipient> byNumber;
179 synchronized (recipients) {
180 byNumber = findByNumberLocked(number);
181 }
182 if (byNumber.isEmpty() || byNumber.get().getAddress().uuid().isEmpty()) {
183 final var aci = aciSupplier.get();
184 if (aci == null) {
185 throw new UnregisteredRecipientException(new RecipientAddress(null, number));
186 }
187
188 return resolveRecipient(new RecipientAddress(aci.uuid(), number), false);
189 }
190 return byNumber.get().getRecipientId();
191 }
192
193 public RecipientId resolveRecipient(RecipientAddress address) {
194 return resolveRecipient(address, false);
195 }
196
197 @Override
198 public RecipientId resolveRecipient(final SignalServiceAddress address) {
199 return resolveRecipient(new RecipientAddress(address), false);
200 }
201
202 public RecipientId resolveRecipientTrusted(RecipientAddress address) {
203 return resolveRecipient(address, true);
204 }
205
206 public RecipientId resolveRecipientTrusted(SignalServiceAddress address) {
207 return resolveRecipient(new RecipientAddress(address), true);
208 }
209
210 public List<RecipientId> resolveRecipientsTrusted(List<RecipientAddress> addresses) {
211 final List<RecipientId> recipientIds;
212 final List<Pair<RecipientId, RecipientId>> toBeMerged = new ArrayList<>();
213 synchronized (recipients) {
214 recipientIds = addresses.stream().map(address -> {
215 final var pair = resolveRecipientLocked(address, true);
216 if (pair.second().isPresent()) {
217 toBeMerged.add(new Pair<>(pair.first(), pair.second().get()));
218 }
219 return pair.first();
220 }).toList();
221 }
222 for (var pair : toBeMerged) {
223 recipientMergeHandler.mergeRecipients(pair.first(), pair.second());
224 }
225 return recipientIds;
226 }
227
228 @Override
229 public void storeContact(RecipientId recipientId, final Contact contact) {
230 synchronized (recipients) {
231 final var recipient = recipients.get(recipientId);
232 storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withContact(contact).build());
233 }
234 }
235
236 @Override
237 public Contact getContact(RecipientId recipientId) {
238 final var recipient = getRecipient(recipientId);
239 return recipient == null ? null : recipient.getContact();
240 }
241
242 @Override
243 public List<Pair<RecipientId, Contact>> getContacts() {
244 return recipients.entrySet()
245 .stream()
246 .filter(e -> e.getValue().getContact() != null)
247 .map(e -> new Pair<>(e.getKey(), e.getValue().getContact()))
248 .toList();
249 }
250
251 @Override
252 public void deleteContact(RecipientId recipientId) {
253 synchronized (recipients) {
254 final var recipient = recipients.get(recipientId);
255 storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withContact(null).build());
256 }
257 }
258
259 public void deleteRecipientData(RecipientId recipientId) {
260 synchronized (recipients) {
261 logger.debug("Deleting recipient data for {}", recipientId);
262 final var recipient = recipients.get(recipientId);
263 storeRecipientLocked(recipientId,
264 Recipient.newBuilder()
265 .withRecipientId(recipientId)
266 .withAddress(new RecipientAddress(recipient.getAddress().uuid().orElse(null)))
267 .build());
268 }
269 }
270
271 @Override
272 public Profile getProfile(final RecipientId recipientId) {
273 final var recipient = getRecipient(recipientId);
274 return recipient == null ? null : recipient.getProfile();
275 }
276
277 @Override
278 public ProfileKey getProfileKey(final RecipientId recipientId) {
279 final var recipient = getRecipient(recipientId);
280 return recipient == null ? null : recipient.getProfileKey();
281 }
282
283 @Override
284 public ProfileKeyCredential getProfileKeyCredential(final RecipientId recipientId) {
285 final var recipient = getRecipient(recipientId);
286 return recipient == null ? null : recipient.getProfileKeyCredential();
287 }
288
289 @Override
290 public void storeProfile(RecipientId recipientId, final Profile profile) {
291 synchronized (recipients) {
292 final var recipient = recipients.get(recipientId);
293 storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withProfile(profile).build());
294 }
295 }
296
297 @Override
298 public void storeProfileKey(RecipientId recipientId, final ProfileKey profileKey) {
299 synchronized (recipients) {
300 final var recipient = recipients.get(recipientId);
301 if (profileKey != null && profileKey.equals(recipient.getProfileKey())) {
302 return;
303 }
304
305 final var newRecipient = Recipient.newBuilder(recipient)
306 .withProfileKey(profileKey)
307 .withProfileKeyCredential(null)
308 .withProfile(recipient.getProfile() == null
309 ? null
310 : Profile.newBuilder(recipient.getProfile()).withLastUpdateTimestamp(0).build())
311 .build();
312 storeRecipientLocked(recipientId, newRecipient);
313 }
314 }
315
316 @Override
317 public void storeProfileKeyCredential(RecipientId recipientId, final ProfileKeyCredential profileKeyCredential) {
318 synchronized (recipients) {
319 final var recipient = recipients.get(recipientId);
320 storeRecipientLocked(recipientId,
321 Recipient.newBuilder(recipient).withProfileKeyCredential(profileKeyCredential).build());
322 }
323 }
324
325 public boolean isEmpty() {
326 synchronized (recipients) {
327 return recipients.isEmpty();
328 }
329 }
330
331 private void addRecipients(final Map<RecipientId, Recipient> recipients) {
332 this.recipients.putAll(recipients);
333 }
334
335 /**
336 * @param isHighTrust true, if the number/uuid connection was obtained from a trusted source.
337 * Has no effect, if the address contains only a number or a uuid.
338 */
339 private RecipientId resolveRecipient(RecipientAddress address, boolean isHighTrust) {
340 final Pair<RecipientId, Optional<RecipientId>> pair;
341 synchronized (recipients) {
342 pair = resolveRecipientLocked(address, isHighTrust);
343 }
344
345 if (pair.second().isPresent()) {
346 recipientMergeHandler.mergeRecipients(pair.first(), pair.second().get());
347 }
348 return pair.first();
349 }
350
351 private Pair<RecipientId, Optional<RecipientId>> resolveRecipientLocked(
352 RecipientAddress address, boolean isHighTrust
353 ) {
354 final var byNumber = address.number().isEmpty()
355 ? Optional.<Recipient>empty()
356 : findByNumberLocked(address.number().get());
357 final var byUuid = address.uuid().isEmpty()
358 ? Optional.<Recipient>empty()
359 : findByUuidLocked(address.uuid().get());
360
361 if (byNumber.isEmpty() && byUuid.isEmpty()) {
362 logger.debug("Got new recipient, both uuid and number are unknown");
363
364 if (isHighTrust || address.uuid().isEmpty() || address.number().isEmpty()) {
365 return new Pair<>(addNewRecipientLocked(address), Optional.empty());
366 }
367
368 return new Pair<>(addNewRecipientLocked(new RecipientAddress(address.uuid().get())), Optional.empty());
369 }
370
371 if (!isHighTrust || address.uuid().isEmpty() || address.number().isEmpty() || byNumber.equals(byUuid)) {
372 return new Pair<>(byUuid.or(() -> byNumber).map(Recipient::getRecipientId).get(), Optional.empty());
373 }
374
375 if (byNumber.isEmpty()) {
376 logger.debug("Got recipient {} existing with uuid, updating with high trust number",
377 byUuid.get().getRecipientId());
378 updateRecipientAddressLocked(byUuid.get().getRecipientId(), address);
379 return new Pair<>(byUuid.get().getRecipientId(), Optional.empty());
380 }
381
382 final var byNumberRecipient = byNumber.get();
383
384 if (byUuid.isEmpty()) {
385 if (byNumberRecipient.getAddress().uuid().isPresent()) {
386 logger.debug(
387 "Got recipient {} existing with number, but different uuid, so stripping its number and adding new recipient",
388 byNumberRecipient.getRecipientId());
389
390 updateRecipientAddressLocked(byNumberRecipient.getRecipientId(),
391 new RecipientAddress(byNumberRecipient.getAddress().uuid().get()));
392 return new Pair<>(addNewRecipientLocked(address), Optional.empty());
393 }
394
395 logger.debug("Got recipient {} existing with number and no uuid, updating with high trust uuid",
396 byNumberRecipient.getRecipientId());
397 updateRecipientAddressLocked(byNumberRecipient.getRecipientId(), address);
398 return new Pair<>(byNumberRecipient.getRecipientId(), Optional.empty());
399 }
400
401 final var byUuidRecipient = byUuid.get();
402
403 if (byNumberRecipient.getAddress().uuid().isPresent()) {
404 logger.debug(
405 "Got separate recipients for high trust number {} and uuid {}, recipient for number has different uuid, so stripping its number",
406 byNumberRecipient.getRecipientId(),
407 byUuidRecipient.getRecipientId());
408
409 updateRecipientAddressLocked(byNumberRecipient.getRecipientId(),
410 new RecipientAddress(byNumberRecipient.getAddress().uuid().get()));
411 updateRecipientAddressLocked(byUuidRecipient.getRecipientId(), address);
412 return new Pair<>(byUuidRecipient.getRecipientId(), Optional.empty());
413 }
414
415 logger.debug("Got separate recipients for high trust number {} and uuid {}, need to merge them",
416 byNumberRecipient.getRecipientId(),
417 byUuidRecipient.getRecipientId());
418 updateRecipientAddressLocked(byUuidRecipient.getRecipientId(), address);
419 // Create a fixed RecipientId that won't update its id after merge
420 final var toBeMergedRecipientId = new RecipientId(byNumberRecipient.getRecipientId().id(), null);
421 mergeRecipientsLocked(byUuidRecipient.getRecipientId(), toBeMergedRecipientId);
422 return new Pair<>(byUuidRecipient.getRecipientId(), Optional.of(toBeMergedRecipientId));
423 }
424
425 private RecipientId addNewRecipientLocked(final RecipientAddress address) {
426 final var nextRecipientId = nextIdLocked();
427 logger.debug("Adding new recipient {} with address {}", nextRecipientId, address);
428 storeRecipientLocked(nextRecipientId, new Recipient(nextRecipientId, address, null, null, null, null));
429 return nextRecipientId;
430 }
431
432 private void updateRecipientAddressLocked(RecipientId recipientId, final RecipientAddress address) {
433 final var recipient = recipients.get(recipientId);
434 storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withAddress(address).build());
435 }
436
437 long getActualRecipientId(long recipientId) {
438 while (recipientsMerged.containsKey(recipientId)) {
439 final var newRecipientId = recipientsMerged.get(recipientId);
440 logger.debug("Using {} instead of {}, because recipients have been merged", newRecipientId, recipientId);
441 recipientId = newRecipientId;
442 }
443 return recipientId;
444 }
445
446 private void storeRecipientLocked(final RecipientId recipientId, final Recipient recipient) {
447 final var existingRecipient = recipients.get(recipientId);
448 if (existingRecipient == null || !existingRecipient.equals(recipient)) {
449 recipients.put(recipientId, recipient);
450 saveLocked();
451 }
452 }
453
454 private void mergeRecipientsLocked(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
455 final var recipient = recipients.get(recipientId);
456 final var toBeMergedRecipient = recipients.get(toBeMergedRecipientId);
457 recipients.put(recipientId,
458 new Recipient(recipientId,
459 recipient.getAddress(),
460 recipient.getContact() != null ? recipient.getContact() : toBeMergedRecipient.getContact(),
461 recipient.getProfileKey() != null
462 ? recipient.getProfileKey()
463 : toBeMergedRecipient.getProfileKey(),
464 recipient.getProfileKeyCredential() != null
465 ? recipient.getProfileKeyCredential()
466 : toBeMergedRecipient.getProfileKeyCredential(),
467 recipient.getProfile() != null ? recipient.getProfile() : toBeMergedRecipient.getProfile()));
468 recipients.remove(toBeMergedRecipientId);
469 recipientsMerged.put(toBeMergedRecipientId.id(), recipientId.id());
470 saveLocked();
471 }
472
473 private Optional<Recipient> findByNumberLocked(final String number) {
474 return recipients.entrySet()
475 .stream()
476 .filter(entry -> entry.getValue().getAddress().number().isPresent() && number.equals(entry.getValue()
477 .getAddress()
478 .number()
479 .get()))
480 .findFirst()
481 .map(Map.Entry::getValue);
482 }
483
484 private Optional<Recipient> findByUuidLocked(final UUID uuid) {
485 return recipients.entrySet()
486 .stream()
487 .filter(entry -> entry.getValue().getAddress().uuid().isPresent() && uuid.equals(entry.getValue()
488 .getAddress()
489 .uuid()
490 .get()))
491 .findFirst()
492 .map(Map.Entry::getValue);
493 }
494
495 private RecipientId nextIdLocked() {
496 return new RecipientId(++this.lastId, this);
497 }
498
499 private void saveLocked() {
500 if (isBulkUpdating) {
501 return;
502 }
503 final var base64 = Base64.getEncoder();
504 var storage = new Storage(recipients.entrySet().stream().map(pair -> {
505 final var recipient = pair.getValue();
506 final var contact = recipient.getContact() == null
507 ? null
508 : new Storage.Recipient.Contact(recipient.getContact().getName(),
509 recipient.getContact().getColor(),
510 recipient.getContact().getMessageExpirationTime(),
511 recipient.getContact().isBlocked(),
512 recipient.getContact().isArchived());
513 final var profile = recipient.getProfile() == null
514 ? null
515 : new Storage.Recipient.Profile(recipient.getProfile().getLastUpdateTimestamp(),
516 recipient.getProfile().getGivenName(),
517 recipient.getProfile().getFamilyName(),
518 recipient.getProfile().getAbout(),
519 recipient.getProfile().getAboutEmoji(),
520 recipient.getProfile().getAvatarUrlPath(),
521 recipient.getProfile().getUnidentifiedAccessMode().name(),
522 recipient.getProfile()
523 .getCapabilities()
524 .stream()
525 .map(Enum::name)
526 .collect(Collectors.toSet()));
527 return new Storage.Recipient(pair.getKey().id(),
528 recipient.getAddress().number().orElse(null),
529 recipient.getAddress().uuid().map(UUID::toString).orElse(null),
530 recipient.getProfileKey() == null
531 ? null
532 : base64.encodeToString(recipient.getProfileKey().serialize()),
533 recipient.getProfileKeyCredential() == null
534 ? null
535 : base64.encodeToString(recipient.getProfileKeyCredential().serialize()),
536 contact,
537 profile);
538 }).toList(), lastId);
539
540 // Write to memory first to prevent corrupting the file in case of serialization errors
541 try (var inMemoryOutput = new ByteArrayOutputStream()) {
542 objectMapper.writeValue(inMemoryOutput, storage);
543
544 var input = new ByteArrayInputStream(inMemoryOutput.toByteArray());
545 try (var outputStream = new FileOutputStream(file)) {
546 input.transferTo(outputStream);
547 }
548 } catch (Exception e) {
549 logger.error("Error saving recipient store file: {}", e.getMessage());
550 }
551 }
552
553 private record Storage(List<Recipient> recipients, long lastId) {
554
555 private record Recipient(
556 long id,
557 String number,
558 String uuid,
559 String profileKey,
560 String profileKeyCredential,
561 Storage.Recipient.Contact contact,
562 Storage.Recipient.Profile profile
563 ) {
564
565 private record Contact(
566 String name, String color, int messageExpirationTime, boolean blocked, boolean archived
567 ) {}
568
569 private record Profile(
570 long lastUpdateTimestamp,
571 String givenName,
572 String familyName,
573 String about,
574 String aboutEmoji,
575 String avatarUrlPath,
576 String unidentifiedAccessMode,
577 Set<String> capabilities
578 ) {}
579 }
580 }
581
582 public interface RecipientMergeHandler {
583
584 void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId);
585 }
586 }