1 package org
.asamk
.signal
.manager
.storage
.recipients
;
3 import com
.fasterxml
.jackson
.databind
.ObjectMapper
;
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
;
18 import java
.io
.ByteArrayInputStream
;
19 import java
.io
.ByteArrayOutputStream
;
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
;
30 import java
.util
.Objects
;
31 import java
.util
.Optional
;
33 import java
.util
.UUID
;
34 import java
.util
.function
.Supplier
;
35 import java
.util
.stream
.Collectors
;
37 public class RecipientStore
implements RecipientResolver
, ContactsStore
, ProfileStore
{
39 private final static Logger logger
= LoggerFactory
.getLogger(RecipientStore
.class);
41 private final ObjectMapper objectMapper
;
42 private final File file
;
43 private final RecipientMergeHandler recipientMergeHandler
;
45 private final Map
<RecipientId
, Recipient
> recipients
;
46 private final Map
<RecipientId
, RecipientId
> recipientsMerged
= new HashMap
<>();
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
));
59 Contact contact
= null;
60 if (r
.contact
!= null) {
61 contact
= new Contact(r
.contact
.name
,
63 r
.contact
.messageExpirationTime
,
68 ProfileKey profileKey
= null;
69 if (r
.profileKey
!= null) {
71 profileKey
= new ProfileKey(Base64
.getDecoder().decode(r
.profileKey
));
72 } catch (InvalidInputException ignored
) {
76 ProfileKeyCredential profileKeyCredential
= null;
77 if (r
.profileKeyCredential
!= null) {
79 profileKeyCredential
= new ProfileKeyCredential(Base64
.getDecoder()
80 .decode(r
.profileKeyCredential
));
81 } catch (Throwable ignored
) {
85 Profile profile
= null;
86 if (r
.profile
!= null) {
87 profile
= new Profile(r
.profile
.lastUpdateTimestamp
,
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()));
100 return new Recipient(recipientId
, address
, contact
, profileKey
, profileKeyCredential
, profile
);
101 }).collect(Collectors
.toMap(Recipient
::getRecipientId
, r
-> r
));
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);
110 private RecipientStore(
111 final ObjectMapper objectMapper
,
113 final RecipientMergeHandler recipientMergeHandler
,
114 final Map
<RecipientId
, Recipient
> recipients
,
117 this.objectMapper
= objectMapper
;
119 this.recipientMergeHandler
= recipientMergeHandler
;
120 this.recipients
= recipients
;
121 this.lastId
= lastId
;
124 public RecipientAddress
resolveRecipientAddress(RecipientId recipientId
) {
125 synchronized (recipients
) {
126 return getRecipient(recipientId
).getAddress();
130 public Recipient
getRecipient(RecipientId recipientId
) {
131 synchronized (recipients
) {
132 while (recipientsMerged
.containsKey(recipientId
)) {
133 recipientId
= recipientsMerged
.get(recipientId
);
135 return recipients
.get(recipientId
);
140 public RecipientId
resolveRecipient(UUID uuid
) {
141 return resolveRecipient(new RecipientAddress(uuid
), false);
145 public RecipientId
resolveRecipient(final String identifier
) {
146 return resolveRecipient(Utils
.getRecipientAddressFromIdentifier(identifier
), false);
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
);
156 if (byNumber
.isEmpty() || byNumber
.get().getAddress().getUuid().isEmpty()) {
157 final var uuid
= uuidSupplier
.get();
159 throw new UnregisteredUserException(number
, null);
162 return resolveRecipient(new RecipientAddress(uuid
, number
), false);
164 return byNumber
.get().getRecipientId();
167 public RecipientId
resolveRecipient(RecipientAddress address
) {
168 return resolveRecipient(address
, false);
172 public RecipientId
resolveRecipient(final SignalServiceAddress address
) {
173 return resolveRecipient(new RecipientAddress(address
), false);
176 public RecipientId
resolveRecipientTrusted(RecipientAddress address
) {
177 return resolveRecipient(address
, true);
180 public RecipientId
resolveRecipientTrusted(SignalServiceAddress address
) {
181 return resolveRecipient(new RecipientAddress(address
), true);
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()));
194 }).collect(Collectors
.toList());
196 for (var pair
: toBeMerged
) {
197 recipientMergeHandler
.mergeRecipients(pair
.first(), pair
.second());
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());
211 public Contact
getContact(final RecipientId recipientId
) {
212 final var recipient
= getRecipient(recipientId
);
213 return recipient
== null ?
null : recipient
.getContact();
217 public List
<Pair
<RecipientId
, Contact
>> getContacts() {
218 return recipients
.entrySet()
220 .filter(e
-> e
.getValue().getContact() != null)
221 .map(e
-> new Pair
<>(e
.getKey(), e
.getValue().getContact()))
222 .collect(Collectors
.toList());
226 public Profile
getProfile(final RecipientId recipientId
) {
227 final var recipient
= getRecipient(recipientId
);
228 return recipient
== null ?
null : recipient
.getProfile();
232 public ProfileKey
getProfileKey(final RecipientId recipientId
) {
233 final var recipient
= getRecipient(recipientId
);
234 return recipient
== null ?
null : recipient
.getProfileKey();
238 public ProfileKeyCredential
getProfileKeyCredential(final RecipientId recipientId
) {
239 final var recipient
= getRecipient(recipientId
);
240 return recipient
== null ?
null : recipient
.getProfileKeyCredential();
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());
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())) {
259 final var newRecipient
= Recipient
.newBuilder(recipient
)
260 .withProfileKey(profileKey
)
261 .withProfileKeyCredential(null)
262 .withProfile(recipient
.getProfile() == null
264 : Profile
.newBuilder(recipient
.getProfile()).withLastUpdateTimestamp(0).build())
266 storeRecipientLocked(recipientId
, newRecipient
);
271 public void storeProfileKeyCredential(
272 final RecipientId recipientId
, final ProfileKeyCredential profileKeyCredential
274 synchronized (recipients
) {
275 final var recipient
= recipients
.get(recipientId
);
276 storeRecipientLocked(recipientId
,
277 Recipient
.newBuilder(recipient
).withProfileKeyCredential(profileKeyCredential
).build());
281 public boolean isEmpty() {
282 synchronized (recipients
) {
283 return recipients
.isEmpty();
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.
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());
300 if (pair
.second().isPresent()) {
301 recipientMergeHandler
.mergeRecipients(pair
.first(), pair
.second().get());
306 private Pair
<RecipientId
, Optional
<RecipientId
>> resolveRecipientLocked(
307 RecipientAddress address
, boolean isHighTrust
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());
316 if (byNumber
.isEmpty() && byUuid
.isEmpty()) {
317 logger
.debug("Got new recipient, both uuid and number are unknown");
319 if (isHighTrust
|| address
.getUuid().isEmpty() || address
.getNumber().isEmpty()) {
320 return new Pair
<>(addNewRecipientLocked(address
), Optional
.empty());
323 return new Pair
<>(addNewRecipientLocked(new RecipientAddress(address
.getUuid().get())), Optional
.empty());
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());
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());
336 if (byUuid
.isEmpty()) {
337 if (byNumber
.get().getAddress().getUuid().isPresent()) {
339 "Got recipient existing with number, but different uuid, so stripping its number and adding new recipient");
341 updateRecipientAddressLocked(byNumber
.get().getRecipientId(),
342 new RecipientAddress(byNumber
.get().getAddress().getUuid().get()));
343 return new Pair
<>(addNewRecipientLocked(address
), Optional
.empty());
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());
351 if (byNumber
.get().getAddress().getUuid().isPresent()) {
353 "Got separate recipients for high trust number and uuid, recipient for number has different uuid, so stripping its number");
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());
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
));
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
;
373 private void updateRecipientAddressLocked(
374 final RecipientId recipientId
, final RecipientAddress address
376 final var recipient
= recipients
.get(recipientId
);
377 storeRecipientLocked(recipientId
, Recipient
.newBuilder(recipient
).withAddress(address
).build());
380 private void storeRecipientLocked(
381 final RecipientId recipientId
, final Recipient recipient
383 recipients
.put(recipientId
, recipient
);
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
);
405 private Optional
<Recipient
> findByNumberLocked(final String number
) {
406 return recipients
.entrySet()
408 .filter(entry
-> entry
.getValue().getAddress().getNumber().isPresent() && number
.equals(entry
.getValue()
413 .map(Map
.Entry
::getValue
);
416 private Optional
<Recipient
> findByUuidLocked(final UUID uuid
) {
417 return recipients
.entrySet()
419 .filter(entry
-> entry
.getValue().getAddress().getUuid().isPresent() && uuid
.equals(entry
.getValue()
424 .map(Map
.Entry
::getValue
);
427 private RecipientId
nextIdLocked() {
428 return new RecipientId(++this.lastId
);
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
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
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()
455 .collect(Collectors
.toSet()));
456 return new Storage
.Recipient(pair
.getKey().getId(),
457 recipient
.getAddress().getNumber().orElse(null),
458 recipient
.getAddress().getUuid().map(UUID
::toString
).orElse(null),
459 recipient
.getProfileKey() == null
461 : base64
.encodeToString(recipient
.getProfileKey().serialize()),
462 recipient
.getProfileKeyCredential() == null
464 : base64
.encodeToString(recipient
.getProfileKeyCredential().serialize()),
467 }).collect(Collectors
.toList()), lastId
);
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
);
473 var input
= new ByteArrayInputStream(inMemoryOutput
.toByteArray());
474 try (var outputStream
= new FileOutputStream(file
)) {
475 input
.transferTo(outputStream
);
477 } catch (Exception e
) {
478 logger
.error("Error saving recipient store file: {}", e
.getMessage());
482 private static class Storage
{
484 public List
<Recipient
> recipients
;
488 // For deserialization
492 public Storage(final List
<Recipient
> recipients
, final long lastId
) {
493 this.recipients
= recipients
;
494 this.lastId
= lastId
;
497 private static class Recipient
{
500 public String number
;
502 public String profileKey
;
503 public String profileKeyCredential
;
504 public Contact contact
;
505 public Profile profile
;
507 // For deserialization
508 private Recipient() {
515 final String profileKey
,
516 final String profileKeyCredential
,
517 final Contact contact
,
518 final Profile profile
521 this.number
= number
;
523 this.profileKey
= profileKey
;
524 this.profileKeyCredential
= profileKeyCredential
;
525 this.contact
= contact
;
526 this.profile
= profile
;
529 private static class Contact
{
533 public int messageExpirationTime
;
534 public boolean blocked
;
535 public boolean archived
;
537 // For deserialization
544 final int messageExpirationTime
,
545 final boolean blocked
,
546 final boolean archived
550 this.messageExpirationTime
= messageExpirationTime
;
551 this.blocked
= blocked
;
552 this.archived
= archived
;
556 private static class Profile
{
558 public long lastUpdateTimestamp
;
559 public String givenName
;
560 public String familyName
;
562 public String aboutEmoji
;
563 public String avatarUrlPath
;
564 public String unidentifiedAccessMode
;
565 public Set
<String
> capabilities
;
567 // For deserialization
572 final long lastUpdateTimestamp
,
573 final String givenName
,
574 final String familyName
,
576 final String aboutEmoji
,
577 final String avatarUrlPath
,
578 final String unidentifiedAccessMode
,
579 final Set
<String
> capabilities
581 this.lastUpdateTimestamp
= lastUpdateTimestamp
;
582 this.givenName
= givenName
;
583 this.familyName
= familyName
;
585 this.aboutEmoji
= aboutEmoji
;
586 this.avatarUrlPath
= avatarUrlPath
;
587 this.unidentifiedAccessMode
= unidentifiedAccessMode
;
588 this.capabilities
= capabilities
;
594 public interface RecipientMergeHandler
{
596 void mergeRecipients(RecipientId recipientId
, RecipientId toBeMergedRecipientId
);