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 storeRecipientLocked(recipientId
, Recipient
.newBuilder(recipient
).withProfileKey(profileKey
).build());
234 public void storeProfileKeyCredential(
235 final RecipientId recipientId
, final ProfileKeyCredential profileKeyCredential
237 synchronized (recipients
) {
238 final var recipient
= recipients
.get(recipientId
);
239 storeRecipientLocked(recipientId
,
240 Recipient
.newBuilder(recipient
).withProfileKeyCredential(profileKeyCredential
).build());
244 public boolean isEmpty() {
245 synchronized (recipients
) {
246 return recipients
.isEmpty();
251 * @param isHighTrust true, if the number/uuid connection was obtained from a trusted source.
252 * Has no effect, if the address contains only a number or a uuid.
254 private RecipientId
resolveRecipient(SignalServiceAddress address
, boolean isHighTrust
) {
255 final Pair
<RecipientId
, Optional
<RecipientId
>> pair
;
256 synchronized (recipients
) {
257 pair
= resolveRecipientLocked(address
, isHighTrust
);
258 if (pair
.second().isPresent()) {
259 recipientsMerged
.put(pair
.second().get(), pair
.first());
263 if (pair
.second().isPresent()) {
264 recipientMergeHandler
.mergeRecipients(pair
.first(), pair
.second().get());
269 private Pair
<RecipientId
, Optional
<RecipientId
>> resolveRecipientLocked(
270 SignalServiceAddress address
, boolean isHighTrust
272 final var byNumber
= !address
.getNumber().isPresent()
273 ? Optional
.<Recipient
>empty()
274 : findByNameLocked(address
.getNumber().get());
275 final var byUuid
= !address
.getUuid().isPresent()
276 ? Optional
.<Recipient
>empty()
277 : findByUuidLocked(address
.getUuid().get());
279 if (byNumber
.isEmpty() && byUuid
.isEmpty()) {
280 logger
.debug("Got new recipient, both uuid and number are unknown");
282 if (isHighTrust
|| !address
.getUuid().isPresent() || !address
.getNumber().isPresent()) {
283 return new Pair
<>(addNewRecipientLocked(address
), Optional
.empty());
286 return new Pair
<>(addNewRecipientLocked(new SignalServiceAddress(address
.getUuid().get(), null)),
291 || !address
.getUuid().isPresent()
292 || !address
.getNumber().isPresent()
293 || byNumber
.equals(byUuid
)) {
294 return new Pair
<>(byUuid
.or(() -> byNumber
).map(Recipient
::getRecipientId
).get(), Optional
.empty());
297 if (byNumber
.isEmpty()) {
298 logger
.debug("Got recipient existing with uuid, updating with high trust number");
299 updateRecipientAddressLocked(byUuid
.get().getRecipientId(), address
);
300 return new Pair
<>(byUuid
.get().getRecipientId(), Optional
.empty());
303 if (byUuid
.isEmpty()) {
304 logger
.debug("Got recipient existing with number, updating with high trust uuid");
305 updateRecipientAddressLocked(byNumber
.get().getRecipientId(), address
);
306 return new Pair
<>(byNumber
.get().getRecipientId(), Optional
.empty());
309 if (byNumber
.get().getAddress().getUuid().isPresent()) {
311 "Got separate recipients for high trust number and uuid, recipient for number has different uuid, so stripping its number");
313 updateRecipientAddressLocked(byNumber
.get().getRecipientId(),
314 new SignalServiceAddress(byNumber
.get().getAddress().getUuid().get(), null));
315 updateRecipientAddressLocked(byUuid
.get().getRecipientId(), address
);
316 return new Pair
<>(byUuid
.get().getRecipientId(), Optional
.empty());
319 logger
.debug("Got separate recipients for high trust number and uuid, need to merge them");
320 updateRecipientAddressLocked(byUuid
.get().getRecipientId(), address
);
321 mergeRecipientsLocked(byUuid
.get().getRecipientId(), byNumber
.get().getRecipientId());
322 return new Pair
<>(byUuid
.get().getRecipientId(), byNumber
.map(Recipient
::getRecipientId
));
325 private RecipientId
addNewRecipientLocked(final SignalServiceAddress serviceAddress
) {
326 final var nextRecipientId
= nextIdLocked();
327 storeRecipientLocked(nextRecipientId
, new Recipient(nextRecipientId
, serviceAddress
, null, null, null, null));
328 return nextRecipientId
;
331 private void updateRecipientAddressLocked(
332 final RecipientId recipientId
, final SignalServiceAddress address
334 final var recipient
= recipients
.get(recipientId
);
335 storeRecipientLocked(recipientId
, Recipient
.newBuilder(recipient
).withAddress(address
).build());
338 private void storeRecipientLocked(
339 final RecipientId recipientId
, final Recipient recipient
341 recipients
.put(recipientId
, recipient
);
345 private void mergeRecipientsLocked(RecipientId recipientId
, RecipientId toBeMergedRecipientId
) {
346 final var recipient
= recipients
.get(recipientId
);
347 final var toBeMergedRecipient
= recipients
.get(toBeMergedRecipientId
);
348 recipients
.put(recipientId
,
349 new Recipient(recipientId
,
350 recipient
.getAddress(),
351 recipient
.getContact() != null ? recipient
.getContact() : toBeMergedRecipient
.getContact(),
352 recipient
.getProfileKey() != null
353 ? recipient
.getProfileKey()
354 : toBeMergedRecipient
.getProfileKey(),
355 recipient
.getProfileKeyCredential() != null
356 ? recipient
.getProfileKeyCredential()
357 : toBeMergedRecipient
.getProfileKeyCredential(),
358 recipient
.getProfile() != null ? recipient
.getProfile() : toBeMergedRecipient
.getProfile()));
359 recipients
.remove(toBeMergedRecipientId
);
363 private Optional
<Recipient
> findByNameLocked(final String number
) {
364 return recipients
.entrySet()
366 .filter(entry
-> entry
.getValue().getAddress().getNumber().isPresent() && number
.equals(entry
.getValue()
371 .map(Map
.Entry
::getValue
);
374 private Optional
<Recipient
> findByUuidLocked(final UUID uuid
) {
375 return recipients
.entrySet()
377 .filter(entry
-> entry
.getValue().getAddress().getUuid().isPresent() && uuid
.equals(entry
.getValue()
382 .map(Map
.Entry
::getValue
);
385 private RecipientId
nextIdLocked() {
386 return new RecipientId(++this.lastId
);
389 private void saveLocked() {
390 final var base64
= Base64
.getEncoder();
391 var storage
= new Storage(recipients
.entrySet().stream().map(pair
-> {
392 final var recipient
= pair
.getValue();
393 final var contact
= recipient
.getContact() == null
395 : new Storage
.Recipient
.Contact(recipient
.getContact().getName(),
396 recipient
.getContact().getColor(),
397 recipient
.getContact().getMessageExpirationTime(),
398 recipient
.getContact().isBlocked(),
399 recipient
.getContact().isArchived());
400 final var profile
= recipient
.getProfile() == null
402 : new Storage
.Recipient
.Profile(recipient
.getProfile().getLastUpdateTimestamp(),
403 recipient
.getProfile().getGivenName(),
404 recipient
.getProfile().getFamilyName(),
405 recipient
.getProfile().getAbout(),
406 recipient
.getProfile().getAboutEmoji(),
407 recipient
.getProfile().getUnidentifiedAccessMode().name(),
408 recipient
.getProfile()
412 .collect(Collectors
.toSet()));
413 return new Storage
.Recipient(pair
.getKey().getId(),
414 recipient
.getAddress().getNumber().orNull(),
415 recipient
.getAddress().getUuid().transform(UUID
::toString
).orNull(),
416 recipient
.getProfileKey() == null
418 : base64
.encodeToString(recipient
.getProfileKey().serialize()),
419 recipient
.getProfileKeyCredential() == null
421 : base64
.encodeToString(recipient
.getProfileKeyCredential().serialize()),
424 }).collect(Collectors
.toList()), lastId
);
426 // Write to memory first to prevent corrupting the file in case of serialization errors
427 try (var inMemoryOutput
= new ByteArrayOutputStream()) {
428 objectMapper
.writeValue(inMemoryOutput
, storage
);
430 var input
= new ByteArrayInputStream(inMemoryOutput
.toByteArray());
431 try (var outputStream
= new FileOutputStream(file
)) {
432 input
.transferTo(outputStream
);
434 } catch (Exception e
) {
435 logger
.error("Error saving recipient store file: {}", e
.getMessage());
439 private static class Storage
{
441 public List
<Recipient
> recipients
;
445 // For deserialization
449 public Storage(final List
<Recipient
> recipients
, final long lastId
) {
450 this.recipients
= recipients
;
451 this.lastId
= lastId
;
454 private static class Recipient
{
457 public String number
;
459 public String profileKey
;
460 public String profileKeyCredential
;
461 public Contact contact
;
462 public Profile profile
;
464 // For deserialization
465 private Recipient() {
472 final String profileKey
,
473 final String profileKeyCredential
,
474 final Contact contact
,
475 final Profile profile
478 this.number
= number
;
480 this.profileKey
= profileKey
;
481 this.profileKeyCredential
= profileKeyCredential
;
482 this.contact
= contact
;
483 this.profile
= profile
;
486 private static class Contact
{
490 public int messageExpirationTime
;
491 public boolean blocked
;
492 public boolean archived
;
494 // For deserialization
501 final int messageExpirationTime
,
502 final boolean blocked
,
503 final boolean archived
507 this.messageExpirationTime
= messageExpirationTime
;
508 this.blocked
= blocked
;
509 this.archived
= archived
;
513 private static class Profile
{
515 public long lastUpdateTimestamp
;
516 public String givenName
;
517 public String familyName
;
519 public String aboutEmoji
;
520 public String unidentifiedAccessMode
;
521 public Set
<String
> capabilities
;
523 // For deserialization
528 final long lastUpdateTimestamp
,
529 final String givenName
,
530 final String familyName
,
532 final String aboutEmoji
,
533 final String unidentifiedAccessMode
,
534 final Set
<String
> capabilities
536 this.lastUpdateTimestamp
= lastUpdateTimestamp
;
537 this.givenName
= givenName
;
538 this.familyName
= familyName
;
540 this.aboutEmoji
= aboutEmoji
;
541 this.unidentifiedAccessMode
= unidentifiedAccessMode
;
542 this.capabilities
= capabilities
;
548 public interface RecipientMergeHandler
{
550 void mergeRecipients(RecipientId recipientId
, RecipientId toBeMergedRecipientId
);