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 Profile
.UnidentifiedAccessMode
.valueOfOrUnknown(r
.profile
.unidentifiedAccessMode
),
93 r
.profile
.capabilities
.stream()
94 .map(Profile
.Capability
::valueOfOrNull
)
95 .filter(Objects
::nonNull
)
96 .collect(Collectors
.toSet()));
99 return new Recipient(recipientId
, address
, contact
, profileKey
, profileKeyCredential
, profile
);
100 }).collect(Collectors
.toMap(Recipient
::getRecipientId
, r
-> r
));
102 return new RecipientStore(objectMapper
, file
, recipientMergeHandler
, recipients
, storage
.lastId
);
103 } catch (FileNotFoundException e
) {
104 logger
.debug("Creating new recipient store.");
105 return new RecipientStore(objectMapper
, file
, recipientMergeHandler
, new HashMap
<>(), 0);
109 private RecipientStore(
110 final ObjectMapper objectMapper
,
112 final RecipientMergeHandler recipientMergeHandler
,
113 final Map
<RecipientId
, Recipient
> recipients
,
116 this.objectMapper
= objectMapper
;
118 this.recipientMergeHandler
= recipientMergeHandler
;
119 this.recipients
= recipients
;
120 this.lastId
= lastId
;
123 public RecipientAddress
resolveRecipientAddress(RecipientId recipientId
) {
124 synchronized (recipients
) {
125 return getRecipient(recipientId
).getAddress();
129 public Recipient
getRecipient(RecipientId recipientId
) {
130 synchronized (recipients
) {
131 while (recipientsMerged
.containsKey(recipientId
)) {
132 recipientId
= recipientsMerged
.get(recipientId
);
134 return recipients
.get(recipientId
);
139 public RecipientId
resolveRecipient(UUID uuid
) {
140 return resolveRecipient(new RecipientAddress(uuid
), false);
144 public RecipientId
resolveRecipient(final String identifier
) {
145 return resolveRecipient(Utils
.getRecipientAddressFromIdentifier(identifier
), false);
148 public RecipientId
resolveRecipient(
149 final String number
, Supplier
<UUID
> uuidSupplier
150 ) throws UnregisteredUserException
{
151 final Optional
<Recipient
> byNumber
;
152 synchronized (recipients
) {
153 byNumber
= findByNumberLocked(number
);
155 if (byNumber
.isEmpty() || byNumber
.get().getAddress().getUuid().isEmpty()) {
156 final var uuid
= uuidSupplier
.get();
158 throw new UnregisteredUserException(number
, null);
161 return resolveRecipient(new RecipientAddress(uuid
, number
), false);
163 return byNumber
.get().getRecipientId();
166 public RecipientId
resolveRecipient(RecipientAddress address
) {
167 return resolveRecipient(address
, false);
171 public RecipientId
resolveRecipient(final SignalServiceAddress address
) {
172 return resolveRecipient(new RecipientAddress(address
), false);
175 public RecipientId
resolveRecipientTrusted(RecipientAddress address
) {
176 return resolveRecipient(address
, true);
179 public RecipientId
resolveRecipientTrusted(SignalServiceAddress address
) {
180 return resolveRecipient(new RecipientAddress(address
), true);
183 public List
<RecipientId
> resolveRecipientsTrusted(List
<RecipientAddress
> addresses
) {
184 final List
<RecipientId
> recipientIds
;
185 final List
<Pair
<RecipientId
, RecipientId
>> toBeMerged
= new ArrayList
<>();
186 synchronized (recipients
) {
187 recipientIds
= addresses
.stream().map(address
-> {
188 final var pair
= resolveRecipientLocked(address
, true);
189 if (pair
.second().isPresent()) {
190 toBeMerged
.add(new Pair
<>(pair
.first(), pair
.second().get()));
193 }).collect(Collectors
.toList());
195 for (var pair
: toBeMerged
) {
196 recipientMergeHandler
.mergeRecipients(pair
.first(), pair
.second());
202 public void storeContact(final RecipientId recipientId
, final Contact contact
) {
203 synchronized (recipients
) {
204 final var recipient
= recipients
.get(recipientId
);
205 storeRecipientLocked(recipientId
, Recipient
.newBuilder(recipient
).withContact(contact
).build());
210 public Contact
getContact(final RecipientId recipientId
) {
211 final var recipient
= getRecipient(recipientId
);
212 return recipient
== null ?
null : recipient
.getContact();
216 public List
<Pair
<RecipientId
, Contact
>> getContacts() {
217 return recipients
.entrySet()
219 .filter(e
-> e
.getValue().getContact() != null)
220 .map(e
-> new Pair
<>(e
.getKey(), e
.getValue().getContact()))
221 .collect(Collectors
.toList());
225 public Profile
getProfile(final RecipientId recipientId
) {
226 final var recipient
= getRecipient(recipientId
);
227 return recipient
== null ?
null : recipient
.getProfile();
231 public ProfileKey
getProfileKey(final RecipientId recipientId
) {
232 final var recipient
= getRecipient(recipientId
);
233 return recipient
== null ?
null : recipient
.getProfileKey();
237 public ProfileKeyCredential
getProfileKeyCredential(final RecipientId recipientId
) {
238 final var recipient
= getRecipient(recipientId
);
239 return recipient
== null ?
null : recipient
.getProfileKeyCredential();
243 public void storeProfile(final RecipientId recipientId
, final Profile profile
) {
244 synchronized (recipients
) {
245 final var recipient
= recipients
.get(recipientId
);
246 storeRecipientLocked(recipientId
, Recipient
.newBuilder(recipient
).withProfile(profile
).build());
251 public void storeProfileKey(final RecipientId recipientId
, final ProfileKey profileKey
) {
252 synchronized (recipients
) {
253 final var recipient
= recipients
.get(recipientId
);
254 if (profileKey
!= null && profileKey
.equals(recipient
.getProfileKey())) {
258 final var newRecipient
= Recipient
.newBuilder(recipient
)
259 .withProfileKey(profileKey
)
260 .withProfileKeyCredential(null)
261 .withProfile(recipient
.getProfile() == null
263 : Profile
.newBuilder(recipient
.getProfile()).withLastUpdateTimestamp(0).build())
265 storeRecipientLocked(recipientId
, newRecipient
);
270 public void storeProfileKeyCredential(
271 final RecipientId recipientId
, final ProfileKeyCredential profileKeyCredential
273 synchronized (recipients
) {
274 final var recipient
= recipients
.get(recipientId
);
275 storeRecipientLocked(recipientId
,
276 Recipient
.newBuilder(recipient
).withProfileKeyCredential(profileKeyCredential
).build());
280 public boolean isEmpty() {
281 synchronized (recipients
) {
282 return recipients
.isEmpty();
287 * @param isHighTrust true, if the number/uuid connection was obtained from a trusted source.
288 * Has no effect, if the address contains only a number or a uuid.
290 private RecipientId
resolveRecipient(RecipientAddress address
, boolean isHighTrust
) {
291 final Pair
<RecipientId
, Optional
<RecipientId
>> pair
;
292 synchronized (recipients
) {
293 pair
= resolveRecipientLocked(address
, isHighTrust
);
294 if (pair
.second().isPresent()) {
295 recipientsMerged
.put(pair
.second().get(), pair
.first());
299 if (pair
.second().isPresent()) {
300 recipientMergeHandler
.mergeRecipients(pair
.first(), pair
.second().get());
305 private Pair
<RecipientId
, Optional
<RecipientId
>> resolveRecipientLocked(
306 RecipientAddress address
, boolean isHighTrust
308 final var byNumber
= address
.getNumber().isEmpty()
309 ? Optional
.<Recipient
>empty()
310 : findByNumberLocked(address
.getNumber().get());
311 final var byUuid
= address
.getUuid().isEmpty() || address
.getUuid().get().equals(UuidUtil
.UNKNOWN_UUID
)
312 ? Optional
.<Recipient
>empty()
313 : findByUuidLocked(address
.getUuid().get());
315 if (byNumber
.isEmpty() && byUuid
.isEmpty()) {
316 logger
.debug("Got new recipient, both uuid and number are unknown");
318 if (isHighTrust
|| address
.getUuid().isEmpty() || address
.getNumber().isEmpty()) {
319 return new Pair
<>(addNewRecipientLocked(address
), Optional
.empty());
322 return new Pair
<>(addNewRecipientLocked(new RecipientAddress(address
.getUuid().get())), Optional
.empty());
325 if (!isHighTrust
|| address
.getUuid().isEmpty() || address
.getNumber().isEmpty() || byNumber
.equals(byUuid
)) {
326 return new Pair
<>(byUuid
.or(() -> byNumber
).map(Recipient
::getRecipientId
).get(), Optional
.empty());
329 if (byNumber
.isEmpty()) {
330 logger
.debug("Got recipient existing with uuid, updating with high trust number");
331 updateRecipientAddressLocked(byUuid
.get().getRecipientId(), address
);
332 return new Pair
<>(byUuid
.get().getRecipientId(), Optional
.empty());
335 if (byUuid
.isEmpty()) {
336 if (byNumber
.get().getAddress().getUuid().isPresent()) {
338 "Got recipient existing with number, but different uuid, so stripping its number and adding new recipient");
340 updateRecipientAddressLocked(byNumber
.get().getRecipientId(),
341 new RecipientAddress(byNumber
.get().getAddress().getUuid().get()));
342 return new Pair
<>(addNewRecipientLocked(address
), Optional
.empty());
345 logger
.debug("Got recipient existing with number and no uuid, updating with high trust uuid");
346 updateRecipientAddressLocked(byNumber
.get().getRecipientId(), address
);
347 return new Pair
<>(byNumber
.get().getRecipientId(), Optional
.empty());
350 if (byNumber
.get().getAddress().getUuid().isPresent()) {
352 "Got separate recipients for high trust number and uuid, recipient for number has different uuid, so stripping its number");
354 updateRecipientAddressLocked(byNumber
.get().getRecipientId(),
355 new RecipientAddress(byNumber
.get().getAddress().getUuid().get()));
356 updateRecipientAddressLocked(byUuid
.get().getRecipientId(), address
);
357 return new Pair
<>(byUuid
.get().getRecipientId(), Optional
.empty());
360 logger
.debug("Got separate recipients for high trust number and uuid, need to merge them");
361 updateRecipientAddressLocked(byUuid
.get().getRecipientId(), address
);
362 mergeRecipientsLocked(byUuid
.get().getRecipientId(), byNumber
.get().getRecipientId());
363 return new Pair
<>(byUuid
.get().getRecipientId(), byNumber
.map(Recipient
::getRecipientId
));
366 private RecipientId
addNewRecipientLocked(final RecipientAddress address
) {
367 final var nextRecipientId
= nextIdLocked();
368 storeRecipientLocked(nextRecipientId
, new Recipient(nextRecipientId
, address
, null, null, null, null));
369 return nextRecipientId
;
372 private void updateRecipientAddressLocked(
373 final RecipientId recipientId
, final RecipientAddress address
375 final var recipient
= recipients
.get(recipientId
);
376 storeRecipientLocked(recipientId
, Recipient
.newBuilder(recipient
).withAddress(address
).build());
379 private void storeRecipientLocked(
380 final RecipientId recipientId
, final Recipient recipient
382 recipients
.put(recipientId
, recipient
);
386 private void mergeRecipientsLocked(RecipientId recipientId
, RecipientId toBeMergedRecipientId
) {
387 final var recipient
= recipients
.get(recipientId
);
388 final var toBeMergedRecipient
= recipients
.get(toBeMergedRecipientId
);
389 recipients
.put(recipientId
,
390 new Recipient(recipientId
,
391 recipient
.getAddress(),
392 recipient
.getContact() != null ? recipient
.getContact() : toBeMergedRecipient
.getContact(),
393 recipient
.getProfileKey() != null
394 ? recipient
.getProfileKey()
395 : toBeMergedRecipient
.getProfileKey(),
396 recipient
.getProfileKeyCredential() != null
397 ? recipient
.getProfileKeyCredential()
398 : toBeMergedRecipient
.getProfileKeyCredential(),
399 recipient
.getProfile() != null ? recipient
.getProfile() : toBeMergedRecipient
.getProfile()));
400 recipients
.remove(toBeMergedRecipientId
);
404 private Optional
<Recipient
> findByNumberLocked(final String number
) {
405 return recipients
.entrySet()
407 .filter(entry
-> entry
.getValue().getAddress().getNumber().isPresent() && number
.equals(entry
.getValue()
412 .map(Map
.Entry
::getValue
);
415 private Optional
<Recipient
> findByUuidLocked(final UUID uuid
) {
416 return recipients
.entrySet()
418 .filter(entry
-> entry
.getValue().getAddress().getUuid().isPresent() && uuid
.equals(entry
.getValue()
423 .map(Map
.Entry
::getValue
);
426 private RecipientId
nextIdLocked() {
427 return new RecipientId(++this.lastId
);
430 private void saveLocked() {
431 final var base64
= Base64
.getEncoder();
432 var storage
= new Storage(recipients
.entrySet().stream().map(pair
-> {
433 final var recipient
= pair
.getValue();
434 final var contact
= recipient
.getContact() == null
436 : new Storage
.Recipient
.Contact(recipient
.getContact().getName(),
437 recipient
.getContact().getColor(),
438 recipient
.getContact().getMessageExpirationTime(),
439 recipient
.getContact().isBlocked(),
440 recipient
.getContact().isArchived());
441 final var profile
= recipient
.getProfile() == null
443 : new Storage
.Recipient
.Profile(recipient
.getProfile().getLastUpdateTimestamp(),
444 recipient
.getProfile().getGivenName(),
445 recipient
.getProfile().getFamilyName(),
446 recipient
.getProfile().getAbout(),
447 recipient
.getProfile().getAboutEmoji(),
448 recipient
.getProfile().getUnidentifiedAccessMode().name(),
449 recipient
.getProfile()
453 .collect(Collectors
.toSet()));
454 return new Storage
.Recipient(pair
.getKey().getId(),
455 recipient
.getAddress().getNumber().orElse(null),
456 recipient
.getAddress().getUuid().map(UUID
::toString
).orElse(null),
457 recipient
.getProfileKey() == null
459 : base64
.encodeToString(recipient
.getProfileKey().serialize()),
460 recipient
.getProfileKeyCredential() == null
462 : base64
.encodeToString(recipient
.getProfileKeyCredential().serialize()),
465 }).collect(Collectors
.toList()), lastId
);
467 // Write to memory first to prevent corrupting the file in case of serialization errors
468 try (var inMemoryOutput
= new ByteArrayOutputStream()) {
469 objectMapper
.writeValue(inMemoryOutput
, storage
);
471 var input
= new ByteArrayInputStream(inMemoryOutput
.toByteArray());
472 try (var outputStream
= new FileOutputStream(file
)) {
473 input
.transferTo(outputStream
);
475 } catch (Exception e
) {
476 logger
.error("Error saving recipient store file: {}", e
.getMessage());
480 private static class Storage
{
482 public List
<Recipient
> recipients
;
486 // For deserialization
490 public Storage(final List
<Recipient
> recipients
, final long lastId
) {
491 this.recipients
= recipients
;
492 this.lastId
= lastId
;
495 private static class Recipient
{
498 public String number
;
500 public String profileKey
;
501 public String profileKeyCredential
;
502 public Contact contact
;
503 public Profile profile
;
505 // For deserialization
506 private Recipient() {
513 final String profileKey
,
514 final String profileKeyCredential
,
515 final Contact contact
,
516 final Profile profile
519 this.number
= number
;
521 this.profileKey
= profileKey
;
522 this.profileKeyCredential
= profileKeyCredential
;
523 this.contact
= contact
;
524 this.profile
= profile
;
527 private static class Contact
{
531 public int messageExpirationTime
;
532 public boolean blocked
;
533 public boolean archived
;
535 // For deserialization
542 final int messageExpirationTime
,
543 final boolean blocked
,
544 final boolean archived
548 this.messageExpirationTime
= messageExpirationTime
;
549 this.blocked
= blocked
;
550 this.archived
= archived
;
554 private static class Profile
{
556 public long lastUpdateTimestamp
;
557 public String givenName
;
558 public String familyName
;
560 public String aboutEmoji
;
561 public String unidentifiedAccessMode
;
562 public Set
<String
> capabilities
;
564 // For deserialization
569 final long lastUpdateTimestamp
,
570 final String givenName
,
571 final String familyName
,
573 final String aboutEmoji
,
574 final String unidentifiedAccessMode
,
575 final Set
<String
> capabilities
577 this.lastUpdateTimestamp
= lastUpdateTimestamp
;
578 this.givenName
= givenName
;
579 this.familyName
= familyName
;
581 this.aboutEmoji
= aboutEmoji
;
582 this.unidentifiedAccessMode
= unidentifiedAccessMode
;
583 this.capabilities
= capabilities
;
589 public interface RecipientMergeHandler
{
591 void mergeRecipients(RecipientId recipientId
, RecipientId toBeMergedRecipientId
);