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