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