1 package org
.asamk
.signal
.manager
.helper
;
3 import org
.asamk
.signal
.manager
.api
.PhoneNumberSharingMode
;
4 import org
.asamk
.signal
.manager
.api
.Profile
;
5 import org
.asamk
.signal
.manager
.internal
.SignalDependencies
;
6 import org
.asamk
.signal
.manager
.storage
.SignalAccount
;
7 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientId
;
8 import org
.signal
.libsignal
.metadata
.certificate
.InvalidCertificateException
;
9 import org
.signal
.libsignal
.metadata
.certificate
.SenderCertificate
;
10 import org
.signal
.libsignal
.zkgroup
.profiles
.ProfileKey
;
11 import org
.slf4j
.Logger
;
12 import org
.slf4j
.LoggerFactory
;
13 import org
.whispersystems
.signalservice
.api
.crypto
.UnidentifiedAccess
;
14 import org
.whispersystems
.signalservice
.api
.crypto
.UnidentifiedAccessPair
;
16 import java
.io
.IOException
;
17 import java
.util
.List
;
18 import java
.util
.Optional
;
19 import java
.util
.concurrent
.TimeUnit
;
21 public class UnidentifiedAccessHelper
{
23 private static final Logger logger
= LoggerFactory
.getLogger(UnidentifiedAccessHelper
.class);
24 private static final long CERTIFICATE_EXPIRATION_BUFFER
= TimeUnit
.DAYS
.toMillis(1);
25 private static final byte[] UNRESTRICTED_KEY
= new byte[16];
27 private final SignalAccount account
;
28 private final SignalDependencies dependencies
;
29 private final Context context
;
31 private SenderCertificate privacySenderCertificate
;
32 private SenderCertificate senderCertificate
;
34 public UnidentifiedAccessHelper(final Context context
) {
35 this.account
= context
.getAccount();
36 this.dependencies
= context
.getDependencies();
37 this.context
= context
;
40 public void rotateSenderCertificates() {
41 privacySenderCertificate
= null;
42 senderCertificate
= null;
45 public List
<Optional
<UnidentifiedAccessPair
>> getAccessFor(List
<RecipientId
> recipients
) {
46 return recipients
.stream().map(this::getAccessFor
).toList();
49 public Optional
<UnidentifiedAccessPair
> getAccessFor(RecipientId recipient
) {
50 return getAccessFor(recipient
, false);
53 public Optional
<UnidentifiedAccessPair
> getAccessFor(RecipientId recipientId
, boolean noRefresh
) {
54 var recipientUnidentifiedAccessKey
= getTargetUnidentifiedAccessKey(recipientId
, noRefresh
);
55 if (recipientUnidentifiedAccessKey
== null) {
56 logger
.trace("Unidentified access not available for {}", recipientId
);
57 return Optional
.empty();
60 var selfUnidentifiedAccessKey
= getSelfUnidentifiedAccessKey(noRefresh
);
61 if (selfUnidentifiedAccessKey
== null) {
62 logger
.trace("Unidentified access not available for self");
63 return Optional
.empty();
66 var senderCertificate
= getSenderCertificateFor(recipientId
);
67 if (senderCertificate
== null) {
68 logger
.trace("Unidentified access not available due to missing sender certificate");
69 return Optional
.empty();
73 return Optional
.of(new UnidentifiedAccessPair(new UnidentifiedAccess(recipientUnidentifiedAccessKey
,
75 false), new UnidentifiedAccess(selfUnidentifiedAccessKey
, senderCertificate
, false)));
76 } catch (InvalidCertificateException e
) {
77 return Optional
.empty();
81 public Optional
<UnidentifiedAccessPair
> getAccessForSync() {
82 var selfUnidentifiedAccessKey
= getSelfUnidentifiedAccessKey(false);
83 var selfUnidentifiedAccessCertificate
= getSenderCertificate();
85 if (selfUnidentifiedAccessKey
== null || selfUnidentifiedAccessCertificate
== null) {
86 return Optional
.empty();
90 return Optional
.of(new UnidentifiedAccessPair(new UnidentifiedAccess(selfUnidentifiedAccessKey
,
91 selfUnidentifiedAccessCertificate
,
93 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
, false)));
94 } catch (InvalidCertificateException e
) {
95 return Optional
.empty();
99 private byte[] getSenderCertificateFor(final RecipientId recipientId
) {
100 final var sharingMode
= account
.getConfigurationStore().getPhoneNumberSharingMode();
101 if (sharingMode
== PhoneNumberSharingMode
.EVERYBODY
|| (
102 sharingMode
== PhoneNumberSharingMode
.CONTACTS
103 && account
.getContactStore().getContact(recipientId
) != null
105 logger
.trace("Using normal sender certificate for message to {}", recipientId
);
106 return getSenderCertificate();
108 logger
.trace("Using phone number privacy sender certificate for message to {}", recipientId
);
109 return getSenderCertificateForPhoneNumberPrivacy();
113 private byte[] getSenderCertificateForPhoneNumberPrivacy() {
114 if (privacySenderCertificate
!= null && System
.currentTimeMillis() < (
115 privacySenderCertificate
.getExpiration() - CERTIFICATE_EXPIRATION_BUFFER
117 return privacySenderCertificate
.getSerialized();
120 final var certificate
= dependencies
.getAccountManager().getSenderCertificateForPhoneNumberPrivacy();
121 privacySenderCertificate
= new SenderCertificate(certificate
);
123 } catch (IOException
| InvalidCertificateException e
) {
124 logger
.warn("Failed to get sender certificate, ignoring: {}", e
.getMessage());
129 private byte[] getSenderCertificate() {
130 if (senderCertificate
!= null && System
.currentTimeMillis() < (
131 senderCertificate
.getExpiration() - CERTIFICATE_EXPIRATION_BUFFER
133 return senderCertificate
.getSerialized();
136 final var certificate
= dependencies
.getAccountManager().getSenderCertificate();
137 this.senderCertificate
= new SenderCertificate(certificate
);
139 } catch (IOException
| InvalidCertificateException e
) {
140 logger
.warn("Failed to get sender certificate, ignoring: {}", e
.getMessage());
145 private byte[] getSelfUnidentifiedAccessKey(boolean noRefresh
) {
146 var selfProfile
= noRefresh
147 ? account
.getProfileStore().getProfile(account
.getSelfRecipientId())
148 : context
.getProfileHelper().getSelfProfile();
149 if (selfProfile
!= null
150 && selfProfile
.getUnidentifiedAccessMode() == Profile
.UnidentifiedAccessMode
.UNRESTRICTED
) {
151 return createUnrestrictedUnidentifiedAccess();
153 return UnidentifiedAccess
.deriveAccessKeyFrom(account
.getProfileKey());
156 private byte[] getTargetUnidentifiedAccessKey(RecipientId recipientId
, boolean noRefresh
) {
157 var targetProfile
= noRefresh
158 ? account
.getProfileStore().getProfile(recipientId
)
159 : context
.getProfileHelper().getRecipientProfile(recipientId
);
160 if (targetProfile
== null) {
164 var theirProfileKey
= account
.getProfileStore().getProfileKey(recipientId
);
165 return getTargetUnidentifiedAccessKey(targetProfile
, theirProfileKey
);
168 private static byte[] getTargetUnidentifiedAccessKey(
169 final Profile targetProfile
, final ProfileKey theirProfileKey
171 return switch (targetProfile
.getUnidentifiedAccessMode()) {
172 case ENABLED
-> theirProfileKey
== null ?
null : UnidentifiedAccess
.deriveAccessKeyFrom(theirProfileKey
);
173 case UNRESTRICTED
-> createUnrestrictedUnidentifiedAccess();
178 private static byte[] createUnrestrictedUnidentifiedAccess() {
179 return UNRESTRICTED_KEY
;