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