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