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