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