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
.util
.UuidUtil
;
17 import java
.io
.ByteArrayInputStream
;
18 import java
.io
.ByteArrayOutputStream
;
20 import java
.io
.FileInputStream
;
21 import java
.io
.FileNotFoundException
;
22 import java
.io
.FileOutputStream
;
23 import java
.io
.IOException
;
24 import java
.util
.ArrayList
;
25 import java
.util
.Base64
;
26 import java
.util
.HashMap
;
27 import java
.util
.List
;
29 import java
.util
.Objects
;
30 import java
.util
.Optional
;
32 import java
.util
.UUID
;
33 import java
.util
.stream
.Collectors
;
35 public class RecipientStore
implements ContactsStore
, ProfileStore
{
37 private final static Logger logger
= LoggerFactory
.getLogger(RecipientStore
.class);
39 private final ObjectMapper objectMapper
;
40 private final File file
;
41 private final RecipientMergeHandler recipientMergeHandler
;
43 private final Map
<RecipientId
, Recipient
> recipients
;
44 private final Map
<RecipientId
, RecipientId
> recipientsMerged
= new HashMap
<>();
48 public static RecipientStore
load(File file
, RecipientMergeHandler recipientMergeHandler
) throws IOException
{
49 final var objectMapper
= Utils
.createStorageObjectMapper();
50 try (var inputStream
= new FileInputStream(file
)) {
51 final var storage
= objectMapper
.readValue(inputStream
, Storage
.class);
52 final var recipients
= storage
.recipients
.stream().map(r
-> {
53 final var recipientId
= new RecipientId(r
.id
);
54 final var address
= new SignalServiceAddress(org
.whispersystems
.libsignal
.util
.guava
.Optional
.fromNullable(
55 r
.uuid
).transform(UuidUtil
::parseOrThrow
),
56 org
.whispersystems
.libsignal
.util
.guava
.Optional
.fromNullable(r
.number
));
58 Contact contact
= null;
59 if (r
.contact
!= null) {
60 contact
= new Contact(r
.contact
.name
,
62 r
.contact
.messageExpirationTime
,
67 ProfileKey profileKey
= null;
68 if (r
.profileKey
!= null) {
70 profileKey
= new ProfileKey(Base64
.getDecoder().decode(r
.profileKey
));
71 } catch (InvalidInputException ignored
) {
75 ProfileKeyCredential profileKeyCredential
= null;
76 if (r
.profileKeyCredential
!= null) {
78 profileKeyCredential
= new ProfileKeyCredential(Base64
.getDecoder()
79 .decode(r
.profileKeyCredential
));
80 } catch (Throwable ignored
) {
84 Profile profile
= null;
85 if (r
.profile
!= null) {
86 profile
= new Profile(r
.profile
.lastUpdateTimestamp
,
91 Profile
.UnidentifiedAccessMode
.valueOfOrUnknown(r
.profile
.unidentifiedAccessMode
),
92 r
.profile
.capabilities
.stream()
93 .map(Profile
.Capability
::valueOfOrNull
)
94 .filter(Objects
::nonNull
)
95 .collect(Collectors
.toSet()));
98 return new Recipient(recipientId
, address
, contact
, profileKey
, profileKeyCredential
, profile
);
99 }).collect(Collectors
.toMap(Recipient
::getRecipientId
, r
-> r
));
101 return new RecipientStore(objectMapper
, file
, recipientMergeHandler
, recipients
, storage
.lastId
);
102 } catch (FileNotFoundException e
) {
103 logger
.debug("Creating new recipient store.");
104 return new RecipientStore(objectMapper
, file
, recipientMergeHandler
, new HashMap
<>(), 0);
108 private RecipientStore(
109 final ObjectMapper objectMapper
,
111 final RecipientMergeHandler recipientMergeHandler
,
112 final Map
<RecipientId
, Recipient
> recipients
,
115 this.objectMapper
= objectMapper
;
117 this.recipientMergeHandler
= recipientMergeHandler
;
118 this.recipients
= recipients
;
119 this.lastId
= lastId
;
122 public SignalServiceAddress
resolveServiceAddress(RecipientId recipientId
) {
123 synchronized (recipients
) {
124 return getRecipient(recipientId
).getAddress();
128 public Recipient
getRecipient(RecipientId recipientId
) {
129 synchronized (recipients
) {
130 while (recipientsMerged
.containsKey(recipientId
)) {
131 recipientId
= recipientsMerged
.get(recipientId
);
133 return recipients
.get(recipientId
);
138 public SignalServiceAddress
resolveServiceAddress(SignalServiceAddress address
) {
139 return resolveServiceAddress(resolveRecipient(address
, false));
142 public RecipientId
resolveRecipient(UUID uuid
) {
143 return resolveRecipient(new SignalServiceAddress(uuid
, null), false);
146 public RecipientId
resolveRecipient(String number
) {
147 return resolveRecipient(new SignalServiceAddress(null, number
), false);
150 public RecipientId
resolveRecipientTrusted(SignalServiceAddress address
) {
151 return resolveRecipient(address
, true);
154 public List
<RecipientId
> resolveRecipientsTrusted(List
<SignalServiceAddress
> addresses
) {
155 final List
<RecipientId
> recipientIds
;
156 final List
<Pair
<RecipientId
, RecipientId
>> toBeMerged
= new ArrayList
<>();
157 synchronized (recipients
) {
158 recipientIds
= addresses
.stream().map(address
-> {
159 final var pair
= resolveRecipientLocked(address
, true);
160 if (pair
.second().isPresent()) {
161 toBeMerged
.add(new Pair
<>(pair
.first(), pair
.second().get()));
164 }).collect(Collectors
.toList());
166 for (var pair
: toBeMerged
) {
167 recipientMergeHandler
.mergeRecipients(pair
.first(), pair
.second());
172 public RecipientId
resolveRecipient(SignalServiceAddress address
) {
173 return resolveRecipient(address
, false);
177 public void storeContact(final RecipientId recipientId
, final Contact contact
) {
178 synchronized (recipients
) {
179 final var recipient
= recipients
.get(recipientId
);
180 storeRecipientLocked(recipientId
, Recipient
.newBuilder(recipient
).withContact(contact
).build());
185 public Contact
getContact(final RecipientId recipientId
) {
186 final var recipient
= getRecipient(recipientId
);
187 return recipient
== null ?
null : recipient
.getContact();
191 public List
<Pair
<RecipientId
, Contact
>> getContacts() {
192 return recipients
.entrySet()
194 .filter(e
-> e
.getValue().getContact() != null)
195 .map(e
-> new Pair
<>(e
.getKey(), e
.getValue().getContact()))
196 .collect(Collectors
.toList());
200 public Profile
getProfile(final RecipientId recipientId
) {
201 final var recipient
= getRecipient(recipientId
);
202 return recipient
== null ?
null : recipient
.getProfile();
206 public ProfileKey
getProfileKey(final RecipientId recipientId
) {
207 final var recipient
= getRecipient(recipientId
);
208 return recipient
== null ?
null : recipient
.getProfileKey();
212 public ProfileKeyCredential
getProfileKeyCredential(final RecipientId recipientId
) {
213 final var recipient
= getRecipient(recipientId
);
214 return recipient
== null ?
null : recipient
.getProfileKeyCredential();
218 public void storeProfile(final RecipientId recipientId
, final Profile profile
) {
219 synchronized (recipients
) {
220 final var recipient
= recipients
.get(recipientId
);
221 storeRecipientLocked(recipientId
, Recipient
.newBuilder(recipient
).withProfile(profile
).build());
226 public void storeProfileKey(final RecipientId recipientId
, final ProfileKey profileKey
) {
227 synchronized (recipients
) {
228 final var recipient
= recipients
.get(recipientId
);
229 if (profileKey
.equals(recipient
.getProfileKey())) {
233 final var newRecipient
= Recipient
.newBuilder(recipient
)
234 .withProfileKey(profileKey
)
235 .withProfileKeyCredential(null)
237 storeRecipientLocked(recipientId
, newRecipient
);
242 public void storeProfileKeyCredential(
243 final RecipientId recipientId
, final ProfileKeyCredential profileKeyCredential
245 synchronized (recipients
) {
246 final var recipient
= recipients
.get(recipientId
);
247 storeRecipientLocked(recipientId
,
248 Recipient
.newBuilder(recipient
).withProfileKeyCredential(profileKeyCredential
).build());
252 public boolean isEmpty() {
253 synchronized (recipients
) {
254 return recipients
.isEmpty();
259 * @param isHighTrust true, if the number/uuid connection was obtained from a trusted source.
260 * Has no effect, if the address contains only a number or a uuid.
262 private RecipientId
resolveRecipient(SignalServiceAddress address
, boolean isHighTrust
) {
263 final Pair
<RecipientId
, Optional
<RecipientId
>> pair
;
264 synchronized (recipients
) {
265 pair
= resolveRecipientLocked(address
, isHighTrust
);
266 if (pair
.second().isPresent()) {
267 recipientsMerged
.put(pair
.second().get(), pair
.first());
271 if (pair
.second().isPresent()) {
272 recipientMergeHandler
.mergeRecipients(pair
.first(), pair
.second().get());
277 private Pair
<RecipientId
, Optional
<RecipientId
>> resolveRecipientLocked(
278 SignalServiceAddress address
, boolean isHighTrust
280 final var byNumber
= !address
.getNumber().isPresent()
281 ? Optional
.<Recipient
>empty()
282 : findByNameLocked(address
.getNumber().get());
283 final var byUuid
= !address
.getUuid().isPresent()
284 ? Optional
.<Recipient
>empty()
285 : findByUuidLocked(address
.getUuid().get());
287 if (byNumber
.isEmpty() && byUuid
.isEmpty()) {
288 logger
.debug("Got new recipient, both uuid and number are unknown");
290 if (isHighTrust
|| !address
.getUuid().isPresent() || !address
.getNumber().isPresent()) {
291 return new Pair
<>(addNewRecipientLocked(address
), Optional
.empty());
294 return new Pair
<>(addNewRecipientLocked(new SignalServiceAddress(address
.getUuid().get(), null)),
299 || !address
.getUuid().isPresent()
300 || !address
.getNumber().isPresent()
301 || byNumber
.equals(byUuid
)) {
302 return new Pair
<>(byUuid
.or(() -> byNumber
).map(Recipient
::getRecipientId
).get(), Optional
.empty());
305 if (byNumber
.isEmpty()) {
306 logger
.debug("Got recipient existing with uuid, updating with high trust number");
307 updateRecipientAddressLocked(byUuid
.get().getRecipientId(), address
);
308 return new Pair
<>(byUuid
.get().getRecipientId(), Optional
.empty());
311 if (byUuid
.isEmpty()) {
312 if (byNumber
.get().getAddress().getUuid().isPresent()) {
314 "Got recipient existing with number, but different uuid, so stripping its number and adding new recipient");
316 updateRecipientAddressLocked(byNumber
.get().getRecipientId(),
317 new SignalServiceAddress(byNumber
.get().getAddress().getUuid().get(), null));
318 return new Pair
<>(addNewRecipientLocked(address
), Optional
.empty());
321 logger
.debug("Got recipient existing with number and no uuid, updating with high trust uuid");
322 updateRecipientAddressLocked(byNumber
.get().getRecipientId(), address
);
323 return new Pair
<>(byNumber
.get().getRecipientId(), Optional
.empty());
326 if (byNumber
.get().getAddress().getUuid().isPresent()) {
328 "Got separate recipients for high trust number and uuid, recipient for number has different uuid, so stripping its number");
330 updateRecipientAddressLocked(byNumber
.get().getRecipientId(),
331 new SignalServiceAddress(byNumber
.get().getAddress().getUuid().get(), null));
332 updateRecipientAddressLocked(byUuid
.get().getRecipientId(), address
);
333 return new Pair
<>(byUuid
.get().getRecipientId(), Optional
.empty());
336 logger
.debug("Got separate recipients for high trust number and uuid, need to merge them");
337 updateRecipientAddressLocked(byUuid
.get().getRecipientId(), address
);
338 mergeRecipientsLocked(byUuid
.get().getRecipientId(), byNumber
.get().getRecipientId());
339 return new Pair
<>(byUuid
.get().getRecipientId(), byNumber
.map(Recipient
::getRecipientId
));
342 private RecipientId
addNewRecipientLocked(final SignalServiceAddress serviceAddress
) {
343 final var nextRecipientId
= nextIdLocked();
344 storeRecipientLocked(nextRecipientId
, new Recipient(nextRecipientId
, serviceAddress
, null, null, null, null));
345 return nextRecipientId
;
348 private void updateRecipientAddressLocked(
349 final RecipientId recipientId
, final SignalServiceAddress address
351 final var recipient
= recipients
.get(recipientId
);
352 storeRecipientLocked(recipientId
, Recipient
.newBuilder(recipient
).withAddress(address
).build());
355 private void storeRecipientLocked(
356 final RecipientId recipientId
, final Recipient recipient
358 recipients
.put(recipientId
, recipient
);
362 private void mergeRecipientsLocked(RecipientId recipientId
, RecipientId toBeMergedRecipientId
) {
363 final var recipient
= recipients
.get(recipientId
);
364 final var toBeMergedRecipient
= recipients
.get(toBeMergedRecipientId
);
365 recipients
.put(recipientId
,
366 new Recipient(recipientId
,
367 recipient
.getAddress(),
368 recipient
.getContact() != null ? recipient
.getContact() : toBeMergedRecipient
.getContact(),
369 recipient
.getProfileKey() != null
370 ? recipient
.getProfileKey()
371 : toBeMergedRecipient
.getProfileKey(),
372 recipient
.getProfileKeyCredential() != null
373 ? recipient
.getProfileKeyCredential()
374 : toBeMergedRecipient
.getProfileKeyCredential(),
375 recipient
.getProfile() != null ? recipient
.getProfile() : toBeMergedRecipient
.getProfile()));
376 recipients
.remove(toBeMergedRecipientId
);
380 private Optional
<Recipient
> findByNameLocked(final String number
) {
381 return recipients
.entrySet()
383 .filter(entry
-> entry
.getValue().getAddress().getNumber().isPresent() && number
.equals(entry
.getValue()
388 .map(Map
.Entry
::getValue
);
391 private Optional
<Recipient
> findByUuidLocked(final UUID uuid
) {
392 return recipients
.entrySet()
394 .filter(entry
-> entry
.getValue().getAddress().getUuid().isPresent() && uuid
.equals(entry
.getValue()
399 .map(Map
.Entry
::getValue
);
402 private RecipientId
nextIdLocked() {
403 return new RecipientId(++this.lastId
);
406 private void saveLocked() {
407 final var base64
= Base64
.getEncoder();
408 var storage
= new Storage(recipients
.entrySet().stream().map(pair
-> {
409 final var recipient
= pair
.getValue();
410 final var contact
= recipient
.getContact() == null
412 : new Storage
.Recipient
.Contact(recipient
.getContact().getName(),
413 recipient
.getContact().getColor(),
414 recipient
.getContact().getMessageExpirationTime(),
415 recipient
.getContact().isBlocked(),
416 recipient
.getContact().isArchived());
417 final var profile
= recipient
.getProfile() == null
419 : new Storage
.Recipient
.Profile(recipient
.getProfile().getLastUpdateTimestamp(),
420 recipient
.getProfile().getGivenName(),
421 recipient
.getProfile().getFamilyName(),
422 recipient
.getProfile().getAbout(),
423 recipient
.getProfile().getAboutEmoji(),
424 recipient
.getProfile().getUnidentifiedAccessMode().name(),
425 recipient
.getProfile()
429 .collect(Collectors
.toSet()));
430 return new Storage
.Recipient(pair
.getKey().getId(),
431 recipient
.getAddress().getNumber().orNull(),
432 recipient
.getAddress().getUuid().transform(UUID
::toString
).orNull(),
433 recipient
.getProfileKey() == null
435 : base64
.encodeToString(recipient
.getProfileKey().serialize()),
436 recipient
.getProfileKeyCredential() == null
438 : base64
.encodeToString(recipient
.getProfileKeyCredential().serialize()),
441 }).collect(Collectors
.toList()), lastId
);
443 // Write to memory first to prevent corrupting the file in case of serialization errors
444 try (var inMemoryOutput
= new ByteArrayOutputStream()) {
445 objectMapper
.writeValue(inMemoryOutput
, storage
);
447 var input
= new ByteArrayInputStream(inMemoryOutput
.toByteArray());
448 try (var outputStream
= new FileOutputStream(file
)) {
449 input
.transferTo(outputStream
);
451 } catch (Exception e
) {
452 logger
.error("Error saving recipient store file: {}", e
.getMessage());
456 private static class Storage
{
458 public List
<Recipient
> recipients
;
462 // For deserialization
466 public Storage(final List
<Recipient
> recipients
, final long lastId
) {
467 this.recipients
= recipients
;
468 this.lastId
= lastId
;
471 private static class Recipient
{
474 public String number
;
476 public String profileKey
;
477 public String profileKeyCredential
;
478 public Contact contact
;
479 public Profile profile
;
481 // For deserialization
482 private Recipient() {
489 final String profileKey
,
490 final String profileKeyCredential
,
491 final Contact contact
,
492 final Profile profile
495 this.number
= number
;
497 this.profileKey
= profileKey
;
498 this.profileKeyCredential
= profileKeyCredential
;
499 this.contact
= contact
;
500 this.profile
= profile
;
503 private static class Contact
{
507 public int messageExpirationTime
;
508 public boolean blocked
;
509 public boolean archived
;
511 // For deserialization
518 final int messageExpirationTime
,
519 final boolean blocked
,
520 final boolean archived
524 this.messageExpirationTime
= messageExpirationTime
;
525 this.blocked
= blocked
;
526 this.archived
= archived
;
530 private static class Profile
{
532 public long lastUpdateTimestamp
;
533 public String givenName
;
534 public String familyName
;
536 public String aboutEmoji
;
537 public String unidentifiedAccessMode
;
538 public Set
<String
> capabilities
;
540 // For deserialization
545 final long lastUpdateTimestamp
,
546 final String givenName
,
547 final String familyName
,
549 final String aboutEmoji
,
550 final String unidentifiedAccessMode
,
551 final Set
<String
> capabilities
553 this.lastUpdateTimestamp
= lastUpdateTimestamp
;
554 this.givenName
= givenName
;
555 this.familyName
= familyName
;
557 this.aboutEmoji
= aboutEmoji
;
558 this.unidentifiedAccessMode
= unidentifiedAccessMode
;
559 this.capabilities
= capabilities
;
565 public interface RecipientMergeHandler
{
567 void mergeRecipients(RecipientId recipientId
, RecipientId toBeMergedRecipientId
);