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