1 package org
.asamk
.signal
.manager
.storage
.recipients
;
3 import com
.fasterxml
.jackson
.databind
.ObjectMapper
;
5 import org
.asamk
.signal
.manager
.api
.Pair
;
6 import org
.asamk
.signal
.manager
.api
.UnregisteredRecipientException
;
7 import org
.asamk
.signal
.manager
.storage
.Utils
;
8 import org
.asamk
.signal
.manager
.storage
.contacts
.ContactsStore
;
9 import org
.asamk
.signal
.manager
.storage
.profiles
.ProfileStore
;
10 import org
.signal
.libsignal
.zkgroup
.InvalidInputException
;
11 import org
.signal
.libsignal
.zkgroup
.profiles
.ProfileKey
;
12 import org
.signal
.libsignal
.zkgroup
.profiles
.ProfileKeyCredential
;
13 import org
.slf4j
.Logger
;
14 import org
.slf4j
.LoggerFactory
;
15 import org
.whispersystems
.signalservice
.api
.push
.ACI
;
16 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
;
17 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
18 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
20 import java
.io
.ByteArrayInputStream
;
21 import java
.io
.ByteArrayOutputStream
;
23 import java
.io
.FileInputStream
;
24 import java
.io
.FileNotFoundException
;
25 import java
.io
.FileOutputStream
;
26 import java
.io
.IOException
;
27 import java
.util
.ArrayList
;
28 import java
.util
.Base64
;
29 import java
.util
.HashMap
;
30 import java
.util
.List
;
32 import java
.util
.Objects
;
33 import java
.util
.Optional
;
35 import java
.util
.UUID
;
36 import java
.util
.function
.Supplier
;
37 import java
.util
.stream
.Collectors
;
39 public class RecipientStore
implements RecipientResolver
, ContactsStore
, ProfileStore
{
41 private final static Logger logger
= LoggerFactory
.getLogger(RecipientStore
.class);
43 private final ObjectMapper objectMapper
;
44 private final File file
;
45 private final RecipientMergeHandler recipientMergeHandler
;
46 private final SelfAddressProvider selfAddressProvider
;
48 private final Map
<RecipientId
, Recipient
> recipients
;
49 private final Map
<Long
, Long
> recipientsMerged
= new HashMap
<>();
52 private boolean isBulkUpdating
;
54 public static RecipientStore
load(
55 File file
, RecipientMergeHandler recipientMergeHandler
, SelfAddressProvider selfAddressProvider
57 final var objectMapper
= Utils
.createStorageObjectMapper();
58 try (var inputStream
= new FileInputStream(file
)) {
59 final var storage
= objectMapper
.readValue(inputStream
, Storage
.class);
61 final var recipientStore
= new RecipientStore(objectMapper
,
63 recipientMergeHandler
,
67 final var recipients
= storage
.recipients
.stream().map(r
-> {
68 final var recipientId
= new RecipientId(r
.id
, recipientStore
);
69 final var address
= new RecipientAddress(Optional
.ofNullable(r
.uuid
).map(UuidUtil
::parseOrThrow
),
70 Optional
.ofNullable(r
.number
));
72 Contact contact
= null;
73 if (r
.contact
!= null) {
74 contact
= new Contact(r
.contact
.name
,
76 r
.contact
.messageExpirationTime
,
81 ProfileKey profileKey
= null;
82 if (r
.profileKey
!= null) {
84 profileKey
= new ProfileKey(Base64
.getDecoder().decode(r
.profileKey
));
85 } catch (InvalidInputException ignored
) {
89 ProfileKeyCredential profileKeyCredential
= null;
90 if (r
.profileKeyCredential
!= null) {
92 profileKeyCredential
= new ProfileKeyCredential(Base64
.getDecoder()
93 .decode(r
.profileKeyCredential
));
94 } catch (Throwable ignored
) {
98 Profile profile
= null;
99 if (r
.profile
!= null) {
100 profile
= new Profile(r
.profile
.lastUpdateTimestamp
,
102 r
.profile
.familyName
,
104 r
.profile
.aboutEmoji
,
105 r
.profile
.avatarUrlPath
,
106 r
.profile
.paymentAddress
== null
108 : Base64
.getDecoder().decode(r
.profile
.paymentAddress
),
109 Profile
.UnidentifiedAccessMode
.valueOfOrUnknown(r
.profile
.unidentifiedAccessMode
),
110 r
.profile
.capabilities
.stream()
111 .map(Profile
.Capability
::valueOfOrNull
)
112 .filter(Objects
::nonNull
)
113 .collect(Collectors
.toSet()));
116 return new Recipient(recipientId
, address
, contact
, profileKey
, profileKeyCredential
, profile
);
117 }).collect(Collectors
.toMap(Recipient
::getRecipientId
, r
-> r
));
119 recipientStore
.addRecipients(recipients
);
121 return recipientStore
;
122 } catch (FileNotFoundException e
) {
123 logger
.trace("Creating new recipient store.");
124 return new RecipientStore(objectMapper
,
126 recipientMergeHandler
,
130 } catch (IOException e
) {
131 logger
.warn("Failed to load recipient store", e
);
132 throw new RuntimeException(e
);
136 private RecipientStore(
137 final ObjectMapper objectMapper
,
139 final RecipientMergeHandler recipientMergeHandler
,
140 final SelfAddressProvider selfAddressProvider
,
141 final Map
<RecipientId
, Recipient
> recipients
,
144 this.objectMapper
= objectMapper
;
146 this.recipientMergeHandler
= recipientMergeHandler
;
147 this.selfAddressProvider
= selfAddressProvider
;
148 this.recipients
= recipients
;
149 this.lastId
= lastId
;
152 public boolean isBulkUpdating() {
153 return isBulkUpdating
;
156 public void setBulkUpdating(final boolean bulkUpdating
) {
157 isBulkUpdating
= bulkUpdating
;
159 synchronized (recipients
) {
165 public RecipientAddress
resolveRecipientAddress(RecipientId recipientId
) {
166 synchronized (recipients
) {
167 return getRecipient(recipientId
).getAddress();
171 public Recipient
getRecipient(RecipientId recipientId
) {
172 synchronized (recipients
) {
173 return recipients
.get(recipientId
);
178 public RecipientId
resolveRecipient(ServiceId serviceId
) {
179 return resolveRecipient(new RecipientAddress(serviceId
.uuid()), false, false);
183 public RecipientId
resolveRecipient(final long recipientId
) {
184 final var recipient
= getRecipient(new RecipientId(recipientId
, this));
185 return recipient
== null ?
null : recipient
.getRecipientId();
189 public RecipientId
resolveRecipient(final String identifier
) {
190 return resolveRecipient(Utils
.getRecipientAddressFromIdentifier(identifier
), false, false);
193 public RecipientId
resolveRecipient(
194 final String number
, Supplier
<ACI
> aciSupplier
195 ) throws UnregisteredRecipientException
{
196 final Optional
<Recipient
> byNumber
;
197 synchronized (recipients
) {
198 byNumber
= findByNumberLocked(number
);
200 if (byNumber
.isEmpty() || byNumber
.get().getAddress().uuid().isEmpty()) {
201 final var aci
= aciSupplier
.get();
203 throw new UnregisteredRecipientException(new RecipientAddress(null, number
));
206 return resolveRecipient(new RecipientAddress(aci
.uuid(), number
), false, false);
208 return byNumber
.get().getRecipientId();
211 public RecipientId
resolveRecipient(RecipientAddress address
) {
212 return resolveRecipient(address
, false, false);
216 public RecipientId
resolveRecipient(final SignalServiceAddress address
) {
217 return resolveRecipient(new RecipientAddress(address
), false, false);
220 public RecipientId
resolveSelfRecipientTrusted(RecipientAddress address
) {
221 return resolveRecipient(address
, true, true);
224 public RecipientId
resolveRecipientTrusted(RecipientAddress address
) {
225 return resolveRecipient(address
, true, false);
228 public RecipientId
resolveRecipientTrusted(SignalServiceAddress address
) {
229 return resolveRecipient(new RecipientAddress(address
), true, false);
232 public List
<RecipientId
> resolveRecipientsTrusted(List
<RecipientAddress
> addresses
) {
233 final List
<RecipientId
> recipientIds
;
234 final List
<Pair
<RecipientId
, RecipientId
>> toBeMerged
= new ArrayList
<>();
235 synchronized (recipients
) {
236 recipientIds
= addresses
.stream().map(address
-> {
237 final var pair
= resolveRecipientLocked(address
, true, false);
238 if (pair
.second().isPresent()) {
239 toBeMerged
.add(new Pair
<>(pair
.first(), pair
.second().get()));
244 for (var pair
: toBeMerged
) {
245 recipientMergeHandler
.mergeRecipients(pair
.first(), pair
.second());
251 public void storeContact(RecipientId recipientId
, final Contact contact
) {
252 synchronized (recipients
) {
253 final var recipient
= recipients
.get(recipientId
);
254 storeRecipientLocked(recipientId
, Recipient
.newBuilder(recipient
).withContact(contact
).build());
259 public Contact
getContact(RecipientId recipientId
) {
260 final var recipient
= getRecipient(recipientId
);
261 return recipient
== null ?
null : recipient
.getContact();
265 public List
<Pair
<RecipientId
, Contact
>> getContacts() {
266 return recipients
.entrySet()
268 .filter(e
-> e
.getValue().getContact() != null)
269 .map(e
-> new Pair
<>(e
.getKey(), e
.getValue().getContact()))
274 public void deleteContact(RecipientId recipientId
) {
275 synchronized (recipients
) {
276 final var recipient
= recipients
.get(recipientId
);
277 storeRecipientLocked(recipientId
, Recipient
.newBuilder(recipient
).withContact(null).build());
281 public void deleteRecipientData(RecipientId recipientId
) {
282 synchronized (recipients
) {
283 logger
.debug("Deleting recipient data for {}", recipientId
);
284 final var recipient
= recipients
.get(recipientId
);
285 recipient
.getAddress()
287 .ifPresent(uuid
-> storeRecipientLocked(recipientId
,
288 Recipient
.newBuilder()
289 .withRecipientId(recipientId
)
290 .withAddress(new RecipientAddress(uuid
))
296 public Profile
getProfile(final RecipientId recipientId
) {
297 final var recipient
= getRecipient(recipientId
);
298 return recipient
== null ?
null : recipient
.getProfile();
302 public ProfileKey
getProfileKey(final RecipientId recipientId
) {
303 final var recipient
= getRecipient(recipientId
);
304 return recipient
== null ?
null : recipient
.getProfileKey();
308 public ProfileKeyCredential
getProfileKeyCredential(final RecipientId recipientId
) {
309 final var recipient
= getRecipient(recipientId
);
310 return recipient
== null ?
null : recipient
.getProfileKeyCredential();
314 public void storeProfile(RecipientId recipientId
, final Profile profile
) {
315 synchronized (recipients
) {
316 final var recipient
= recipients
.get(recipientId
);
317 storeRecipientLocked(recipientId
, Recipient
.newBuilder(recipient
).withProfile(profile
).build());
322 public void storeProfileKey(RecipientId recipientId
, final ProfileKey profileKey
) {
323 synchronized (recipients
) {
324 final var recipient
= recipients
.get(recipientId
);
325 if (profileKey
!= null && profileKey
.equals(recipient
.getProfileKey()) && (
326 recipient
.getProfile() == null || (
327 recipient
.getProfile().getUnidentifiedAccessMode() != Profile
.UnidentifiedAccessMode
.UNKNOWN
328 && recipient
.getProfile().getUnidentifiedAccessMode()
329 != Profile
.UnidentifiedAccessMode
.DISABLED
335 final var newRecipient
= Recipient
.newBuilder(recipient
)
336 .withProfileKey(profileKey
)
337 .withProfileKeyCredential(null)
338 .withProfile(recipient
.getProfile() == null
340 : Profile
.newBuilder(recipient
.getProfile()).withLastUpdateTimestamp(0).build())
342 storeRecipientLocked(recipientId
, newRecipient
);
347 public void storeProfileKeyCredential(RecipientId recipientId
, final ProfileKeyCredential profileKeyCredential
) {
348 synchronized (recipients
) {
349 final var recipient
= recipients
.get(recipientId
);
350 storeRecipientLocked(recipientId
,
351 Recipient
.newBuilder(recipient
).withProfileKeyCredential(profileKeyCredential
).build());
355 public boolean isEmpty() {
356 synchronized (recipients
) {
357 return recipients
.isEmpty();
361 private void addRecipients(final Map
<RecipientId
, Recipient
> recipients
) {
362 this.recipients
.putAll(recipients
);
366 * @param isHighTrust true, if the number/uuid connection was obtained from a trusted source.
367 * Has no effect, if the address contains only a number or a uuid.
369 private RecipientId
resolveRecipient(RecipientAddress address
, boolean isHighTrust
, boolean isSelf
) {
370 final Pair
<RecipientId
, Optional
<RecipientId
>> pair
;
371 synchronized (recipients
) {
372 pair
= resolveRecipientLocked(address
, isHighTrust
, isSelf
);
375 if (pair
.second().isPresent()) {
376 recipientMergeHandler
.mergeRecipients(pair
.first(), pair
.second().get());
381 private Pair
<RecipientId
, Optional
<RecipientId
>> resolveRecipientLocked(
382 RecipientAddress address
, boolean isHighTrust
, boolean isSelf
384 if (isHighTrust
&& !isSelf
) {
385 if (selfAddressProvider
.getSelfAddress().matches(address
)) {
389 final var byNumber
= address
.number().isEmpty()
390 ? Optional
.<Recipient
>empty()
391 : findByNumberLocked(address
.number().get());
392 final var byUuid
= address
.uuid().isEmpty()
393 ? Optional
.<Recipient
>empty()
394 : findByUuidLocked(address
.uuid().get());
396 if (byNumber
.isEmpty() && byUuid
.isEmpty()) {
397 logger
.debug("Got new recipient, both uuid and number are unknown");
399 if (isHighTrust
|| address
.uuid().isEmpty() || address
.number().isEmpty()) {
400 return new Pair
<>(addNewRecipientLocked(address
), Optional
.empty());
403 return new Pair
<>(addNewRecipientLocked(new RecipientAddress(address
.uuid().get())), Optional
.empty());
406 if (!isHighTrust
|| address
.uuid().isEmpty() || address
.number().isEmpty() || byNumber
.equals(byUuid
)) {
407 return new Pair
<>(byUuid
.or(() -> byNumber
).map(Recipient
::getRecipientId
).get(), Optional
.empty());
410 if (byNumber
.isEmpty()) {
411 logger
.debug("Got recipient {} existing with uuid, updating with high trust number",
412 byUuid
.get().getRecipientId());
413 updateRecipientAddressLocked(byUuid
.get().getRecipientId(), address
);
414 return new Pair
<>(byUuid
.get().getRecipientId(), Optional
.empty());
417 final var byNumberRecipient
= byNumber
.get();
419 if (byUuid
.isEmpty()) {
420 if (byNumberRecipient
.getAddress().uuid().isPresent()) {
422 "Got recipient {} existing with number, but different uuid, so stripping its number and adding new recipient",
423 byNumberRecipient
.getRecipientId());
425 updateRecipientAddressLocked(byNumberRecipient
.getRecipientId(),
426 new RecipientAddress(byNumberRecipient
.getAddress().uuid().get()));
427 return new Pair
<>(addNewRecipientLocked(address
), Optional
.empty());
430 logger
.debug("Got recipient {} existing with number and no uuid, updating with high trust uuid",
431 byNumberRecipient
.getRecipientId());
432 updateRecipientAddressLocked(byNumberRecipient
.getRecipientId(), address
);
433 return new Pair
<>(byNumberRecipient
.getRecipientId(), Optional
.empty());
436 final var byUuidRecipient
= byUuid
.get();
438 if (byNumberRecipient
.getAddress().uuid().isPresent()) {
440 "Got separate recipients for high trust number {} and uuid {}, recipient for number has different uuid, so stripping its number",
441 byNumberRecipient
.getRecipientId(),
442 byUuidRecipient
.getRecipientId());
444 updateRecipientAddressLocked(byNumberRecipient
.getRecipientId(),
445 new RecipientAddress(byNumberRecipient
.getAddress().uuid().get()));
446 updateRecipientAddressLocked(byUuidRecipient
.getRecipientId(), address
);
447 return new Pair
<>(byUuidRecipient
.getRecipientId(), Optional
.empty());
450 logger
.debug("Got separate recipients for high trust number {} and uuid {}, need to merge them",
451 byNumberRecipient
.getRecipientId(),
452 byUuidRecipient
.getRecipientId());
453 updateRecipientAddressLocked(byUuidRecipient
.getRecipientId(), address
);
454 // Create a fixed RecipientId that won't update its id after merge
455 final var toBeMergedRecipientId
= new RecipientId(byNumberRecipient
.getRecipientId().id(), null);
456 mergeRecipientsLocked(byUuidRecipient
.getRecipientId(), toBeMergedRecipientId
);
457 return new Pair
<>(byUuidRecipient
.getRecipientId(), Optional
.of(toBeMergedRecipientId
));
460 private RecipientId
addNewRecipientLocked(final RecipientAddress address
) {
461 final var nextRecipientId
= nextIdLocked();
462 logger
.debug("Adding new recipient {} with address {}", nextRecipientId
, address
);
463 storeRecipientLocked(nextRecipientId
, new Recipient(nextRecipientId
, address
, null, null, null, null));
464 return nextRecipientId
;
467 private void updateRecipientAddressLocked(RecipientId recipientId
, final RecipientAddress address
) {
468 final var recipient
= recipients
.get(recipientId
);
469 storeRecipientLocked(recipientId
, Recipient
.newBuilder(recipient
).withAddress(address
).build());
472 long getActualRecipientId(long recipientId
) {
473 while (recipientsMerged
.containsKey(recipientId
)) {
474 final var newRecipientId
= recipientsMerged
.get(recipientId
);
475 logger
.debug("Using {} instead of {}, because recipients have been merged", newRecipientId
, recipientId
);
476 recipientId
= newRecipientId
;
481 private void storeRecipientLocked(final RecipientId recipientId
, final Recipient recipient
) {
482 final var existingRecipient
= recipients
.get(recipientId
);
483 if (existingRecipient
== null || !existingRecipient
.equals(recipient
)) {
484 recipients
.put(recipientId
, recipient
);
489 private void mergeRecipientsLocked(RecipientId recipientId
, RecipientId toBeMergedRecipientId
) {
490 final var recipient
= recipients
.get(recipientId
);
491 final var toBeMergedRecipient
= recipients
.get(toBeMergedRecipientId
);
492 recipients
.put(recipientId
,
493 new Recipient(recipientId
,
494 recipient
.getAddress(),
495 recipient
.getContact() != null ? recipient
.getContact() : toBeMergedRecipient
.getContact(),
496 recipient
.getProfileKey() != null
497 ? recipient
.getProfileKey()
498 : toBeMergedRecipient
.getProfileKey(),
499 recipient
.getProfileKeyCredential() != null
500 ? recipient
.getProfileKeyCredential()
501 : toBeMergedRecipient
.getProfileKeyCredential(),
502 recipient
.getProfile() != null ? recipient
.getProfile() : toBeMergedRecipient
.getProfile()));
503 recipients
.remove(toBeMergedRecipientId
);
504 recipientsMerged
.put(toBeMergedRecipientId
.id(), recipientId
.id());
508 private Optional
<Recipient
> findByNumberLocked(final String number
) {
509 return recipients
.entrySet()
511 .filter(entry
-> entry
.getValue().getAddress().number().isPresent() && number
.equals(entry
.getValue()
516 .map(Map
.Entry
::getValue
);
519 private Optional
<Recipient
> findByUuidLocked(final UUID uuid
) {
520 return recipients
.entrySet()
522 .filter(entry
-> entry
.getValue().getAddress().uuid().isPresent() && uuid
.equals(entry
.getValue()
527 .map(Map
.Entry
::getValue
);
530 private RecipientId
nextIdLocked() {
531 return new RecipientId(++this.lastId
, this);
534 private void saveLocked() {
535 if (isBulkUpdating
) {
538 final var base64
= Base64
.getEncoder();
539 var storage
= new Storage(recipients
.entrySet().stream().map(pair
-> {
540 final var recipient
= pair
.getValue();
541 final var recipientContact
= recipient
.getContact();
542 final var contact
= recipientContact
== null
544 : new Storage
.Recipient
.Contact(recipientContact
.getName(),
545 recipientContact
.getColor(),
546 recipientContact
.getMessageExpirationTime(),
547 recipientContact
.isBlocked(),
548 recipientContact
.isArchived());
549 final var recipientProfile
= recipient
.getProfile();
550 final var profile
= recipientProfile
== null
552 : new Storage
.Recipient
.Profile(recipientProfile
.getLastUpdateTimestamp(),
553 recipientProfile
.getGivenName(),
554 recipientProfile
.getFamilyName(),
555 recipientProfile
.getAbout(),
556 recipientProfile
.getAboutEmoji(),
557 recipientProfile
.getAvatarUrlPath(),
558 recipientProfile
.getPaymentAddress() == null
560 : base64
.encodeToString(recipientProfile
.getPaymentAddress()),
561 recipientProfile
.getUnidentifiedAccessMode().name(),
562 recipientProfile
.getCapabilities().stream().map(Enum
::name
).collect(Collectors
.toSet()));
563 return new Storage
.Recipient(pair
.getKey().id(),
564 recipient
.getAddress().number().orElse(null),
565 recipient
.getAddress().uuid().map(UUID
::toString
).orElse(null),
566 recipient
.getProfileKey() == null
568 : base64
.encodeToString(recipient
.getProfileKey().serialize()),
569 recipient
.getProfileKeyCredential() == null
571 : base64
.encodeToString(recipient
.getProfileKeyCredential().serialize()),
574 }).toList(), lastId
);
576 // Write to memory first to prevent corrupting the file in case of serialization errors
577 try (var inMemoryOutput
= new ByteArrayOutputStream()) {
578 objectMapper
.writeValue(inMemoryOutput
, storage
);
580 var input
= new ByteArrayInputStream(inMemoryOutput
.toByteArray());
581 try (var outputStream
= new FileOutputStream(file
)) {
582 input
.transferTo(outputStream
);
584 } catch (Exception e
) {
585 logger
.error("Error saving recipient store file: {}", e
.getMessage());
589 private record Storage(List
<Recipient
> recipients
, long lastId
) {
591 private record Recipient(
596 String profileKeyCredential
,
597 Storage
.Recipient
.Contact contact
,
598 Storage
.Recipient
.Profile profile
601 private record Contact(
602 String name
, String color
, int messageExpirationTime
, boolean blocked
, boolean archived
605 private record Profile(
606 long lastUpdateTimestamp
,
611 String avatarUrlPath
,
612 String paymentAddress
,
613 String unidentifiedAccessMode
,
614 Set
<String
> capabilities
619 public interface RecipientMergeHandler
{
621 void mergeRecipients(RecipientId recipientId
, RecipientId toBeMergedRecipientId
);