]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java
Update libsignal-service
[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.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.jetbrains.annotations.Nullable;
9 import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
10 import org.signal.libsignal.metadata.certificate.SenderCertificate;
11 import org.signal.libsignal.zkgroup.profiles.ProfileKey;
12 import org.slf4j.Logger;
13 import org.slf4j.LoggerFactory;
14 import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
15 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
16
17 import java.io.IOException;
18 import java.util.List;
19 import java.util.concurrent.TimeUnit;
20
21 import static org.asamk.signal.manager.util.Utils.handleResponseException;
22
23 public class UnidentifiedAccessHelper {
24
25 private static final Logger logger = LoggerFactory.getLogger(UnidentifiedAccessHelper.class);
26 private static final long CERTIFICATE_EXPIRATION_BUFFER = TimeUnit.DAYS.toMillis(1);
27 private static final byte[] UNRESTRICTED_KEY = new byte[16];
28
29 private final SignalAccount account;
30 private final SignalDependencies dependencies;
31 private final Context context;
32
33 private SenderCertificate privacySenderCertificate;
34 private SenderCertificate senderCertificate;
35
36 public UnidentifiedAccessHelper(final Context context) {
37 this.account = context.getAccount();
38 this.dependencies = context.getDependencies();
39 this.context = context;
40 }
41
42 public void rotateSenderCertificates() {
43 privacySenderCertificate = null;
44 senderCertificate = null;
45 }
46
47 public List<SealedSenderAccess> getSealedSenderAccessFor(List<RecipientId> recipients) {
48 return recipients.stream().map(this::getAccessFor).map(SealedSenderAccess::forIndividual).toList();
49 }
50
51 public @Nullable SealedSenderAccess getSealedSenderAccessFor(RecipientId recipient) {
52 return getSealedSenderAccessFor(recipient, false);
53 }
54
55 public @Nullable SealedSenderAccess getSealedSenderAccessFor(RecipientId recipient, boolean noRefresh) {
56 return SealedSenderAccess.forIndividual(getAccessFor(recipient, noRefresh));
57 }
58
59 public List<UnidentifiedAccess> getAccessFor(List<RecipientId> recipients) {
60 return recipients.stream().map(this::getAccessFor).toList();
61 }
62
63 private @Nullable UnidentifiedAccess getAccessFor(RecipientId recipient) {
64 return getAccessFor(recipient, false);
65 }
66
67 private @Nullable UnidentifiedAccess getAccessFor(RecipientId recipientId, boolean noRefresh) {
68 var recipientUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipientId, noRefresh);
69 if (recipientUnidentifiedAccessKey == null) {
70 logger.trace("Unidentified access not available for {}", recipientId);
71 return null;
72 }
73
74 var selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(noRefresh);
75 if (selfUnidentifiedAccessKey == null) {
76 logger.trace("Unidentified access not available for self");
77 return null;
78 }
79
80 var senderCertificate = getSenderCertificateFor(recipientId);
81 if (senderCertificate == null) {
82 logger.trace("Unidentified access not available due to missing sender certificate");
83 return null;
84 }
85
86 try {
87 return new UnidentifiedAccess(recipientUnidentifiedAccessKey, senderCertificate, false);
88 } catch (InvalidCertificateException e) {
89 return null;
90 }
91 }
92
93 private byte[] getSenderCertificateFor(final RecipientId recipientId) {
94 final var sharingMode = account.getConfigurationStore().getPhoneNumberSharingMode();
95 if (sharingMode == PhoneNumberSharingMode.EVERYBODY || (
96 sharingMode == PhoneNumberSharingMode.CONTACTS
97 && account.getContactStore().getContact(recipientId) != null
98 )) {
99 logger.trace("Using normal sender certificate for message to {}", recipientId);
100 return getSenderCertificate();
101 } else {
102 logger.trace("Using phone number privacy sender certificate for message to {}", recipientId);
103 return getSenderCertificateForPhoneNumberPrivacy();
104 }
105 }
106
107 private byte[] getSenderCertificateForPhoneNumberPrivacy() {
108 if (privacySenderCertificate != null && System.currentTimeMillis() < (
109 privacySenderCertificate.getExpiration() - CERTIFICATE_EXPIRATION_BUFFER
110 )) {
111 return privacySenderCertificate.getSerialized();
112 }
113 try {
114 final var certificate = handleResponseException(dependencies.getCertificateApi()
115 .getSenderCertificateForPhoneNumberPrivacy());
116 privacySenderCertificate = new SenderCertificate(certificate);
117 return certificate;
118 } catch (IOException | InvalidCertificateException e) {
119 logger.warn("Failed to get sender certificate (pnp), ignoring: {}", e.getMessage());
120 return null;
121 }
122 }
123
124 private byte[] getSenderCertificate() {
125 if (senderCertificate != null && System.currentTimeMillis() < (
126 senderCertificate.getExpiration() - CERTIFICATE_EXPIRATION_BUFFER
127 )) {
128 return senderCertificate.getSerialized();
129 }
130 try {
131 final var certificate = handleResponseException(dependencies.getCertificateApi().getSenderCertificate());
132 this.senderCertificate = new SenderCertificate(certificate);
133 return certificate;
134 } catch (IOException | InvalidCertificateException e) {
135 logger.warn("Failed to get sender certificate, ignoring: {}", e.getMessage());
136 return null;
137 }
138 }
139
140 private byte[] getSelfUnidentifiedAccessKey(boolean noRefresh) {
141 var selfProfile = noRefresh
142 ? account.getProfileStore().getProfile(account.getSelfRecipientId())
143 : context.getProfileHelper().getSelfProfile();
144 if (selfProfile != null
145 && selfProfile.getUnidentifiedAccessMode() == Profile.UnidentifiedAccessMode.UNRESTRICTED) {
146 return createUnrestrictedUnidentifiedAccess();
147 }
148 return UnidentifiedAccess.deriveAccessKeyFrom(account.getProfileKey());
149 }
150
151 private byte[] getTargetUnidentifiedAccessKey(RecipientId recipientId, boolean noRefresh) {
152 var targetProfile = noRefresh
153 ? account.getProfileStore().getProfile(recipientId)
154 : context.getProfileHelper().getRecipientProfile(recipientId);
155 if (targetProfile == null) {
156 return null;
157 }
158
159 var theirProfileKey = account.getProfileStore().getProfileKey(recipientId);
160 return getTargetUnidentifiedAccessKey(targetProfile, theirProfileKey);
161 }
162
163 private static byte[] getTargetUnidentifiedAccessKey(
164 final Profile targetProfile,
165 final ProfileKey theirProfileKey
166 ) {
167 return switch (targetProfile.getUnidentifiedAccessMode()) {
168 case ENABLED -> theirProfileKey == null ? null : UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);
169 case UNRESTRICTED -> createUnrestrictedUnidentifiedAccess();
170 default -> null;
171 };
172 }
173
174 private static byte[] createUnrestrictedUnidentifiedAccess() {
175 return UNRESTRICTED_KEY;
176 }
177 }