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