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