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