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