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
.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
;
19 import java
.io
.ByteArrayInputStream
;
20 import java
.io
.ByteArrayOutputStream
;
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
;
31 import java
.util
.Objects
;
32 import java
.util
.Optional
;
34 import java
.util
.UUID
;
35 import java
.util
.function
.Supplier
;
36 import java
.util
.stream
.Collectors
;
38 public class RecipientStore
implements RecipientResolver
, ContactsStore
, ProfileStore
{
40 private final static Logger logger
= LoggerFactory
.getLogger(RecipientStore
.class);
42 private final ObjectMapper objectMapper
;
43 private final File file
;
44 private final RecipientMergeHandler recipientMergeHandler
;
46 private final Map
<RecipientId
, Recipient
> recipients
;
47 private final Map
<RecipientId
, RecipientId
> recipientsMerged
= new HashMap
<>();
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
));
60 Contact contact
= null;
61 if (r
.contact
!= null) {
62 contact
= new Contact(r
.contact
.name
,
64 r
.contact
.messageExpirationTime
,
69 ProfileKey profileKey
= null;
70 if (r
.profileKey
!= null) {
72 profileKey
= new ProfileKey(Base64
.getDecoder().decode(r
.profileKey
));
73 } catch (InvalidInputException ignored
) {
77 ProfileKeyCredential profileKeyCredential
= null;
78 if (r
.profileKeyCredential
!= null) {
80 profileKeyCredential
= new ProfileKeyCredential(Base64
.getDecoder()
81 .decode(r
.profileKeyCredential
));
82 } catch (Throwable ignored
) {
86 Profile profile
= null;
87 if (r
.profile
!= null) {
88 profile
= new Profile(r
.profile
.lastUpdateTimestamp
,
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()));
101 return new Recipient(recipientId
, address
, contact
, profileKey
, profileKeyCredential
, profile
);
102 }).collect(Collectors
.toMap(Recipient
::getRecipientId
, r
-> r
));
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);
111 private RecipientStore(
112 final ObjectMapper objectMapper
,
114 final RecipientMergeHandler recipientMergeHandler
,
115 final Map
<RecipientId
, Recipient
> recipients
,
118 this.objectMapper
= objectMapper
;
120 this.recipientMergeHandler
= recipientMergeHandler
;
121 this.recipients
= recipients
;
122 this.lastId
= lastId
;
125 public RecipientAddress
resolveRecipientAddress(RecipientId recipientId
) {
126 synchronized (recipients
) {
127 return getRecipient(recipientId
).getAddress();
131 public Recipient
getRecipient(RecipientId recipientId
) {
132 synchronized (recipients
) {
133 return getRecipientLocked(recipientId
);
138 public RecipientId
resolveRecipient(ACI aci
) {
139 return resolveRecipient(new RecipientAddress(aci
== null ?
null : aci
.uuid()), false);
143 public RecipientId
resolveRecipient(final String identifier
) {
144 return resolveRecipient(Utils
.getRecipientAddressFromIdentifier(identifier
), false);
147 public RecipientId
resolveRecipient(
148 final String number
, Supplier
<ACI
> aciSupplier
149 ) throws UnregisteredUserException
{
150 final Optional
<Recipient
> byNumber
;
151 synchronized (recipients
) {
152 byNumber
= findByNumberLocked(number
);
154 if (byNumber
.isEmpty() || byNumber
.get().getAddress().getUuid().isEmpty()) {
155 final var aci
= aciSupplier
.get();
157 throw new UnregisteredUserException(number
, null);
160 return resolveRecipient(new RecipientAddress(aci
.uuid(), number
), false);
162 return byNumber
.get().getRecipientId();
165 public RecipientId
resolveRecipient(RecipientAddress address
) {
166 return resolveRecipient(address
, false);
170 public RecipientId
resolveRecipient(final SignalServiceAddress address
) {
171 return resolveRecipient(new RecipientAddress(address
), false);
174 public RecipientId
resolveRecipientTrusted(RecipientAddress address
) {
175 return resolveRecipient(address
, true);
178 public RecipientId
resolveRecipientTrusted(SignalServiceAddress address
) {
179 return resolveRecipient(new RecipientAddress(address
), true);
182 public List
<RecipientId
> resolveRecipientsTrusted(List
<RecipientAddress
> addresses
) {
183 final List
<RecipientId
> recipientIds
;
184 final List
<Pair
<RecipientId
, RecipientId
>> toBeMerged
= new ArrayList
<>();
185 synchronized (recipients
) {
186 recipientIds
= addresses
.stream().map(address
-> {
187 final var pair
= resolveRecipientLocked(address
, true);
188 if (pair
.second().isPresent()) {
189 toBeMerged
.add(new Pair
<>(pair
.first(), pair
.second().get()));
192 }).collect(Collectors
.toList());
194 for (var pair
: toBeMerged
) {
195 recipientMergeHandler
.mergeRecipients(pair
.first(), pair
.second());
201 public void storeContact(final RecipientId recipientId
, final Contact contact
) {
202 synchronized (recipients
) {
203 final var recipient
= recipients
.get(recipientId
);
204 storeRecipientLocked(recipientId
, Recipient
.newBuilder(recipient
).withContact(contact
).build());
209 public Contact
getContact(final RecipientId recipientId
) {
210 final var recipient
= getRecipient(recipientId
);
211 return recipient
== null ?
null : recipient
.getContact();
215 public List
<Pair
<RecipientId
, Contact
>> getContacts() {
216 return recipients
.entrySet()
218 .filter(e
-> e
.getValue().getContact() != null)
219 .map(e
-> new Pair
<>(e
.getKey(), e
.getValue().getContact()))
220 .collect(Collectors
.toList());
224 public void deleteContact(final RecipientId recipientId
) {
225 synchronized (recipients
) {
226 final var recipient
= recipients
.get(recipientId
);
227 storeRecipientLocked(recipientId
, Recipient
.newBuilder(recipient
).withContact(null).build());
231 public void deleteRecipientData(final RecipientId recipientId
) {
232 synchronized (recipients
) {
233 final var recipient
= recipients
.get(recipientId
);
234 storeRecipientLocked(recipientId
,
235 Recipient
.newBuilder()
236 .withRecipientId(recipientId
)
237 .withAddress(new RecipientAddress(recipient
.getAddress().getUuid().orElse(null)))
243 public Profile
getProfile(final RecipientId recipientId
) {
244 final var recipient
= getRecipient(recipientId
);
245 return recipient
== null ?
null : recipient
.getProfile();
249 public ProfileKey
getProfileKey(final RecipientId recipientId
) {
250 final var recipient
= getRecipient(recipientId
);
251 return recipient
== null ?
null : recipient
.getProfileKey();
255 public ProfileKeyCredential
getProfileKeyCredential(final RecipientId recipientId
) {
256 final var recipient
= getRecipient(recipientId
);
257 return recipient
== null ?
null : recipient
.getProfileKeyCredential();
261 public void storeProfile(final RecipientId recipientId
, final Profile profile
) {
262 synchronized (recipients
) {
263 final var recipient
= recipients
.get(recipientId
);
264 storeRecipientLocked(recipientId
, Recipient
.newBuilder(recipient
).withProfile(profile
).build());
269 public void storeProfileKey(final RecipientId recipientId
, final ProfileKey profileKey
) {
270 synchronized (recipients
) {
271 final var recipient
= recipients
.get(recipientId
);
272 if (profileKey
!= null && profileKey
.equals(recipient
.getProfileKey())) {
276 final var newRecipient
= Recipient
.newBuilder(recipient
)
277 .withProfileKey(profileKey
)
278 .withProfileKeyCredential(null)
279 .withProfile(recipient
.getProfile() == null
281 : Profile
.newBuilder(recipient
.getProfile()).withLastUpdateTimestamp(0).build())
283 storeRecipientLocked(recipientId
, newRecipient
);
288 public void storeProfileKeyCredential(
289 final RecipientId recipientId
, final ProfileKeyCredential profileKeyCredential
291 synchronized (recipients
) {
292 final var recipient
= recipients
.get(recipientId
);
293 storeRecipientLocked(recipientId
,
294 Recipient
.newBuilder(recipient
).withProfileKeyCredential(profileKeyCredential
).build());
298 public boolean isEmpty() {
299 synchronized (recipients
) {
300 return recipients
.isEmpty();
305 * @param isHighTrust true, if the number/uuid connection was obtained from a trusted source.
306 * Has no effect, if the address contains only a number or a uuid.
308 private RecipientId
resolveRecipient(RecipientAddress address
, boolean isHighTrust
) {
309 final Pair
<RecipientId
, Optional
<RecipientId
>> pair
;
310 synchronized (recipients
) {
311 pair
= resolveRecipientLocked(address
, isHighTrust
);
314 if (pair
.second().isPresent()) {
315 recipientMergeHandler
.mergeRecipients(pair
.first(), pair
.second().get());
320 private Pair
<RecipientId
, Optional
<RecipientId
>> resolveRecipientLocked(
321 RecipientAddress address
, boolean isHighTrust
323 final var byNumber
= address
.getNumber().isEmpty()
324 ? Optional
.<Recipient
>empty()
325 : findByNumberLocked(address
.getNumber().get());
326 final var byUuid
= address
.getUuid().isEmpty()
327 ? Optional
.<Recipient
>empty()
328 : findByUuidLocked(address
.getUuid().get());
330 if (byNumber
.isEmpty() && byUuid
.isEmpty()) {
331 logger
.debug("Got new recipient, both uuid and number are unknown");
333 if (isHighTrust
|| address
.getUuid().isEmpty() || address
.getNumber().isEmpty()) {
334 return new Pair
<>(addNewRecipientLocked(address
), Optional
.empty());
337 return new Pair
<>(addNewRecipientLocked(new RecipientAddress(address
.getUuid().get())), Optional
.empty());
340 if (!isHighTrust
|| address
.getUuid().isEmpty() || address
.getNumber().isEmpty() || byNumber
.equals(byUuid
)) {
341 return new Pair
<>(byUuid
.or(() -> byNumber
).map(Recipient
::getRecipientId
).get(), Optional
.empty());
344 if (byNumber
.isEmpty()) {
345 logger
.debug("Got recipient existing with uuid, updating with high trust number");
346 updateRecipientAddressLocked(byUuid
.get().getRecipientId(), address
);
347 return new Pair
<>(byUuid
.get().getRecipientId(), Optional
.empty());
350 if (byUuid
.isEmpty()) {
351 if (byNumber
.get().getAddress().getUuid().isPresent()) {
353 "Got recipient existing with number, but different uuid, so stripping its number and adding new recipient");
355 updateRecipientAddressLocked(byNumber
.get().getRecipientId(),
356 new RecipientAddress(byNumber
.get().getAddress().getUuid().get()));
357 return new Pair
<>(addNewRecipientLocked(address
), Optional
.empty());
360 logger
.debug("Got recipient existing with number and no uuid, updating with high trust uuid");
361 updateRecipientAddressLocked(byNumber
.get().getRecipientId(), address
);
362 return new Pair
<>(byNumber
.get().getRecipientId(), Optional
.empty());
365 if (byNumber
.get().getAddress().getUuid().isPresent()) {
367 "Got separate recipients for high trust number and uuid, recipient for number has different uuid, so stripping its number");
369 updateRecipientAddressLocked(byNumber
.get().getRecipientId(),
370 new RecipientAddress(byNumber
.get().getAddress().getUuid().get()));
371 updateRecipientAddressLocked(byUuid
.get().getRecipientId(), address
);
372 return new Pair
<>(byUuid
.get().getRecipientId(), Optional
.empty());
375 logger
.debug("Got separate recipients for high trust number and uuid, need to merge them");
376 updateRecipientAddressLocked(byUuid
.get().getRecipientId(), address
);
377 mergeRecipientsLocked(byUuid
.get().getRecipientId(), byNumber
.get().getRecipientId());
378 recipientsMerged
.put(byNumber
.get().getRecipientId(), byUuid
.get().getRecipientId());
379 return new Pair
<>(byUuid
.get().getRecipientId(), byNumber
.map(Recipient
::getRecipientId
));
382 private RecipientId
addNewRecipientLocked(final RecipientAddress address
) {
383 final var nextRecipientId
= nextIdLocked();
384 storeRecipientLocked(nextRecipientId
, new Recipient(nextRecipientId
, address
, null, null, null, null));
385 return nextRecipientId
;
388 private void updateRecipientAddressLocked(
389 final RecipientId recipientId
, final RecipientAddress address
391 final var recipient
= recipients
.get(recipientId
);
392 storeRecipientLocked(recipientId
, Recipient
.newBuilder(recipient
).withAddress(address
).build());
395 private Recipient
getRecipientLocked(RecipientId recipientId
) {
396 while (recipientsMerged
.containsKey(recipientId
)) {
397 recipientId
= recipientsMerged
.get(recipientId
);
399 return recipients
.get(recipientId
);
402 private void storeRecipientLocked(
403 final RecipientId recipientId
, final Recipient recipient
405 final var existingRecipient
= getRecipientLocked(recipientId
);
406 if (existingRecipient
== null || !existingRecipient
.equals(recipient
)) {
407 recipients
.put(recipientId
, recipient
);
412 private void mergeRecipientsLocked(RecipientId recipientId
, RecipientId toBeMergedRecipientId
) {
413 final var recipient
= recipients
.get(recipientId
);
414 final var toBeMergedRecipient
= recipients
.get(toBeMergedRecipientId
);
415 recipients
.put(recipientId
,
416 new Recipient(recipientId
,
417 recipient
.getAddress(),
418 recipient
.getContact() != null ? recipient
.getContact() : toBeMergedRecipient
.getContact(),
419 recipient
.getProfileKey() != null
420 ? recipient
.getProfileKey()
421 : toBeMergedRecipient
.getProfileKey(),
422 recipient
.getProfileKeyCredential() != null
423 ? recipient
.getProfileKeyCredential()
424 : toBeMergedRecipient
.getProfileKeyCredential(),
425 recipient
.getProfile() != null ? recipient
.getProfile() : toBeMergedRecipient
.getProfile()));
426 recipients
.remove(toBeMergedRecipientId
);
430 private Optional
<Recipient
> findByNumberLocked(final String number
) {
431 return recipients
.entrySet()
433 .filter(entry
-> entry
.getValue().getAddress().getNumber().isPresent() && number
.equals(entry
.getValue()
438 .map(Map
.Entry
::getValue
);
441 private Optional
<Recipient
> findByUuidLocked(final UUID uuid
) {
442 return recipients
.entrySet()
444 .filter(entry
-> entry
.getValue().getAddress().getUuid().isPresent() && uuid
.equals(entry
.getValue()
449 .map(Map
.Entry
::getValue
);
452 private RecipientId
nextIdLocked() {
453 return new RecipientId(++this.lastId
);
456 private void saveLocked() {
457 final var base64
= Base64
.getEncoder();
458 var storage
= new Storage(recipients
.entrySet().stream().map(pair
-> {
459 final var recipient
= pair
.getValue();
460 final var contact
= recipient
.getContact() == null
462 : new Storage
.Recipient
.Contact(recipient
.getContact().getName(),
463 recipient
.getContact().getColor(),
464 recipient
.getContact().getMessageExpirationTime(),
465 recipient
.getContact().isBlocked(),
466 recipient
.getContact().isArchived());
467 final var profile
= recipient
.getProfile() == null
469 : new Storage
.Recipient
.Profile(recipient
.getProfile().getLastUpdateTimestamp(),
470 recipient
.getProfile().getGivenName(),
471 recipient
.getProfile().getFamilyName(),
472 recipient
.getProfile().getAbout(),
473 recipient
.getProfile().getAboutEmoji(),
474 recipient
.getProfile().getAvatarUrlPath(),
475 recipient
.getProfile().getUnidentifiedAccessMode().name(),
476 recipient
.getProfile()
480 .collect(Collectors
.toSet()));
481 return new Storage
.Recipient(pair
.getKey().id(),
482 recipient
.getAddress().getNumber().orElse(null),
483 recipient
.getAddress().getUuid().map(UUID
::toString
).orElse(null),
484 recipient
.getProfileKey() == null
486 : base64
.encodeToString(recipient
.getProfileKey().serialize()),
487 recipient
.getProfileKeyCredential() == null
489 : base64
.encodeToString(recipient
.getProfileKeyCredential().serialize()),
492 }).collect(Collectors
.toList()), lastId
);
494 // Write to memory first to prevent corrupting the file in case of serialization errors
495 try (var inMemoryOutput
= new ByteArrayOutputStream()) {
496 objectMapper
.writeValue(inMemoryOutput
, storage
);
498 var input
= new ByteArrayInputStream(inMemoryOutput
.toByteArray());
499 try (var outputStream
= new FileOutputStream(file
)) {
500 input
.transferTo(outputStream
);
502 } catch (Exception e
) {
503 logger
.error("Error saving recipient store file: {}", e
.getMessage());
507 private record Storage(List
<Recipient
> recipients
, long lastId
) {
509 private record Recipient(
514 String profileKeyCredential
,
515 Storage
.Recipient
.Contact contact
,
516 Storage
.Recipient
.Profile profile
519 private record Contact(
520 String name
, String color
, int messageExpirationTime
, boolean blocked
, boolean archived
523 private record Profile(
524 long lastUpdateTimestamp
,
529 String avatarUrlPath
,
530 String unidentifiedAccessMode
,
531 Set
<String
> capabilities
536 public interface RecipientMergeHandler
{
538 void mergeRecipients(RecipientId recipientId
, RecipientId toBeMergedRecipientId
);