]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java
920315765fce70859dfc28f4e4b26a68ba17b2ff
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / helper / UnidentifiedAccessHelper.java
1 package org.asamk.signal.manager.helper;
2
3 import org.asamk.signal.manager.SignalDependencies;
4 import org.asamk.signal.manager.api.PhoneNumberSharingMode;
5 import org.asamk.signal.manager.storage.SignalAccount;
6 import org.asamk.signal.manager.storage.recipients.Profile;
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;
15
16 import java.io.IOException;
17 import java.util.List;
18 import java.util.Optional;
19 import java.util.concurrent.TimeUnit;
20
21 public class UnidentifiedAccessHelper {
22
23 private final static Logger logger = LoggerFactory.getLogger(UnidentifiedAccessHelper.class);
24 private final static long CERTIFICATE_EXPIRATION_BUFFER = TimeUnit.DAYS.toMillis(1);
25 private static final byte[] UNRESTRICTED_KEY = new byte[16];
26
27 private final SignalAccount account;
28 private final SignalDependencies dependencies;
29 private final Context context;
30
31 private SenderCertificate privacySenderCertificate;
32 private SenderCertificate senderCertificate;
33
34 public UnidentifiedAccessHelper(final Context context) {
35 this.account = context.getAccount();
36 this.dependencies = context.getDependencies();
37 this.context = context;
38 }
39
40 public void rotateSenderCertificates() {
41 privacySenderCertificate = null;
42 senderCertificate = null;
43 }
44
45 public List<Optional<UnidentifiedAccessPair>> getAccessFor(List<RecipientId> recipients) {
46 return recipients.stream().map(this::getAccessFor).toList();
47 }
48
49 public Optional<UnidentifiedAccessPair> getAccessFor(RecipientId recipient) {
50 return getAccessFor(recipient, false);
51 }
52
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();
58 }
59
60 var selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(noRefresh);
61 if (selfUnidentifiedAccessKey == null) {
62 logger.trace("Unidentified access not available for self");
63 return Optional.empty();
64 }
65
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();
70 }
71
72 try {
73 return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(recipientUnidentifiedAccessKey,
74 senderCertificate,
75 false), new UnidentifiedAccess(selfUnidentifiedAccessKey, senderCertificate, false)));
76 } catch (InvalidCertificateException e) {
77 return Optional.empty();
78 }
79 }
80
81 public Optional<UnidentifiedAccessPair> getAccessForSync() {
82 var selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(false);
83 var selfUnidentifiedAccessCertificate = getSenderCertificate();
84
85 if (selfUnidentifiedAccessKey == null || selfUnidentifiedAccessCertificate == null) {
86 return Optional.empty();
87 }
88
89 try {
90 return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(selfUnidentifiedAccessKey,
91 selfUnidentifiedAccessCertificate,
92 false),
93 new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate, false)));
94 } catch (InvalidCertificateException e) {
95 return Optional.empty();
96 }
97 }
98
99 private byte[] getSenderCertificateFor(final RecipientId recipientId) {
100 final var sharingMode = account.getConfigurationStore().getPhoneNumberSharingMode();
101 if (sharingMode == null || sharingMode == PhoneNumberSharingMode.EVERYBODY || (
102 sharingMode == PhoneNumberSharingMode.CONTACTS
103 && account.getContactStore().getContact(recipientId) != null
104 )) {
105 logger.trace("Using normal sender certificate for message to {}", recipientId);
106 return getSenderCertificate();
107 } else {
108 logger.trace("Using phone number privacy sender certificate for message to {}", recipientId);
109 return getSenderCertificateForPhoneNumberPrivacy();
110 }
111 }
112
113 private byte[] getSenderCertificateForPhoneNumberPrivacy() {
114 if (privacySenderCertificate != null && System.currentTimeMillis() < (
115 privacySenderCertificate.getExpiration() - CERTIFICATE_EXPIRATION_BUFFER
116 )) {
117 return privacySenderCertificate.getSerialized();
118 }
119 try {
120 final var certificate = dependencies.getAccountManager().getSenderCertificateForPhoneNumberPrivacy();
121 privacySenderCertificate = new SenderCertificate(certificate);
122 return certificate;
123 } catch (IOException | InvalidCertificateException e) {
124 logger.warn("Failed to get sender certificate, ignoring: {}", e.getMessage());
125 return null;
126 }
127 }
128
129 private byte[] getSenderCertificate() {
130 if (senderCertificate != null && System.currentTimeMillis() < (
131 senderCertificate.getExpiration() - CERTIFICATE_EXPIRATION_BUFFER
132 )) {
133 return senderCertificate.getSerialized();
134 }
135 try {
136 final var certificate = dependencies.getAccountManager().getSenderCertificate();
137 this.senderCertificate = new SenderCertificate(certificate);
138 return certificate;
139 } catch (IOException | InvalidCertificateException e) {
140 logger.warn("Failed to get sender certificate, ignoring: {}", e.getMessage());
141 return null;
142 }
143 }
144
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();
152 }
153 return UnidentifiedAccess.deriveAccessKeyFrom(account.getProfileKey());
154 }
155
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) {
161 return null;
162 }
163
164 var theirProfileKey = account.getProfileStore().getProfileKey(recipientId);
165 return getTargetUnidentifiedAccessKey(targetProfile, theirProfileKey);
166 }
167
168 private static byte[] getTargetUnidentifiedAccessKey(
169 final Profile targetProfile, final ProfileKey theirProfileKey
170 ) {
171 return switch (targetProfile.getUnidentifiedAccessMode()) {
172 case ENABLED -> theirProfileKey == null ? null : UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);
173 case UNRESTRICTED -> createUnrestrictedUnidentifiedAccess();
174 default -> null;
175 };
176 }
177
178 private static byte[] createUnrestrictedUnidentifiedAccess() {
179 return UNRESTRICTED_KEY;
180 }
181 }