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
!= null && profileKey
.equals(recipient
.getProfileKey())) {
233 final var newRecipient
= Recipient
.newBuilder(recipient
)
234 .withProfileKey(profileKey
)
235 .withProfileKeyCredential(null)
236 .withProfile(recipient
.getProfile() == null
238 : Profile
.newBuilder(recipient
.getProfile()).withLastUpdateTimestamp(0).build())
240 storeRecipientLocked(recipientId
, newRecipient
);
245 public void storeProfileKeyCredential(
246 final RecipientId recipientId
, final ProfileKeyCredential profileKeyCredential
248 synchronized (recipients
) {
249 final var recipient
= recipients
.get(recipientId
);
250 storeRecipientLocked(recipientId
,
251 Recipient
.newBuilder(recipient
).withProfileKeyCredential(profileKeyCredential
).build());
255 public boolean isEmpty() {
256 synchronized (recipients
) {
257 return recipients
.isEmpty();
262 * @param isHighTrust true, if the number/uuid connection was obtained from a trusted source.
263 * Has no effect, if the address contains only a number or a uuid.
265 private RecipientId
resolveRecipient(SignalServiceAddress address
, boolean isHighTrust
) {
266 final Pair
<RecipientId
, Optional
<RecipientId
>> pair
;
267 synchronized (recipients
) {
268 pair
= resolveRecipientLocked(address
, isHighTrust
);
269 if (pair
.second().isPresent()) {
270 recipientsMerged
.put(pair
.second().get(), pair
.first());
274 if (pair
.second().isPresent()) {
275 recipientMergeHandler
.mergeRecipients(pair
.first(), pair
.second().get());
280 private Pair
<RecipientId
, Optional
<RecipientId
>> resolveRecipientLocked(
281 SignalServiceAddress address
, boolean isHighTrust
283 final var byNumber
= !address
.getNumber().isPresent()
284 ? Optional
.<Recipient
>empty()
285 : findByNameLocked(address
.getNumber().get());
286 final var byUuid
= !address
.getUuid().isPresent()
287 ? Optional
.<Recipient
>empty()
288 : findByUuidLocked(address
.getUuid().get());
290 if (byNumber
.isEmpty() && byUuid
.isEmpty()) {
291 logger
.debug("Got new recipient, both uuid and number are unknown");
293 if (isHighTrust
|| !address
.getUuid().isPresent() || !address
.getNumber().isPresent()) {
294 return new Pair
<>(addNewRecipientLocked(address
), Optional
.empty());
297 return new Pair
<>(addNewRecipientLocked(new SignalServiceAddress(address
.getUuid().get(), null)),
302 || !address
.getUuid().isPresent()
303 || !address
.getNumber().isPresent()
304 || byNumber
.equals(byUuid
)) {
305 return new Pair
<>(byUuid
.or(() -> byNumber
).map(Recipient
::getRecipientId
).get(), Optional
.empty());
308 if (byNumber
.isEmpty()) {
309 logger
.debug("Got recipient existing with uuid, updating with high trust number");
310 updateRecipientAddressLocked(byUuid
.get().getRecipientId(), address
);
311 return new Pair
<>(byUuid
.get().getRecipientId(), Optional
.empty());
314 if (byUuid
.isEmpty()) {
315 if (byNumber
.get().getAddress().getUuid().isPresent()) {
317 "Got recipient existing with number, but different uuid, so stripping its number and adding new recipient");
319 updateRecipientAddressLocked(byNumber
.get().getRecipientId(),
320 new SignalServiceAddress(byNumber
.get().getAddress().getUuid().get(), null));
321 return new Pair
<>(addNewRecipientLocked(address
), Optional
.empty());
324 logger
.debug("Got recipient existing with number and no uuid, updating with high trust uuid");
325 updateRecipientAddressLocked(byNumber
.get().getRecipientId(), address
);
326 return new Pair
<>(byNumber
.get().getRecipientId(), Optional
.empty());
329 if (byNumber
.get().getAddress().getUuid().isPresent()) {
331 "Got separate recipients for high trust number and uuid, recipient for number has different uuid, so stripping its number");
333 updateRecipientAddressLocked(byNumber
.get().getRecipientId(),
334 new SignalServiceAddress(byNumber
.get().getAddress().getUuid().get(), null));
335 updateRecipientAddressLocked(byUuid
.get().getRecipientId(), address
);
336 return new Pair
<>(byUuid
.get().getRecipientId(), Optional
.empty());
339 logger
.debug("Got separate recipients for high trust number and uuid, need to merge them");
340 updateRecipientAddressLocked(byUuid
.get().getRecipientId(), address
);
341 mergeRecipientsLocked(byUuid
.get().getRecipientId(), byNumber
.get().getRecipientId());
342 return new Pair
<>(byUuid
.get().getRecipientId(), byNumber
.map(Recipient
::getRecipientId
));
345 private RecipientId
addNewRecipientLocked(final SignalServiceAddress serviceAddress
) {
346 final var nextRecipientId
= nextIdLocked();
347 storeRecipientLocked(nextRecipientId
, new Recipient(nextRecipientId
, serviceAddress
, null, null, null, null));
348 return nextRecipientId
;
351 private void updateRecipientAddressLocked(
352 final RecipientId recipientId
, final SignalServiceAddress address
354 final var recipient
= recipients
.get(recipientId
);
355 storeRecipientLocked(recipientId
, Recipient
.newBuilder(recipient
).withAddress(address
).build());
358 private void storeRecipientLocked(
359 final RecipientId recipientId
, final Recipient recipient
361 recipients
.put(recipientId
, recipient
);
365 private void mergeRecipientsLocked(RecipientId recipientId
, RecipientId toBeMergedRecipientId
) {
366 final var recipient
= recipients
.get(recipientId
);
367 final var toBeMergedRecipient
= recipients
.get(toBeMergedRecipientId
);
368 recipients
.put(recipientId
,
369 new Recipient(recipientId
,
370 recipient
.getAddress(),
371 recipient
.getContact() != null ? recipient
.getContact() : toBeMergedRecipient
.getContact(),
372 recipient
.getProfileKey() != null
373 ? recipient
.getProfileKey()
374 : toBeMergedRecipient
.getProfileKey(),
375 recipient
.getProfileKeyCredential() != null
376 ? recipient
.getProfileKeyCredential()
377 : toBeMergedRecipient
.getProfileKeyCredential(),
378 recipient
.getProfile() != null ? recipient
.getProfile() : toBeMergedRecipient
.getProfile()));
379 recipients
.remove(toBeMergedRecipientId
);
383 private Optional
<Recipient
> findByNameLocked(final String number
) {
384 return recipients
.entrySet()
386 .filter(entry
-> entry
.getValue().getAddress().getNumber().isPresent() && number
.equals(entry
.getValue()
391 .map(Map
.Entry
::getValue
);
394 private Optional
<Recipient
> findByUuidLocked(final UUID uuid
) {
395 return recipients
.entrySet()
397 .filter(entry
-> entry
.getValue().getAddress().getUuid().isPresent() && uuid
.equals(entry
.getValue()
402 .map(Map
.Entry
::getValue
);
405 private RecipientId
nextIdLocked() {
406 return new RecipientId(++this.lastId
);
409 private void saveLocked() {
410 final var base64
= Base64
.getEncoder();
411 var storage
= new Storage(recipients
.entrySet().stream().map(pair
-> {
412 final var recipient
= pair
.getValue();
413 final var contact
= recipient
.getContact() == null
415 : new Storage
.Recipient
.Contact(recipient
.getContact().getName(),
416 recipient
.getContact().getColor(),
417 recipient
.getContact().getMessageExpirationTime(),
418 recipient
.getContact().isBlocked(),
419 recipient
.getContact().isArchived());
420 final var profile
= recipient
.getProfile() == null
422 : new Storage
.Recipient
.Profile(recipient
.getProfile().getLastUpdateTimestamp(),
423 recipient
.getProfile().getGivenName(),
424 recipient
.getProfile().getFamilyName(),
425 recipient
.getProfile().getAbout(),
426 recipient
.getProfile().getAboutEmoji(),
427 recipient
.getProfile().getUnidentifiedAccessMode().name(),
428 recipient
.getProfile()
432 .collect(Collectors
.toSet()));
433 return new Storage
.Recipient(pair
.getKey().getId(),
434 recipient
.getAddress().getNumber().orNull(),
435 recipient
.getAddress().getUuid().transform(UUID
::toString
).orNull(),
436 recipient
.getProfileKey() == null
438 : base64
.encodeToString(recipient
.getProfileKey().serialize()),
439 recipient
.getProfileKeyCredential() == null
441 : base64
.encodeToString(recipient
.getProfileKeyCredential().serialize()),
444 }).collect(Collectors
.toList()), lastId
);
446 // Write to memory first to prevent corrupting the file in case of serialization errors
447 try (var inMemoryOutput
= new ByteArrayOutputStream()) {
448 objectMapper
.writeValue(inMemoryOutput
, storage
);
450 var input
= new ByteArrayInputStream(inMemoryOutput
.toByteArray());
451 try (var outputStream
= new FileOutputStream(file
)) {
452 input
.transferTo(outputStream
);
454 } catch (Exception e
) {
455 logger
.error("Error saving recipient store file: {}", e
.getMessage());
459 private static class Storage
{
461 public List
<Recipient
> recipients
;
465 // For deserialization
469 public Storage(final List
<Recipient
> recipients
, final long lastId
) {
470 this.recipients
= recipients
;
471 this.lastId
= lastId
;
474 private static class Recipient
{
477 public String number
;
479 public String profileKey
;
480 public String profileKeyCredential
;
481 public Contact contact
;
482 public Profile profile
;
484 // For deserialization
485 private Recipient() {
492 final String profileKey
,
493 final String profileKeyCredential
,
494 final Contact contact
,
495 final Profile profile
498 this.number
= number
;
500 this.profileKey
= profileKey
;
501 this.profileKeyCredential
= profileKeyCredential
;
502 this.contact
= contact
;
503 this.profile
= profile
;
506 private static class Contact
{
510 public int messageExpirationTime
;
511 public boolean blocked
;
512 public boolean archived
;
514 // For deserialization
521 final int messageExpirationTime
,
522 final boolean blocked
,
523 final boolean archived
527 this.messageExpirationTime
= messageExpirationTime
;
528 this.blocked
= blocked
;
529 this.archived
= archived
;
533 private static class Profile
{
535 public long lastUpdateTimestamp
;
536 public String givenName
;
537 public String familyName
;
539 public String aboutEmoji
;
540 public String unidentifiedAccessMode
;
541 public Set
<String
> capabilities
;
543 // For deserialization
548 final long lastUpdateTimestamp
,
549 final String givenName
,
550 final String familyName
,
552 final String aboutEmoji
,
553 final String unidentifiedAccessMode
,
554 final Set
<String
> capabilities
556 this.lastUpdateTimestamp
= lastUpdateTimestamp
;
557 this.givenName
= givenName
;
558 this.familyName
= familyName
;
560 this.aboutEmoji
= aboutEmoji
;
561 this.unidentifiedAccessMode
= unidentifiedAccessMode
;
562 this.capabilities
= capabilities
;
568 public interface RecipientMergeHandler
{
570 void mergeRecipients(RecipientId recipientId
, RecipientId toBeMergedRecipientId
);