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
<Long
, Long
> 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);
56 final var recipientStore
= new RecipientStore(objectMapper
,
58 recipientMergeHandler
,
61 final var recipients
= storage
.recipients
.stream().map(r
-> {
62 final var recipientId
= new RecipientId(r
.id
, recipientStore
);
63 final var address
= new RecipientAddress(Optional
.ofNullable(r
.uuid
).map(UuidUtil
::parseOrThrow
),
64 Optional
.ofNullable(r
.number
));
66 Contact contact
= null;
67 if (r
.contact
!= null) {
68 contact
= new Contact(r
.contact
.name
,
70 r
.contact
.messageExpirationTime
,
75 ProfileKey profileKey
= null;
76 if (r
.profileKey
!= null) {
78 profileKey
= new ProfileKey(Base64
.getDecoder().decode(r
.profileKey
));
79 } catch (InvalidInputException ignored
) {
83 ProfileKeyCredential profileKeyCredential
= null;
84 if (r
.profileKeyCredential
!= null) {
86 profileKeyCredential
= new ProfileKeyCredential(Base64
.getDecoder()
87 .decode(r
.profileKeyCredential
));
88 } catch (Throwable ignored
) {
92 Profile profile
= null;
93 if (r
.profile
!= null) {
94 profile
= new Profile(r
.profile
.lastUpdateTimestamp
,
99 r
.profile
.avatarUrlPath
,
100 Profile
.UnidentifiedAccessMode
.valueOfOrUnknown(r
.profile
.unidentifiedAccessMode
),
101 r
.profile
.capabilities
.stream()
102 .map(Profile
.Capability
::valueOfOrNull
)
103 .filter(Objects
::nonNull
)
104 .collect(Collectors
.toSet()));
107 return new Recipient(recipientId
, address
, contact
, profileKey
, profileKeyCredential
, profile
);
108 }).collect(Collectors
.toMap(Recipient
::getRecipientId
, r
-> r
));
110 recipientStore
.addRecipients(recipients
);
112 return recipientStore
;
113 } catch (FileNotFoundException e
) {
114 logger
.debug("Creating new recipient store.");
115 return new RecipientStore(objectMapper
, file
, recipientMergeHandler
, new HashMap
<>(), 0);
119 private RecipientStore(
120 final ObjectMapper objectMapper
,
122 final RecipientMergeHandler recipientMergeHandler
,
123 final Map
<RecipientId
, Recipient
> recipients
,
126 this.objectMapper
= objectMapper
;
128 this.recipientMergeHandler
= recipientMergeHandler
;
129 this.recipients
= recipients
;
130 this.lastId
= lastId
;
133 public RecipientAddress
resolveRecipientAddress(RecipientId recipientId
) {
134 synchronized (recipients
) {
135 return getRecipient(recipientId
).getAddress();
139 public Recipient
getRecipient(RecipientId recipientId
) {
140 synchronized (recipients
) {
141 return recipients
.get(recipientId
);
146 public RecipientId
resolveRecipient(ACI aci
) {
147 return resolveRecipient(new RecipientAddress(aci
== null ?
null : aci
.uuid()), false);
151 public RecipientId
resolveRecipient(final long recipientId
) {
152 final var recipient
= getRecipient(new RecipientId(recipientId
, this));
153 return recipient
== null ?
null : recipient
.getRecipientId();
157 public RecipientId
resolveRecipient(final String identifier
) {
158 return resolveRecipient(Utils
.getRecipientAddressFromIdentifier(identifier
), false);
161 public RecipientId
resolveRecipient(
162 final String number
, Supplier
<ACI
> aciSupplier
163 ) throws UnregisteredUserException
{
164 final Optional
<Recipient
> byNumber
;
165 synchronized (recipients
) {
166 byNumber
= findByNumberLocked(number
);
168 if (byNumber
.isEmpty() || byNumber
.get().getAddress().uuid().isEmpty()) {
169 final var aci
= aciSupplier
.get();
171 throw new UnregisteredUserException(number
, null);
174 return resolveRecipient(new RecipientAddress(aci
.uuid(), number
), false);
176 return byNumber
.get().getRecipientId();
179 public RecipientId
resolveRecipient(RecipientAddress address
) {
180 return resolveRecipient(address
, false);
184 public RecipientId
resolveRecipient(final SignalServiceAddress address
) {
185 return resolveRecipient(new RecipientAddress(address
), false);
188 public RecipientId
resolveRecipientTrusted(RecipientAddress address
) {
189 return resolveRecipient(address
, true);
192 public RecipientId
resolveRecipientTrusted(SignalServiceAddress address
) {
193 return resolveRecipient(new RecipientAddress(address
), true);
196 public List
<RecipientId
> resolveRecipientsTrusted(List
<RecipientAddress
> addresses
) {
197 final List
<RecipientId
> recipientIds
;
198 final List
<Pair
<RecipientId
, RecipientId
>> toBeMerged
= new ArrayList
<>();
199 synchronized (recipients
) {
200 recipientIds
= addresses
.stream().map(address
-> {
201 final var pair
= resolveRecipientLocked(address
, true);
202 if (pair
.second().isPresent()) {
203 toBeMerged
.add(new Pair
<>(pair
.first(), pair
.second().get()));
206 }).collect(Collectors
.toList());
208 for (var pair
: toBeMerged
) {
209 recipientMergeHandler
.mergeRecipients(pair
.first(), pair
.second());
215 public void storeContact(RecipientId recipientId
, final Contact contact
) {
216 synchronized (recipients
) {
217 final var recipient
= recipients
.get(recipientId
);
218 storeRecipientLocked(recipientId
, Recipient
.newBuilder(recipient
).withContact(contact
).build());
223 public Contact
getContact(RecipientId recipientId
) {
224 final var recipient
= getRecipient(recipientId
);
225 return recipient
== null ?
null : recipient
.getContact();
229 public List
<Pair
<RecipientId
, Contact
>> getContacts() {
230 return recipients
.entrySet()
232 .filter(e
-> e
.getValue().getContact() != null)
233 .map(e
-> new Pair
<>(e
.getKey(), e
.getValue().getContact()))
234 .collect(Collectors
.toList());
238 public void deleteContact(RecipientId recipientId
) {
239 synchronized (recipients
) {
240 final var recipient
= recipients
.get(recipientId
);
241 storeRecipientLocked(recipientId
, Recipient
.newBuilder(recipient
).withContact(null).build());
245 public void deleteRecipientData(RecipientId recipientId
) {
246 synchronized (recipients
) {
247 logger
.debug("Deleting recipient data for {}", recipientId
);
248 final var recipient
= recipients
.get(recipientId
);
249 storeRecipientLocked(recipientId
,
250 Recipient
.newBuilder()
251 .withRecipientId(recipientId
)
252 .withAddress(new RecipientAddress(recipient
.getAddress().uuid().orElse(null)))
258 public Profile
getProfile(final RecipientId recipientId
) {
259 final var recipient
= getRecipient(recipientId
);
260 return recipient
== null ?
null : recipient
.getProfile();
264 public ProfileKey
getProfileKey(final RecipientId recipientId
) {
265 final var recipient
= getRecipient(recipientId
);
266 return recipient
== null ?
null : recipient
.getProfileKey();
270 public ProfileKeyCredential
getProfileKeyCredential(final RecipientId recipientId
) {
271 final var recipient
= getRecipient(recipientId
);
272 return recipient
== null ?
null : recipient
.getProfileKeyCredential();
276 public void storeProfile(RecipientId recipientId
, final Profile profile
) {
277 synchronized (recipients
) {
278 final var recipient
= recipients
.get(recipientId
);
279 storeRecipientLocked(recipientId
, Recipient
.newBuilder(recipient
).withProfile(profile
).build());
284 public void storeProfileKey(RecipientId recipientId
, final ProfileKey profileKey
) {
285 synchronized (recipients
) {
286 final var recipient
= recipients
.get(recipientId
);
287 if (profileKey
!= null && profileKey
.equals(recipient
.getProfileKey())) {
291 final var newRecipient
= Recipient
.newBuilder(recipient
)
292 .withProfileKey(profileKey
)
293 .withProfileKeyCredential(null)
294 .withProfile(recipient
.getProfile() == null
296 : Profile
.newBuilder(recipient
.getProfile()).withLastUpdateTimestamp(0).build())
298 storeRecipientLocked(recipientId
, newRecipient
);
303 public void storeProfileKeyCredential(RecipientId recipientId
, final ProfileKeyCredential profileKeyCredential
) {
304 synchronized (recipients
) {
305 final var recipient
= recipients
.get(recipientId
);
306 storeRecipientLocked(recipientId
,
307 Recipient
.newBuilder(recipient
).withProfileKeyCredential(profileKeyCredential
).build());
311 public boolean isEmpty() {
312 synchronized (recipients
) {
313 return recipients
.isEmpty();
317 private void addRecipients(final Map
<RecipientId
, Recipient
> recipients
) {
318 this.recipients
.putAll(recipients
);
322 * @param isHighTrust true, if the number/uuid connection was obtained from a trusted source.
323 * Has no effect, if the address contains only a number or a uuid.
325 private RecipientId
resolveRecipient(RecipientAddress address
, boolean isHighTrust
) {
326 final Pair
<RecipientId
, Optional
<RecipientId
>> pair
;
327 synchronized (recipients
) {
328 pair
= resolveRecipientLocked(address
, isHighTrust
);
331 if (pair
.second().isPresent()) {
332 recipientMergeHandler
.mergeRecipients(pair
.first(), pair
.second().get());
337 private Pair
<RecipientId
, Optional
<RecipientId
>> resolveRecipientLocked(
338 RecipientAddress address
, boolean isHighTrust
340 final var byNumber
= address
.number().isEmpty()
341 ? Optional
.<Recipient
>empty()
342 : findByNumberLocked(address
.number().get());
343 final var byUuid
= address
.uuid().isEmpty()
344 ? Optional
.<Recipient
>empty()
345 : findByUuidLocked(address
.uuid().get());
347 if (byNumber
.isEmpty() && byUuid
.isEmpty()) {
348 logger
.debug("Got new recipient, both uuid and number are unknown");
350 if (isHighTrust
|| address
.uuid().isEmpty() || address
.number().isEmpty()) {
351 return new Pair
<>(addNewRecipientLocked(address
), Optional
.empty());
354 return new Pair
<>(addNewRecipientLocked(new RecipientAddress(address
.uuid().get())), Optional
.empty());
357 if (!isHighTrust
|| address
.uuid().isEmpty() || address
.number().isEmpty() || byNumber
.equals(byUuid
)) {
358 return new Pair
<>(byUuid
.or(() -> byNumber
).map(Recipient
::getRecipientId
).get(), Optional
.empty());
361 if (byNumber
.isEmpty()) {
362 logger
.debug("Got recipient {} existing with uuid, updating with high trust number",
363 byUuid
.get().getRecipientId());
364 updateRecipientAddressLocked(byUuid
.get().getRecipientId(), address
);
365 return new Pair
<>(byUuid
.get().getRecipientId(), Optional
.empty());
368 final var byNumberRecipient
= byNumber
.get();
370 if (byUuid
.isEmpty()) {
371 if (byNumberRecipient
.getAddress().uuid().isPresent()) {
373 "Got recipient {} existing with number, but different uuid, so stripping its number and adding new recipient",
374 byNumberRecipient
.getRecipientId());
376 updateRecipientAddressLocked(byNumberRecipient
.getRecipientId(),
377 new RecipientAddress(byNumberRecipient
.getAddress().uuid().get()));
378 return new Pair
<>(addNewRecipientLocked(address
), Optional
.empty());
381 logger
.debug("Got recipient {} existing with number and no uuid, updating with high trust uuid",
382 byNumberRecipient
.getRecipientId());
383 updateRecipientAddressLocked(byNumberRecipient
.getRecipientId(), address
);
384 return new Pair
<>(byNumberRecipient
.getRecipientId(), Optional
.empty());
387 final var byUuidRecipient
= byUuid
.get();
389 if (byNumberRecipient
.getAddress().uuid().isPresent()) {
391 "Got separate recipients for high trust number {} and uuid {}, recipient for number has different uuid, so stripping its number",
392 byNumberRecipient
.getRecipientId(),
393 byUuidRecipient
.getRecipientId());
395 updateRecipientAddressLocked(byNumberRecipient
.getRecipientId(),
396 new RecipientAddress(byNumberRecipient
.getAddress().uuid().get()));
397 updateRecipientAddressLocked(byUuidRecipient
.getRecipientId(), address
);
398 return new Pair
<>(byUuidRecipient
.getRecipientId(), Optional
.empty());
401 logger
.debug("Got separate recipients for high trust number {} and uuid {}, need to merge them",
402 byNumberRecipient
.getRecipientId(),
403 byUuidRecipient
.getRecipientId());
404 updateRecipientAddressLocked(byUuidRecipient
.getRecipientId(), address
);
405 // Create a fixed RecipientId that won't update its id after merge
406 final var toBeMergedRecipientId
= new RecipientId(byNumberRecipient
.getRecipientId().id(), null);
407 mergeRecipientsLocked(byUuidRecipient
.getRecipientId(), toBeMergedRecipientId
);
408 return new Pair
<>(byUuidRecipient
.getRecipientId(), Optional
.of(toBeMergedRecipientId
));
411 private RecipientId
addNewRecipientLocked(final RecipientAddress address
) {
412 final var nextRecipientId
= nextIdLocked();
413 logger
.debug("Adding new recipient {} with address {}", nextRecipientId
, address
);
414 storeRecipientLocked(nextRecipientId
, new Recipient(nextRecipientId
, address
, null, null, null, null));
415 return nextRecipientId
;
418 private void updateRecipientAddressLocked(RecipientId recipientId
, final RecipientAddress address
) {
419 final var recipient
= recipients
.get(recipientId
);
420 storeRecipientLocked(recipientId
, Recipient
.newBuilder(recipient
).withAddress(address
).build());
423 long getActualRecipientId(long recipientId
) {
424 while (recipientsMerged
.containsKey(recipientId
)) {
425 final var newRecipientId
= recipientsMerged
.get(recipientId
);
426 logger
.debug("Using {} instead of {}, because recipients have been merged", newRecipientId
, recipientId
);
427 recipientId
= newRecipientId
;
432 private void storeRecipientLocked(final RecipientId recipientId
, final Recipient recipient
) {
433 final var existingRecipient
= recipients
.get(recipientId
);
434 if (existingRecipient
== null || !existingRecipient
.equals(recipient
)) {
435 recipients
.put(recipientId
, recipient
);
440 private void mergeRecipientsLocked(RecipientId recipientId
, RecipientId toBeMergedRecipientId
) {
441 final var recipient
= recipients
.get(recipientId
);
442 final var toBeMergedRecipient
= recipients
.get(toBeMergedRecipientId
);
443 recipients
.put(recipientId
,
444 new Recipient(recipientId
,
445 recipient
.getAddress(),
446 recipient
.getContact() != null ? recipient
.getContact() : toBeMergedRecipient
.getContact(),
447 recipient
.getProfileKey() != null
448 ? recipient
.getProfileKey()
449 : toBeMergedRecipient
.getProfileKey(),
450 recipient
.getProfileKeyCredential() != null
451 ? recipient
.getProfileKeyCredential()
452 : toBeMergedRecipient
.getProfileKeyCredential(),
453 recipient
.getProfile() != null ? recipient
.getProfile() : toBeMergedRecipient
.getProfile()));
454 recipients
.remove(toBeMergedRecipientId
);
455 recipientsMerged
.put(toBeMergedRecipientId
.id(), recipientId
.id());
459 private Optional
<Recipient
> findByNumberLocked(final String number
) {
460 return recipients
.entrySet()
462 .filter(entry
-> entry
.getValue().getAddress().number().isPresent() && number
.equals(entry
.getValue()
467 .map(Map
.Entry
::getValue
);
470 private Optional
<Recipient
> findByUuidLocked(final UUID uuid
) {
471 return recipients
.entrySet()
473 .filter(entry
-> entry
.getValue().getAddress().uuid().isPresent() && uuid
.equals(entry
.getValue()
478 .map(Map
.Entry
::getValue
);
481 private RecipientId
nextIdLocked() {
482 return new RecipientId(++this.lastId
, this);
485 private void saveLocked() {
486 final var base64
= Base64
.getEncoder();
487 var storage
= new Storage(recipients
.entrySet().stream().map(pair
-> {
488 final var recipient
= pair
.getValue();
489 final var contact
= recipient
.getContact() == null
491 : new Storage
.Recipient
.Contact(recipient
.getContact().getName(),
492 recipient
.getContact().getColor(),
493 recipient
.getContact().getMessageExpirationTime(),
494 recipient
.getContact().isBlocked(),
495 recipient
.getContact().isArchived());
496 final var profile
= recipient
.getProfile() == null
498 : new Storage
.Recipient
.Profile(recipient
.getProfile().getLastUpdateTimestamp(),
499 recipient
.getProfile().getGivenName(),
500 recipient
.getProfile().getFamilyName(),
501 recipient
.getProfile().getAbout(),
502 recipient
.getProfile().getAboutEmoji(),
503 recipient
.getProfile().getAvatarUrlPath(),
504 recipient
.getProfile().getUnidentifiedAccessMode().name(),
505 recipient
.getProfile()
509 .collect(Collectors
.toSet()));
510 return new Storage
.Recipient(pair
.getKey().id(),
511 recipient
.getAddress().number().orElse(null),
512 recipient
.getAddress().uuid().map(UUID
::toString
).orElse(null),
513 recipient
.getProfileKey() == null
515 : base64
.encodeToString(recipient
.getProfileKey().serialize()),
516 recipient
.getProfileKeyCredential() == null
518 : base64
.encodeToString(recipient
.getProfileKeyCredential().serialize()),
521 }).collect(Collectors
.toList()), lastId
);
523 // Write to memory first to prevent corrupting the file in case of serialization errors
524 try (var inMemoryOutput
= new ByteArrayOutputStream()) {
525 objectMapper
.writeValue(inMemoryOutput
, storage
);
527 var input
= new ByteArrayInputStream(inMemoryOutput
.toByteArray());
528 try (var outputStream
= new FileOutputStream(file
)) {
529 input
.transferTo(outputStream
);
531 } catch (Exception e
) {
532 logger
.error("Error saving recipient store file: {}", e
.getMessage());
536 private record Storage(List
<Recipient
> recipients
, long lastId
) {
538 private record Recipient(
543 String profileKeyCredential
,
544 Storage
.Recipient
.Contact contact
,
545 Storage
.Recipient
.Profile profile
548 private record Contact(
549 String name
, String color
, int messageExpirationTime
, boolean blocked
, boolean archived
552 private record Profile(
553 long lastUpdateTimestamp
,
558 String avatarUrlPath
,
559 String unidentifiedAccessMode
,
560 Set
<String
> capabilities
565 public interface RecipientMergeHandler
{
567 void mergeRecipients(RecipientId recipientId
, RecipientId toBeMergedRecipientId
);