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