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