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