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