]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java
d9c553b737a2c57606f2412db7b6acf24586c043
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / storage / identities / IdentityKeyStore.java
1 package org.asamk.signal.manager.storage.identities;
2
3 import com.fasterxml.jackson.databind.ObjectMapper;
4
5 import org.asamk.signal.manager.api.TrustLevel;
6 import org.asamk.signal.manager.storage.recipients.RecipientId;
7 import org.asamk.signal.manager.storage.recipients.RecipientResolver;
8 import org.asamk.signal.manager.util.IOUtils;
9 import org.signal.libsignal.protocol.IdentityKey;
10 import org.signal.libsignal.protocol.IdentityKeyPair;
11 import org.signal.libsignal.protocol.InvalidKeyException;
12 import org.signal.libsignal.protocol.SignalProtocolAddress;
13 import org.slf4j.Logger;
14 import org.slf4j.LoggerFactory;
15
16 import java.io.ByteArrayInputStream;
17 import java.io.ByteArrayOutputStream;
18 import java.io.File;
19 import java.io.FileInputStream;
20 import java.io.FileOutputStream;
21 import java.io.IOException;
22 import java.nio.file.Files;
23 import java.util.Arrays;
24 import java.util.Base64;
25 import java.util.Date;
26 import java.util.HashMap;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Objects;
30 import java.util.regex.Pattern;
31
32 import io.reactivex.rxjava3.subjects.PublishSubject;
33 import io.reactivex.rxjava3.subjects.Subject;
34
35 public class IdentityKeyStore implements org.signal.libsignal.protocol.state.IdentityKeyStore {
36
37 private final static Logger logger = LoggerFactory.getLogger(IdentityKeyStore.class);
38 private final ObjectMapper objectMapper = org.asamk.signal.manager.storage.Utils.createStorageObjectMapper();
39
40 private final Map<RecipientId, IdentityInfo> cachedIdentities = new HashMap<>();
41
42 private final File identitiesPath;
43
44 private final RecipientResolver resolver;
45 private final IdentityKeyPair identityKeyPair;
46 private final int localRegistrationId;
47 private final TrustNewIdentity trustNewIdentity;
48 private final PublishSubject<RecipientId> identityChanges = PublishSubject.create();
49
50 private boolean isRetryingDecryption = false;
51
52 public IdentityKeyStore(
53 final File identitiesPath,
54 final RecipientResolver resolver,
55 final IdentityKeyPair identityKeyPair,
56 final int localRegistrationId,
57 final TrustNewIdentity trustNewIdentity
58 ) {
59 this.identitiesPath = identitiesPath;
60 this.resolver = resolver;
61 this.identityKeyPair = identityKeyPair;
62 this.localRegistrationId = localRegistrationId;
63 this.trustNewIdentity = trustNewIdentity;
64 }
65
66 public Subject<RecipientId> getIdentityChanges() {
67 return identityChanges;
68 }
69
70 @Override
71 public IdentityKeyPair getIdentityKeyPair() {
72 return identityKeyPair;
73 }
74
75 @Override
76 public int getLocalRegistrationId() {
77 return localRegistrationId;
78 }
79
80 @Override
81 public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
82 final var recipientId = resolveRecipient(address.getName());
83
84 return saveIdentity(recipientId, identityKey, new Date());
85 }
86
87 public boolean saveIdentity(final RecipientId recipientId, final IdentityKey identityKey, Date added) {
88 if (isRetryingDecryption) {
89 return false;
90 }
91 synchronized (cachedIdentities) {
92 final var identityInfo = loadIdentityLocked(recipientId);
93 if (identityInfo != null && identityInfo.getIdentityKey().equals(identityKey)) {
94 // Identity already exists, not updating the trust level
95 logger.trace("Not storing new identity for recipient {}, identity already stored", recipientId);
96 return false;
97 }
98
99 final var trustLevel = trustNewIdentity == TrustNewIdentity.ALWAYS || (
100 trustNewIdentity == TrustNewIdentity.ON_FIRST_USE && identityInfo == null
101 ) ? TrustLevel.TRUSTED_UNVERIFIED : TrustLevel.UNTRUSTED;
102 logger.debug("Storing new identity for recipient {} with trust {}", recipientId, trustLevel);
103 final var newIdentityInfo = new IdentityInfo(recipientId, identityKey, trustLevel, added);
104 storeIdentityLocked(recipientId, newIdentityInfo);
105 identityChanges.onNext(recipientId);
106 return true;
107 }
108 }
109
110 public void setRetryingDecryption(final boolean retryingDecryption) {
111 isRetryingDecryption = retryingDecryption;
112 }
113
114 public boolean setIdentityTrustLevel(RecipientId recipientId, IdentityKey identityKey, TrustLevel trustLevel) {
115 synchronized (cachedIdentities) {
116 final var identityInfo = loadIdentityLocked(recipientId);
117 if (identityInfo == null) {
118 logger.debug("Not updating trust level for recipient {}, identity not found", recipientId);
119 return false;
120 }
121 if (!identityInfo.getIdentityKey().equals(identityKey)) {
122 logger.debug("Not updating trust level for recipient {}, different identity found", recipientId);
123 return false;
124 }
125 if (identityInfo.getTrustLevel() == trustLevel) {
126 logger.trace("Not updating trust level for recipient {}, trust level already matches", recipientId);
127 return false;
128 }
129
130 logger.debug("Updating trust level for recipient {} with trust {}", recipientId, trustLevel);
131 final var newIdentityInfo = new IdentityInfo(recipientId,
132 identityKey,
133 trustLevel,
134 identityInfo.getDateAdded());
135 storeIdentityLocked(recipientId, newIdentityInfo);
136 return true;
137 }
138 }
139
140 @Override
141 public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
142 if (trustNewIdentity == TrustNewIdentity.ALWAYS) {
143 return true;
144 }
145
146 var recipientId = resolveRecipient(address.getName());
147
148 synchronized (cachedIdentities) {
149 // TODO implement possibility for different handling of incoming/outgoing trust decisions
150 var identityInfo = loadIdentityLocked(recipientId);
151 if (identityInfo == null) {
152 logger.debug("Initial identity found for {}, saving.", recipientId);
153 saveIdentity(address, identityKey);
154 identityInfo = loadIdentityLocked(recipientId);
155 } else if (!identityInfo.getIdentityKey().equals(identityKey)) {
156 // Identity found, but different
157 if (direction == Direction.SENDING) {
158 logger.debug("Changed identity found for {}, saving.", recipientId);
159 saveIdentity(address, identityKey);
160 identityInfo = loadIdentityLocked(recipientId);
161 } else {
162 logger.trace("Trusting identity for {} for {}: {}", recipientId, direction, false);
163 return false;
164 }
165 }
166
167 final var isTrusted = identityInfo != null && identityInfo.isTrusted();
168 logger.trace("Trusting identity for {} for {}: {}", recipientId, direction, isTrusted);
169 return isTrusted;
170 }
171 }
172
173 @Override
174 public IdentityKey getIdentity(SignalProtocolAddress address) {
175 var recipientId = resolveRecipient(address.getName());
176
177 synchronized (cachedIdentities) {
178 var identity = loadIdentityLocked(recipientId);
179 return identity == null ? null : identity.getIdentityKey();
180 }
181 }
182
183 public IdentityInfo getIdentity(RecipientId recipientId) {
184 synchronized (cachedIdentities) {
185 return loadIdentityLocked(recipientId);
186 }
187 }
188
189 final Pattern identityFileNamePattern = Pattern.compile("(\\d+)");
190
191 public List<IdentityInfo> getIdentities() {
192 final var files = identitiesPath.listFiles();
193 if (files == null) {
194 return List.of();
195 }
196 return Arrays.stream(files)
197 .filter(f -> identityFileNamePattern.matcher(f.getName()).matches())
198 .map(f -> resolver.resolveRecipient(Long.parseLong(f.getName())))
199 .filter(Objects::nonNull)
200 .map(this::loadIdentityLocked)
201 .filter(Objects::nonNull)
202 .toList();
203 }
204
205 public void mergeRecipients(final RecipientId recipientId, final RecipientId toBeMergedRecipientId) {
206 synchronized (cachedIdentities) {
207 deleteIdentityLocked(toBeMergedRecipientId);
208 }
209 }
210
211 public void deleteIdentity(final RecipientId recipientId) {
212 synchronized (cachedIdentities) {
213 deleteIdentityLocked(recipientId);
214 }
215 }
216
217 /**
218 * @param identifier can be either a serialized uuid or a e164 phone number
219 */
220 private RecipientId resolveRecipient(String identifier) {
221 return resolver.resolveRecipient(identifier);
222 }
223
224 private File getIdentityFile(final RecipientId recipientId) {
225 try {
226 IOUtils.createPrivateDirectories(identitiesPath);
227 } catch (IOException e) {
228 throw new AssertionError("Failed to create identities path", e);
229 }
230 return new File(identitiesPath, String.valueOf(recipientId.id()));
231 }
232
233 private IdentityInfo loadIdentityLocked(final RecipientId recipientId) {
234 {
235 final var session = cachedIdentities.get(recipientId);
236 if (session != null) {
237 return session;
238 }
239 }
240
241 final var file = getIdentityFile(recipientId);
242 if (!file.exists()) {
243 return null;
244 }
245 try (var inputStream = new FileInputStream(file)) {
246 var storage = objectMapper.readValue(inputStream, IdentityStorage.class);
247
248 var id = new IdentityKey(Base64.getDecoder().decode(storage.identityKey()));
249 var trustLevel = TrustLevel.fromInt(storage.trustLevel());
250 var added = new Date(storage.addedTimestamp());
251
252 final var identityInfo = new IdentityInfo(recipientId, id, trustLevel, added);
253 cachedIdentities.put(recipientId, identityInfo);
254 return identityInfo;
255 } catch (IOException | InvalidKeyException e) {
256 logger.warn("Failed to load identity key: {}", e.getMessage());
257 return null;
258 }
259 }
260
261 private void storeIdentityLocked(final RecipientId recipientId, final IdentityInfo identityInfo) {
262 logger.trace("Storing identity info for {}, trust: {}, added: {}",
263 recipientId,
264 identityInfo.getTrustLevel(),
265 identityInfo.getDateAdded());
266 cachedIdentities.put(recipientId, identityInfo);
267
268 var storage = new IdentityStorage(Base64.getEncoder().encodeToString(identityInfo.getIdentityKey().serialize()),
269 identityInfo.getTrustLevel().ordinal(),
270 identityInfo.getDateAdded().getTime());
271
272 final var file = getIdentityFile(recipientId);
273 // Write to memory first to prevent corrupting the file in case of serialization errors
274 try (var inMemoryOutput = new ByteArrayOutputStream()) {
275 objectMapper.writeValue(inMemoryOutput, storage);
276
277 var input = new ByteArrayInputStream(inMemoryOutput.toByteArray());
278 try (var outputStream = new FileOutputStream(file)) {
279 input.transferTo(outputStream);
280 }
281 } catch (Exception e) {
282 logger.error("Error saving identity file: {}", e.getMessage());
283 }
284 }
285
286 private void deleteIdentityLocked(final RecipientId recipientId) {
287 cachedIdentities.remove(recipientId);
288
289 final var file = getIdentityFile(recipientId);
290 if (!file.exists()) {
291 return;
292 }
293 try {
294 Files.delete(file.toPath());
295 } catch (IOException e) {
296 logger.error("Failed to delete identity file {}: {}", file, e.getMessage());
297 }
298 }
299
300 private record IdentityStorage(String identityKey, int trustLevel, long addedTimestamp) {}
301 }