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