]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java
b400e45d4c9ab1b2ddbf1c5e44cecb5c93f35048
[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, true));
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 logger.debug("Got recipient existing with number, updating with high trust uuid");
305 updateRecipientAddressLocked(byNumber.get().getRecipientId(), address);
306 return new Pair<>(byNumber.get().getRecipientId(), Optional.empty());
307 }
308
309 if (byNumber.get().getAddress().getUuid().isPresent()) {
310 logger.debug(
311 "Got separate recipients for high trust number and uuid, recipient for number has different uuid, so stripping its number");
312
313 updateRecipientAddressLocked(byNumber.get().getRecipientId(),
314 new SignalServiceAddress(byNumber.get().getAddress().getUuid().get(), null));
315 updateRecipientAddressLocked(byUuid.get().getRecipientId(), address);
316 return new Pair<>(byUuid.get().getRecipientId(), Optional.empty());
317 }
318
319 logger.debug("Got separate recipients for high trust number and uuid, need to merge them");
320 updateRecipientAddressLocked(byUuid.get().getRecipientId(), address);
321 mergeRecipientsLocked(byUuid.get().getRecipientId(), byNumber.get().getRecipientId());
322 return new Pair<>(byUuid.get().getRecipientId(), byNumber.map(Recipient::getRecipientId));
323 }
324
325 private RecipientId addNewRecipientLocked(final SignalServiceAddress serviceAddress) {
326 final var nextRecipientId = nextIdLocked();
327 storeRecipientLocked(nextRecipientId, new Recipient(nextRecipientId, serviceAddress, null, null, null, null));
328 return nextRecipientId;
329 }
330
331 private void updateRecipientAddressLocked(
332 final RecipientId recipientId, final SignalServiceAddress address
333 ) {
334 final var nextRecipientId = nextIdLocked();
335 final var recipient = recipients.get(recipientId);
336 storeRecipientLocked(nextRecipientId, Recipient.newBuilder(recipient).withAddress(address).build());
337 }
338
339 private void storeRecipientLocked(
340 final RecipientId recipientId, final Recipient recipient
341 ) {
342 recipients.put(recipientId, recipient);
343 saveLocked();
344 }
345
346 private void mergeRecipientsLocked(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
347 final var recipient = recipients.get(recipientId);
348 final var toBeMergedRecipient = recipients.get(toBeMergedRecipientId);
349 recipients.put(recipientId,
350 new Recipient(recipientId,
351 recipient.getAddress(),
352 recipient.getContact() != null ? recipient.getContact() : toBeMergedRecipient.getContact(),
353 recipient.getProfileKey() != null
354 ? recipient.getProfileKey()
355 : toBeMergedRecipient.getProfileKey(),
356 recipient.getProfileKeyCredential() != null
357 ? recipient.getProfileKeyCredential()
358 : toBeMergedRecipient.getProfileKeyCredential(),
359 recipient.getProfile() != null ? recipient.getProfile() : toBeMergedRecipient.getProfile()));
360 recipients.remove(toBeMergedRecipientId);
361 saveLocked();
362 }
363
364 private Optional<Recipient> findByNameLocked(final String number) {
365 return recipients.entrySet()
366 .stream()
367 .filter(entry -> entry.getValue().getAddress().getNumber().isPresent() && number.equals(entry.getValue()
368 .getAddress()
369 .getNumber()
370 .get()))
371 .findFirst()
372 .map(Map.Entry::getValue);
373 }
374
375 private Optional<Recipient> findByUuidLocked(final UUID uuid) {
376 return recipients.entrySet()
377 .stream()
378 .filter(entry -> entry.getValue().getAddress().getUuid().isPresent() && uuid.equals(entry.getValue()
379 .getAddress()
380 .getUuid()
381 .get()))
382 .findFirst()
383 .map(Map.Entry::getValue);
384 }
385
386 private RecipientId nextIdLocked() {
387 return new RecipientId(++this.lastId);
388 }
389
390 private void saveLocked() {
391 final var base64 = Base64.getEncoder();
392 var storage = new Storage(recipients.entrySet().stream().map(pair -> {
393 final var recipient = pair.getValue();
394 final var contact = recipient.getContact() == null
395 ? null
396 : new Storage.Recipient.Contact(recipient.getContact().getName(),
397 recipient.getContact().getColor(),
398 recipient.getContact().getMessageExpirationTime(),
399 recipient.getContact().isBlocked(),
400 recipient.getContact().isArchived());
401 final var profile = recipient.getProfile() == null
402 ? null
403 : new Storage.Recipient.Profile(recipient.getProfile().getLastUpdateTimestamp(),
404 recipient.getProfile().getGivenName(),
405 recipient.getProfile().getFamilyName(),
406 recipient.getProfile().getAbout(),
407 recipient.getProfile().getAboutEmoji(),
408 recipient.getProfile().getUnidentifiedAccessMode().name(),
409 recipient.getProfile()
410 .getCapabilities()
411 .stream()
412 .map(Enum::name)
413 .collect(Collectors.toSet()));
414 return new Storage.Recipient(pair.getKey().getId(),
415 recipient.getAddress().getNumber().orNull(),
416 recipient.getAddress().getUuid().transform(UUID::toString).orNull(),
417 recipient.getProfileKey() == null
418 ? null
419 : base64.encodeToString(recipient.getProfileKey().serialize()),
420 recipient.getProfileKeyCredential() == null
421 ? null
422 : base64.encodeToString(recipient.getProfileKeyCredential().serialize()),
423 contact,
424 profile);
425 }).collect(Collectors.toList()), lastId);
426
427 // Write to memory first to prevent corrupting the file in case of serialization errors
428 try (var inMemoryOutput = new ByteArrayOutputStream()) {
429 objectMapper.writeValue(inMemoryOutput, storage);
430
431 var input = new ByteArrayInputStream(inMemoryOutput.toByteArray());
432 try (var outputStream = new FileOutputStream(file)) {
433 input.transferTo(outputStream);
434 }
435 } catch (Exception e) {
436 logger.error("Error saving recipient store file: {}", e.getMessage());
437 }
438 }
439
440 private static class Storage {
441
442 public List<Recipient> recipients;
443
444 public long lastId;
445
446 // For deserialization
447 private Storage() {
448 }
449
450 public Storage(final List<Recipient> recipients, final long lastId) {
451 this.recipients = recipients;
452 this.lastId = lastId;
453 }
454
455 private static class Recipient {
456
457 public long id;
458 public String number;
459 public String uuid;
460 public String profileKey;
461 public String profileKeyCredential;
462 public Contact contact;
463 public Profile profile;
464
465 // For deserialization
466 private Recipient() {
467 }
468
469 public Recipient(
470 final long id,
471 final String number,
472 final String uuid,
473 final String profileKey,
474 final String profileKeyCredential,
475 final Contact contact,
476 final Profile profile
477 ) {
478 this.id = id;
479 this.number = number;
480 this.uuid = uuid;
481 this.profileKey = profileKey;
482 this.profileKeyCredential = profileKeyCredential;
483 this.contact = contact;
484 this.profile = profile;
485 }
486
487 private static class Contact {
488
489 public String name;
490 public String color;
491 public int messageExpirationTime;
492 public boolean blocked;
493 public boolean archived;
494
495 // For deserialization
496 public Contact() {
497 }
498
499 public Contact(
500 final String name,
501 final String color,
502 final int messageExpirationTime,
503 final boolean blocked,
504 final boolean archived
505 ) {
506 this.name = name;
507 this.color = color;
508 this.messageExpirationTime = messageExpirationTime;
509 this.blocked = blocked;
510 this.archived = archived;
511 }
512 }
513
514 private static class Profile {
515
516 public long lastUpdateTimestamp;
517 public String givenName;
518 public String familyName;
519 public String about;
520 public String aboutEmoji;
521 public String unidentifiedAccessMode;
522 public Set<String> capabilities;
523
524 // For deserialization
525 private Profile() {
526 }
527
528 public Profile(
529 final long lastUpdateTimestamp,
530 final String givenName,
531 final String familyName,
532 final String about,
533 final String aboutEmoji,
534 final String unidentifiedAccessMode,
535 final Set<String> capabilities
536 ) {
537 this.lastUpdateTimestamp = lastUpdateTimestamp;
538 this.givenName = givenName;
539 this.familyName = familyName;
540 this.about = about;
541 this.aboutEmoji = aboutEmoji;
542 this.unidentifiedAccessMode = unidentifiedAccessMode;
543 this.capabilities = capabilities;
544 }
545 }
546 }
547 }
548
549 public interface RecipientMergeHandler {
550
551 void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId);
552 }
553 }