]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java
1fd0ae4c2e0949546c005d2e8046402857245e0d
[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 return;
306 }
307
308 final var newRecipient = Recipient.newBuilder(recipient)
309 .withProfileKey(profileKey)
310 .withProfileKeyCredential(null)
311 .withProfile(recipient.getProfile() == null
312 ? null
313 : Profile.newBuilder(recipient.getProfile()).withLastUpdateTimestamp(0).build())
314 .build();
315 storeRecipientLocked(recipientId, newRecipient);
316 }
317 }
318
319 @Override
320 public void storeProfileKeyCredential(RecipientId recipientId, final ProfileKeyCredential profileKeyCredential) {
321 synchronized (recipients) {
322 final var recipient = recipients.get(recipientId);
323 storeRecipientLocked(recipientId,
324 Recipient.newBuilder(recipient).withProfileKeyCredential(profileKeyCredential).build());
325 }
326 }
327
328 public boolean isEmpty() {
329 synchronized (recipients) {
330 return recipients.isEmpty();
331 }
332 }
333
334 private void addRecipients(final Map<RecipientId, Recipient> recipients) {
335 this.recipients.putAll(recipients);
336 }
337
338 /**
339 * @param isHighTrust true, if the number/uuid connection was obtained from a trusted source.
340 * Has no effect, if the address contains only a number or a uuid.
341 */
342 private RecipientId resolveRecipient(RecipientAddress address, boolean isHighTrust) {
343 final Pair<RecipientId, Optional<RecipientId>> pair;
344 synchronized (recipients) {
345 pair = resolveRecipientLocked(address, isHighTrust);
346 }
347
348 if (pair.second().isPresent()) {
349 recipientMergeHandler.mergeRecipients(pair.first(), pair.second().get());
350 }
351 return pair.first();
352 }
353
354 private Pair<RecipientId, Optional<RecipientId>> resolveRecipientLocked(
355 RecipientAddress address, boolean isHighTrust
356 ) {
357 final var byNumber = address.number().isEmpty()
358 ? Optional.<Recipient>empty()
359 : findByNumberLocked(address.number().get());
360 final var byUuid = address.uuid().isEmpty()
361 ? Optional.<Recipient>empty()
362 : findByUuidLocked(address.uuid().get());
363
364 if (byNumber.isEmpty() && byUuid.isEmpty()) {
365 logger.debug("Got new recipient, both uuid and number are unknown");
366
367 if (isHighTrust || address.uuid().isEmpty() || address.number().isEmpty()) {
368 return new Pair<>(addNewRecipientLocked(address), Optional.empty());
369 }
370
371 return new Pair<>(addNewRecipientLocked(new RecipientAddress(address.uuid().get())), Optional.empty());
372 }
373
374 if (!isHighTrust || address.uuid().isEmpty() || address.number().isEmpty() || byNumber.equals(byUuid)) {
375 return new Pair<>(byUuid.or(() -> byNumber).map(Recipient::getRecipientId).get(), Optional.empty());
376 }
377
378 if (byNumber.isEmpty()) {
379 logger.debug("Got recipient {} existing with uuid, updating with high trust number",
380 byUuid.get().getRecipientId());
381 updateRecipientAddressLocked(byUuid.get().getRecipientId(), address);
382 return new Pair<>(byUuid.get().getRecipientId(), Optional.empty());
383 }
384
385 final var byNumberRecipient = byNumber.get();
386
387 if (byUuid.isEmpty()) {
388 if (byNumberRecipient.getAddress().uuid().isPresent()) {
389 logger.debug(
390 "Got recipient {} existing with number, but different uuid, so stripping its number and adding new recipient",
391 byNumberRecipient.getRecipientId());
392
393 updateRecipientAddressLocked(byNumberRecipient.getRecipientId(),
394 new RecipientAddress(byNumberRecipient.getAddress().uuid().get()));
395 return new Pair<>(addNewRecipientLocked(address), Optional.empty());
396 }
397
398 logger.debug("Got recipient {} existing with number and no uuid, updating with high trust uuid",
399 byNumberRecipient.getRecipientId());
400 updateRecipientAddressLocked(byNumberRecipient.getRecipientId(), address);
401 return new Pair<>(byNumberRecipient.getRecipientId(), Optional.empty());
402 }
403
404 final var byUuidRecipient = byUuid.get();
405
406 if (byNumberRecipient.getAddress().uuid().isPresent()) {
407 logger.debug(
408 "Got separate recipients for high trust number {} and uuid {}, recipient for number has different uuid, so stripping its number",
409 byNumberRecipient.getRecipientId(),
410 byUuidRecipient.getRecipientId());
411
412 updateRecipientAddressLocked(byNumberRecipient.getRecipientId(),
413 new RecipientAddress(byNumberRecipient.getAddress().uuid().get()));
414 updateRecipientAddressLocked(byUuidRecipient.getRecipientId(), address);
415 return new Pair<>(byUuidRecipient.getRecipientId(), Optional.empty());
416 }
417
418 logger.debug("Got separate recipients for high trust number {} and uuid {}, need to merge them",
419 byNumberRecipient.getRecipientId(),
420 byUuidRecipient.getRecipientId());
421 updateRecipientAddressLocked(byUuidRecipient.getRecipientId(), address);
422 // Create a fixed RecipientId that won't update its id after merge
423 final var toBeMergedRecipientId = new RecipientId(byNumberRecipient.getRecipientId().id(), null);
424 mergeRecipientsLocked(byUuidRecipient.getRecipientId(), toBeMergedRecipientId);
425 return new Pair<>(byUuidRecipient.getRecipientId(), Optional.of(toBeMergedRecipientId));
426 }
427
428 private RecipientId addNewRecipientLocked(final RecipientAddress address) {
429 final var nextRecipientId = nextIdLocked();
430 logger.debug("Adding new recipient {} with address {}", nextRecipientId, address);
431 storeRecipientLocked(nextRecipientId, new Recipient(nextRecipientId, address, null, null, null, null));
432 return nextRecipientId;
433 }
434
435 private void updateRecipientAddressLocked(RecipientId recipientId, final RecipientAddress address) {
436 final var recipient = recipients.get(recipientId);
437 storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withAddress(address).build());
438 }
439
440 long getActualRecipientId(long recipientId) {
441 while (recipientsMerged.containsKey(recipientId)) {
442 final var newRecipientId = recipientsMerged.get(recipientId);
443 logger.debug("Using {} instead of {}, because recipients have been merged", newRecipientId, recipientId);
444 recipientId = newRecipientId;
445 }
446 return recipientId;
447 }
448
449 private void storeRecipientLocked(final RecipientId recipientId, final Recipient recipient) {
450 final var existingRecipient = recipients.get(recipientId);
451 if (existingRecipient == null || !existingRecipient.equals(recipient)) {
452 recipients.put(recipientId, recipient);
453 saveLocked();
454 }
455 }
456
457 private void mergeRecipientsLocked(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
458 final var recipient = recipients.get(recipientId);
459 final var toBeMergedRecipient = recipients.get(toBeMergedRecipientId);
460 recipients.put(recipientId,
461 new Recipient(recipientId,
462 recipient.getAddress(),
463 recipient.getContact() != null ? recipient.getContact() : toBeMergedRecipient.getContact(),
464 recipient.getProfileKey() != null
465 ? recipient.getProfileKey()
466 : toBeMergedRecipient.getProfileKey(),
467 recipient.getProfileKeyCredential() != null
468 ? recipient.getProfileKeyCredential()
469 : toBeMergedRecipient.getProfileKeyCredential(),
470 recipient.getProfile() != null ? recipient.getProfile() : toBeMergedRecipient.getProfile()));
471 recipients.remove(toBeMergedRecipientId);
472 recipientsMerged.put(toBeMergedRecipientId.id(), recipientId.id());
473 saveLocked();
474 }
475
476 private Optional<Recipient> findByNumberLocked(final String number) {
477 return recipients.entrySet()
478 .stream()
479 .filter(entry -> entry.getValue().getAddress().number().isPresent() && number.equals(entry.getValue()
480 .getAddress()
481 .number()
482 .get()))
483 .findFirst()
484 .map(Map.Entry::getValue);
485 }
486
487 private Optional<Recipient> findByUuidLocked(final UUID uuid) {
488 return recipients.entrySet()
489 .stream()
490 .filter(entry -> entry.getValue().getAddress().uuid().isPresent() && uuid.equals(entry.getValue()
491 .getAddress()
492 .uuid()
493 .get()))
494 .findFirst()
495 .map(Map.Entry::getValue);
496 }
497
498 private RecipientId nextIdLocked() {
499 return new RecipientId(++this.lastId, this);
500 }
501
502 private void saveLocked() {
503 if (isBulkUpdating) {
504 return;
505 }
506 final var base64 = Base64.getEncoder();
507 var storage = new Storage(recipients.entrySet().stream().map(pair -> {
508 final var recipient = pair.getValue();
509 final var contact = recipient.getContact() == null
510 ? null
511 : new Storage.Recipient.Contact(recipient.getContact().getName(),
512 recipient.getContact().getColor(),
513 recipient.getContact().getMessageExpirationTime(),
514 recipient.getContact().isBlocked(),
515 recipient.getContact().isArchived());
516 final var profile = recipient.getProfile() == null
517 ? null
518 : new Storage.Recipient.Profile(recipient.getProfile().getLastUpdateTimestamp(),
519 recipient.getProfile().getGivenName(),
520 recipient.getProfile().getFamilyName(),
521 recipient.getProfile().getAbout(),
522 recipient.getProfile().getAboutEmoji(),
523 recipient.getProfile().getAvatarUrlPath(),
524 recipient.getProfile().getUnidentifiedAccessMode().name(),
525 recipient.getProfile()
526 .getCapabilities()
527 .stream()
528 .map(Enum::name)
529 .collect(Collectors.toSet()));
530 return new Storage.Recipient(pair.getKey().id(),
531 recipient.getAddress().number().orElse(null),
532 recipient.getAddress().uuid().map(UUID::toString).orElse(null),
533 recipient.getProfileKey() == null
534 ? null
535 : base64.encodeToString(recipient.getProfileKey().serialize()),
536 recipient.getProfileKeyCredential() == null
537 ? null
538 : base64.encodeToString(recipient.getProfileKeyCredential().serialize()),
539 contact,
540 profile);
541 }).toList(), lastId);
542
543 // Write to memory first to prevent corrupting the file in case of serialization errors
544 try (var inMemoryOutput = new ByteArrayOutputStream()) {
545 objectMapper.writeValue(inMemoryOutput, storage);
546
547 var input = new ByteArrayInputStream(inMemoryOutput.toByteArray());
548 try (var outputStream = new FileOutputStream(file)) {
549 input.transferTo(outputStream);
550 }
551 } catch (Exception e) {
552 logger.error("Error saving recipient store file: {}", e.getMessage());
553 }
554 }
555
556 private record Storage(List<Recipient> recipients, long lastId) {
557
558 private record Recipient(
559 long id,
560 String number,
561 String uuid,
562 String profileKey,
563 String profileKeyCredential,
564 Storage.Recipient.Contact contact,
565 Storage.Recipient.Profile profile
566 ) {
567
568 private record Contact(
569 String name, String color, int messageExpirationTime, boolean blocked, boolean archived
570 ) {}
571
572 private record Profile(
573 long lastUpdateTimestamp,
574 String givenName,
575 String familyName,
576 String about,
577 String aboutEmoji,
578 String avatarUrlPath,
579 String unidentifiedAccessMode,
580 Set<String> capabilities
581 ) {}
582 }
583 }
584
585 public interface RecipientMergeHandler {
586
587 void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId);
588 }
589 }