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