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