]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java
c8a1134005792155b7f85f00cd6fd43bc046e55f
[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 != null && profileKey.equals(recipient.getProfileKey())) {
230 return;
231 }
232
233 final var newRecipient = Recipient.newBuilder(recipient)
234 .withProfileKey(profileKey)
235 .withProfileKeyCredential(null)
236 .withProfile(recipient.getProfile() == null
237 ? null
238 : Profile.newBuilder(recipient.getProfile()).withLastUpdateTimestamp(0).build())
239 .build();
240 storeRecipientLocked(recipientId, newRecipient);
241 }
242 }
243
244 @Override
245 public void storeProfileKeyCredential(
246 final RecipientId recipientId, final ProfileKeyCredential profileKeyCredential
247 ) {
248 synchronized (recipients) {
249 final var recipient = recipients.get(recipientId);
250 storeRecipientLocked(recipientId,
251 Recipient.newBuilder(recipient).withProfileKeyCredential(profileKeyCredential).build());
252 }
253 }
254
255 public boolean isEmpty() {
256 synchronized (recipients) {
257 return recipients.isEmpty();
258 }
259 }
260
261 /**
262 * @param isHighTrust true, if the number/uuid connection was obtained from a trusted source.
263 * Has no effect, if the address contains only a number or a uuid.
264 */
265 private RecipientId resolveRecipient(SignalServiceAddress address, boolean isHighTrust) {
266 final Pair<RecipientId, Optional<RecipientId>> pair;
267 synchronized (recipients) {
268 pair = resolveRecipientLocked(address, isHighTrust);
269 if (pair.second().isPresent()) {
270 recipientsMerged.put(pair.second().get(), pair.first());
271 }
272 }
273
274 if (pair.second().isPresent()) {
275 recipientMergeHandler.mergeRecipients(pair.first(), pair.second().get());
276 }
277 return pair.first();
278 }
279
280 private Pair<RecipientId, Optional<RecipientId>> resolveRecipientLocked(
281 SignalServiceAddress address, boolean isHighTrust
282 ) {
283 final var byNumber = !address.getNumber().isPresent()
284 ? Optional.<Recipient>empty()
285 : findByNameLocked(address.getNumber().get());
286 final var byUuid = !address.getUuid().isPresent()
287 ? Optional.<Recipient>empty()
288 : findByUuidLocked(address.getUuid().get());
289
290 if (byNumber.isEmpty() && byUuid.isEmpty()) {
291 logger.debug("Got new recipient, both uuid and number are unknown");
292
293 if (isHighTrust || !address.getUuid().isPresent() || !address.getNumber().isPresent()) {
294 return new Pair<>(addNewRecipientLocked(address), Optional.empty());
295 }
296
297 return new Pair<>(addNewRecipientLocked(new SignalServiceAddress(address.getUuid().get(), null)),
298 Optional.empty());
299 }
300
301 if (!isHighTrust
302 || !address.getUuid().isPresent()
303 || !address.getNumber().isPresent()
304 || byNumber.equals(byUuid)) {
305 return new Pair<>(byUuid.or(() -> byNumber).map(Recipient::getRecipientId).get(), Optional.empty());
306 }
307
308 if (byNumber.isEmpty()) {
309 logger.debug("Got recipient existing with uuid, updating with high trust number");
310 updateRecipientAddressLocked(byUuid.get().getRecipientId(), address);
311 return new Pair<>(byUuid.get().getRecipientId(), Optional.empty());
312 }
313
314 if (byUuid.isEmpty()) {
315 if (byNumber.get().getAddress().getUuid().isPresent()) {
316 logger.debug(
317 "Got recipient existing with number, but different uuid, so stripping its number and adding new recipient");
318
319 updateRecipientAddressLocked(byNumber.get().getRecipientId(),
320 new SignalServiceAddress(byNumber.get().getAddress().getUuid().get(), null));
321 return new Pair<>(addNewRecipientLocked(address), Optional.empty());
322 }
323
324 logger.debug("Got recipient existing with number and no uuid, updating with high trust uuid");
325 updateRecipientAddressLocked(byNumber.get().getRecipientId(), address);
326 return new Pair<>(byNumber.get().getRecipientId(), Optional.empty());
327 }
328
329 if (byNumber.get().getAddress().getUuid().isPresent()) {
330 logger.debug(
331 "Got separate recipients for high trust number and uuid, recipient for number has different uuid, so stripping its number");
332
333 updateRecipientAddressLocked(byNumber.get().getRecipientId(),
334 new SignalServiceAddress(byNumber.get().getAddress().getUuid().get(), null));
335 updateRecipientAddressLocked(byUuid.get().getRecipientId(), address);
336 return new Pair<>(byUuid.get().getRecipientId(), Optional.empty());
337 }
338
339 logger.debug("Got separate recipients for high trust number and uuid, need to merge them");
340 updateRecipientAddressLocked(byUuid.get().getRecipientId(), address);
341 mergeRecipientsLocked(byUuid.get().getRecipientId(), byNumber.get().getRecipientId());
342 return new Pair<>(byUuid.get().getRecipientId(), byNumber.map(Recipient::getRecipientId));
343 }
344
345 private RecipientId addNewRecipientLocked(final SignalServiceAddress serviceAddress) {
346 final var nextRecipientId = nextIdLocked();
347 storeRecipientLocked(nextRecipientId, new Recipient(nextRecipientId, serviceAddress, null, null, null, null));
348 return nextRecipientId;
349 }
350
351 private void updateRecipientAddressLocked(
352 final RecipientId recipientId, final SignalServiceAddress address
353 ) {
354 final var recipient = recipients.get(recipientId);
355 storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withAddress(address).build());
356 }
357
358 private void storeRecipientLocked(
359 final RecipientId recipientId, final Recipient recipient
360 ) {
361 recipients.put(recipientId, recipient);
362 saveLocked();
363 }
364
365 private void mergeRecipientsLocked(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
366 final var recipient = recipients.get(recipientId);
367 final var toBeMergedRecipient = recipients.get(toBeMergedRecipientId);
368 recipients.put(recipientId,
369 new Recipient(recipientId,
370 recipient.getAddress(),
371 recipient.getContact() != null ? recipient.getContact() : toBeMergedRecipient.getContact(),
372 recipient.getProfileKey() != null
373 ? recipient.getProfileKey()
374 : toBeMergedRecipient.getProfileKey(),
375 recipient.getProfileKeyCredential() != null
376 ? recipient.getProfileKeyCredential()
377 : toBeMergedRecipient.getProfileKeyCredential(),
378 recipient.getProfile() != null ? recipient.getProfile() : toBeMergedRecipient.getProfile()));
379 recipients.remove(toBeMergedRecipientId);
380 saveLocked();
381 }
382
383 private Optional<Recipient> findByNameLocked(final String number) {
384 return recipients.entrySet()
385 .stream()
386 .filter(entry -> entry.getValue().getAddress().getNumber().isPresent() && number.equals(entry.getValue()
387 .getAddress()
388 .getNumber()
389 .get()))
390 .findFirst()
391 .map(Map.Entry::getValue);
392 }
393
394 private Optional<Recipient> findByUuidLocked(final UUID uuid) {
395 return recipients.entrySet()
396 .stream()
397 .filter(entry -> entry.getValue().getAddress().getUuid().isPresent() && uuid.equals(entry.getValue()
398 .getAddress()
399 .getUuid()
400 .get()))
401 .findFirst()
402 .map(Map.Entry::getValue);
403 }
404
405 private RecipientId nextIdLocked() {
406 return new RecipientId(++this.lastId);
407 }
408
409 private void saveLocked() {
410 final var base64 = Base64.getEncoder();
411 var storage = new Storage(recipients.entrySet().stream().map(pair -> {
412 final var recipient = pair.getValue();
413 final var contact = recipient.getContact() == null
414 ? null
415 : new Storage.Recipient.Contact(recipient.getContact().getName(),
416 recipient.getContact().getColor(),
417 recipient.getContact().getMessageExpirationTime(),
418 recipient.getContact().isBlocked(),
419 recipient.getContact().isArchived());
420 final var profile = recipient.getProfile() == null
421 ? null
422 : new Storage.Recipient.Profile(recipient.getProfile().getLastUpdateTimestamp(),
423 recipient.getProfile().getGivenName(),
424 recipient.getProfile().getFamilyName(),
425 recipient.getProfile().getAbout(),
426 recipient.getProfile().getAboutEmoji(),
427 recipient.getProfile().getUnidentifiedAccessMode().name(),
428 recipient.getProfile()
429 .getCapabilities()
430 .stream()
431 .map(Enum::name)
432 .collect(Collectors.toSet()));
433 return new Storage.Recipient(pair.getKey().getId(),
434 recipient.getAddress().getNumber().orNull(),
435 recipient.getAddress().getUuid().transform(UUID::toString).orNull(),
436 recipient.getProfileKey() == null
437 ? null
438 : base64.encodeToString(recipient.getProfileKey().serialize()),
439 recipient.getProfileKeyCredential() == null
440 ? null
441 : base64.encodeToString(recipient.getProfileKeyCredential().serialize()),
442 contact,
443 profile);
444 }).collect(Collectors.toList()), lastId);
445
446 // Write to memory first to prevent corrupting the file in case of serialization errors
447 try (var inMemoryOutput = new ByteArrayOutputStream()) {
448 objectMapper.writeValue(inMemoryOutput, storage);
449
450 var input = new ByteArrayInputStream(inMemoryOutput.toByteArray());
451 try (var outputStream = new FileOutputStream(file)) {
452 input.transferTo(outputStream);
453 }
454 } catch (Exception e) {
455 logger.error("Error saving recipient store file: {}", e.getMessage());
456 }
457 }
458
459 private static class Storage {
460
461 public List<Recipient> recipients;
462
463 public long lastId;
464
465 // For deserialization
466 private Storage() {
467 }
468
469 public Storage(final List<Recipient> recipients, final long lastId) {
470 this.recipients = recipients;
471 this.lastId = lastId;
472 }
473
474 private static class Recipient {
475
476 public long id;
477 public String number;
478 public String uuid;
479 public String profileKey;
480 public String profileKeyCredential;
481 public Contact contact;
482 public Profile profile;
483
484 // For deserialization
485 private Recipient() {
486 }
487
488 public Recipient(
489 final long id,
490 final String number,
491 final String uuid,
492 final String profileKey,
493 final String profileKeyCredential,
494 final Contact contact,
495 final Profile profile
496 ) {
497 this.id = id;
498 this.number = number;
499 this.uuid = uuid;
500 this.profileKey = profileKey;
501 this.profileKeyCredential = profileKeyCredential;
502 this.contact = contact;
503 this.profile = profile;
504 }
505
506 private static class Contact {
507
508 public String name;
509 public String color;
510 public int messageExpirationTime;
511 public boolean blocked;
512 public boolean archived;
513
514 // For deserialization
515 public Contact() {
516 }
517
518 public Contact(
519 final String name,
520 final String color,
521 final int messageExpirationTime,
522 final boolean blocked,
523 final boolean archived
524 ) {
525 this.name = name;
526 this.color = color;
527 this.messageExpirationTime = messageExpirationTime;
528 this.blocked = blocked;
529 this.archived = archived;
530 }
531 }
532
533 private static class Profile {
534
535 public long lastUpdateTimestamp;
536 public String givenName;
537 public String familyName;
538 public String about;
539 public String aboutEmoji;
540 public String unidentifiedAccessMode;
541 public Set<String> capabilities;
542
543 // For deserialization
544 private Profile() {
545 }
546
547 public Profile(
548 final long lastUpdateTimestamp,
549 final String givenName,
550 final String familyName,
551 final String about,
552 final String aboutEmoji,
553 final String unidentifiedAccessMode,
554 final Set<String> capabilities
555 ) {
556 this.lastUpdateTimestamp = lastUpdateTimestamp;
557 this.givenName = givenName;
558 this.familyName = familyName;
559 this.about = about;
560 this.aboutEmoji = aboutEmoji;
561 this.unidentifiedAccessMode = unidentifiedAccessMode;
562 this.capabilities = capabilities;
563 }
564 }
565 }
566 }
567
568 public interface RecipientMergeHandler {
569
570 void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId);
571 }
572 }