]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java
0cbdf347e218ae952ef7452c9bf205b640ec4f48
[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.asamk.signal.manager.util.Utils;
10 import org.slf4j.Logger;
11 import org.slf4j.LoggerFactory;
12 import org.whispersystems.libsignal.IdentityKey;
13 import org.whispersystems.libsignal.IdentityKeyPair;
14 import org.whispersystems.libsignal.InvalidKeyException;
15 import org.whispersystems.libsignal.SignalProtocolAddress;
16
17 import java.io.ByteArrayInputStream;
18 import java.io.ByteArrayOutputStream;
19 import java.io.File;
20 import java.io.FileInputStream;
21 import java.io.FileOutputStream;
22 import java.io.IOException;
23 import java.nio.file.Files;
24 import java.util.Arrays;
25 import java.util.Base64;
26 import java.util.Date;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Map;
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 -> RecipientId.of(Integer.parseInt(f.getName())))
166 .map(this::loadIdentityLocked)
167 .collect(Collectors.toList());
168 }
169
170 public void mergeRecipients(final RecipientId recipientId, final RecipientId toBeMergedRecipientId) {
171 synchronized (cachedIdentities) {
172 deleteIdentityLocked(toBeMergedRecipientId);
173 }
174 }
175
176 /**
177 * @param identifier can be either a serialized uuid or a e164 phone number
178 */
179 private RecipientId resolveRecipient(String identifier) {
180 return resolver.resolveRecipient(Utils.getSignalServiceAddressFromIdentifier(identifier));
181 }
182
183 private File getIdentityFile(final RecipientId recipientId) {
184 try {
185 IOUtils.createPrivateDirectories(identitiesPath);
186 } catch (IOException e) {
187 throw new AssertionError("Failed to create identities path", e);
188 }
189 return new File(identitiesPath, String.valueOf(recipientId.getId()));
190 }
191
192 private IdentityInfo loadIdentityLocked(final RecipientId recipientId) {
193 {
194 final var session = cachedIdentities.get(recipientId);
195 if (session != null) {
196 return session;
197 }
198 }
199
200 final var file = getIdentityFile(recipientId);
201 if (!file.exists()) {
202 return null;
203 }
204 try (var inputStream = new FileInputStream(file)) {
205 var storage = objectMapper.readValue(inputStream, IdentityStorage.class);
206
207 var id = new IdentityKey(Base64.getDecoder().decode(storage.getIdentityKey()));
208 var trustLevel = TrustLevel.fromInt(storage.getTrustLevel());
209 var added = new Date(storage.getAddedTimestamp());
210
211 final var identityInfo = new IdentityInfo(recipientId, id, trustLevel, added);
212 cachedIdentities.put(recipientId, identityInfo);
213 return identityInfo;
214 } catch (IOException | InvalidKeyException e) {
215 logger.warn("Failed to load identity key: {}", e.getMessage());
216 return null;
217 }
218 }
219
220 private void storeIdentityLocked(final RecipientId recipientId, final IdentityInfo identityInfo) {
221 cachedIdentities.put(recipientId, identityInfo);
222
223 var storage = new IdentityStorage(Base64.getEncoder().encodeToString(identityInfo.getIdentityKey().serialize()),
224 identityInfo.getTrustLevel().ordinal(),
225 identityInfo.getDateAdded().getTime());
226
227 final var file = getIdentityFile(recipientId);
228 // Write to memory first to prevent corrupting the file in case of serialization errors
229 try (var inMemoryOutput = new ByteArrayOutputStream()) {
230 objectMapper.writeValue(inMemoryOutput, storage);
231
232 var input = new ByteArrayInputStream(inMemoryOutput.toByteArray());
233 try (var outputStream = new FileOutputStream(file)) {
234 input.transferTo(outputStream);
235 }
236 } catch (Exception e) {
237 logger.error("Error saving identity file: {}", e.getMessage());
238 }
239 }
240
241 private void deleteIdentityLocked(final RecipientId recipientId) {
242 cachedIdentities.remove(recipientId);
243
244 final var file = getIdentityFile(recipientId);
245 if (!file.exists()) {
246 return;
247 }
248 try {
249 Files.delete(file.toPath());
250 } catch (IOException e) {
251 logger.error("Failed to delete identity file {}: {}", file, e.getMessage());
252 }
253 }
254
255 private static final class IdentityStorage {
256
257 private String identityKey;
258 private int trustLevel;
259 private long addedTimestamp;
260
261 // For deserialization
262 private IdentityStorage() {
263 }
264
265 private IdentityStorage(final String identityKey, final int trustLevel, final long addedTimestamp) {
266 this.identityKey = identityKey;
267 this.trustLevel = trustLevel;
268 this.addedTimestamp = addedTimestamp;
269 }
270
271 public String getIdentityKey() {
272 return identityKey;
273 }
274
275 public int getTrustLevel() {
276 return trustLevel;
277 }
278
279 public long getAddedTimestamp() {
280 return addedTimestamp;
281 }
282 }
283 }