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