miden_client/keystore/
fs_keystore.rs1use alloc::boxed::Box;
2use alloc::collections::{BTreeMap, BTreeSet};
3use alloc::string::String;
4use std::fs;
5use std::hash::{DefaultHasher, Hash, Hasher};
6use std::io::Write;
7use std::path::{Path, PathBuf};
8use std::string::ToString;
9use std::sync::Arc;
10
11use miden_protocol::Word;
12use miden_protocol::account::AccountId;
13use miden_protocol::account::auth::{AuthSecretKey, PublicKey, PublicKeyCommitment, Signature};
14use miden_tx::AuthenticationError;
15use miden_tx::auth::{SigningInputs, TransactionAuthenticator};
16use miden_tx::utils::serde::{Deserializable, Serializable};
17use miden_tx::utils::sync::RwLock;
18use serde::{Deserialize, Serialize};
19
20use super::{KeyStoreError, Keystore};
21
22const INDEX_FILE_NAME: &str = "key_index.json";
26const INDEX_VERSION: u32 = 1;
27
28#[derive(Debug, Clone, Serialize, Deserialize, Default)]
30struct KeyIndex {
31 version: u32,
32 mappings: BTreeMap<String, BTreeSet<String>>,
34}
35
36impl KeyIndex {
37 fn new() -> Self {
38 Self {
39 version: INDEX_VERSION,
40 mappings: BTreeMap::new(),
41 }
42 }
43
44 fn add_mapping(&mut self, account_id: &AccountId, pub_key_commitment: PublicKeyCommitment) {
46 let account_id_hex = account_id.to_hex();
47 let pub_key_hex = Word::from(pub_key_commitment).to_hex();
48
49 self.mappings.entry(account_id_hex).or_default().insert(pub_key_hex);
50 }
51
52 fn remove_all_mappings_for_key(&mut self, pub_key_commitment: PublicKeyCommitment) {
54 let pub_key_hex = Word::from(pub_key_commitment).to_hex();
55
56 self.mappings.retain(|_, commitments| {
58 commitments.remove(&pub_key_hex);
59 !commitments.is_empty()
60 });
61 }
62
63 fn read_from_file(keys_directory: &Path) -> Result<Self, KeyStoreError> {
65 let index_path = keys_directory.join(INDEX_FILE_NAME);
66
67 if !index_path.exists() {
68 return Ok(Self::new());
69 }
70
71 let contents =
72 fs::read_to_string(&index_path).map_err(keystore_error("error reading index file"))?;
73
74 serde_json::from_str(&contents).map_err(|err| {
75 KeyStoreError::DecodingError(format!("error parsing index file: {err:?}"))
76 })
77 }
78
79 fn write_to_file(&self, keys_directory: &Path) -> Result<(), KeyStoreError> {
81 let index_path = keys_directory.join(INDEX_FILE_NAME);
82
83 let contents = serde_json::to_string_pretty(self).map_err(|err| {
84 KeyStoreError::StorageError(format!("error serializing index: {err:?}"))
85 })?;
86
87 let mut temp_file = tempfile::NamedTempFile::new_in(keys_directory)
90 .map_err(keystore_error("error creating temp index file"))?;
91 temp_file
92 .write_all(contents.as_bytes())
93 .map_err(keystore_error("error writing temp index file"))?;
94 temp_file
95 .as_file()
96 .sync_all()
97 .map_err(keystore_error("error syncing temp index file"))?;
98
99 temp_file
101 .persist(&index_path)
102 .map_err(|err| keystore_error("error renaming index file")(err.error))?;
103
104 Ok(())
105 }
106
107 fn get_account_id(&self, pub_key_commitment: PublicKeyCommitment) -> Option<AccountId> {
112 let pub_key_hex = Word::from(pub_key_commitment).to_hex();
113
114 for (account_id_hex, commitments) in &self.mappings {
115 if commitments.contains(&pub_key_hex) {
116 return AccountId::from_hex(account_id_hex).ok();
117 }
118 }
119
120 None
121 }
122
123 fn get_commitments(
125 &self,
126 account_id: &AccountId,
127 ) -> Result<BTreeSet<PublicKeyCommitment>, KeyStoreError> {
128 let account_id_hex = account_id.to_hex();
129
130 self.mappings
131 .get(&account_id_hex)
132 .map(|commitments| {
133 commitments
134 .iter()
135 .filter_map(|hex| {
136 Word::try_from(hex.as_str()).ok().map(PublicKeyCommitment::from)
137 })
138 .collect()
139 })
140 .ok_or_else(|| {
141 KeyStoreError::StorageError(format!("account not found {account_id_hex}"))
142 })
143 }
144}
145
146#[derive(Debug)]
155pub struct FilesystemKeyStore {
156 pub keys_directory: PathBuf,
158 index: RwLock<KeyIndex>,
160}
161
162impl Clone for FilesystemKeyStore {
163 fn clone(&self) -> Self {
164 let index = self.index.read().clone();
165 Self {
166 keys_directory: self.keys_directory.clone(),
167 index: RwLock::new(index),
168 }
169 }
170}
171
172impl FilesystemKeyStore {
173 pub fn new(keys_directory: PathBuf) -> Result<Self, KeyStoreError> {
175 if !keys_directory.exists() {
176 fs::create_dir_all(&keys_directory)
177 .map_err(keystore_error("error creating keys directory"))?;
178 }
179
180 let index = KeyIndex::read_from_file(&keys_directory)?;
181
182 Ok(FilesystemKeyStore {
183 keys_directory,
184 index: RwLock::new(index),
185 })
186 }
187
188 fn add_key_without_account(&self, key: &AuthSecretKey) -> Result<(), KeyStoreError> {
192 let pub_key_commitment = key.public_key().to_commitment();
193 let file_path = key_file_path(&self.keys_directory, pub_key_commitment);
194 write_secret_key_file(&file_path, key)
195 }
196
197 pub fn get_key_sync(
199 &self,
200 pub_key: PublicKeyCommitment,
201 ) -> Result<Option<AuthSecretKey>, KeyStoreError> {
202 let file_path = key_file_path(&self.keys_directory, pub_key);
203 match fs::read(&file_path) {
204 Ok(bytes) => {
205 let key = AuthSecretKey::read_from_bytes(&bytes).map_err(|err| {
206 KeyStoreError::DecodingError(format!(
207 "error reading secret key from file: {err:?}"
208 ))
209 })?;
210 Ok(Some(key))
211 },
212 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
213 Err(e) => Err(keystore_error("error reading secret key file")(e)),
214 }
215 }
216
217 fn save_index(&self) -> Result<(), KeyStoreError> {
219 let index = self.index.read();
220 index.write_to_file(&self.keys_directory)
221 }
222}
223
224impl TransactionAuthenticator for FilesystemKeyStore {
225 async fn get_signature(
233 &self,
234 pub_key: PublicKeyCommitment,
235 signing_info: &SigningInputs,
236 ) -> Result<Signature, AuthenticationError> {
237 let message = signing_info.to_commitment();
238
239 let secret_key = self
240 .get_key_sync(pub_key)
241 .map_err(|err| {
242 AuthenticationError::other_with_source("failed to load secret key", err)
243 })?
244 .ok_or(AuthenticationError::UnknownPublicKey(pub_key))?;
245
246 let signature = secret_key.sign(message);
247
248 Ok(signature)
249 }
250
251 async fn get_public_key(
253 &self,
254 pub_key_commitment: PublicKeyCommitment,
255 ) -> Option<Arc<PublicKey>> {
256 self.get_key(pub_key_commitment)
257 .await
258 .ok()
259 .flatten()
260 .map(|key| Arc::new(key.public_key()))
261 }
262}
263
264#[async_trait::async_trait]
265impl Keystore for FilesystemKeyStore {
266 async fn add_key(
267 &self,
268 key: &AuthSecretKey,
269 account_id: AccountId,
270 ) -> Result<(), KeyStoreError> {
271 let pub_key_commitment = key.public_key().to_commitment();
272
273 self.add_key_without_account(key)?;
275
276 {
278 let mut index = self.index.write();
279 index.add_mapping(&account_id, pub_key_commitment);
280 }
281
282 self.save_index()?;
284
285 Ok(())
286 }
287
288 async fn remove_key(&self, pub_key: PublicKeyCommitment) -> Result<(), KeyStoreError> {
289 {
291 let mut index = self.index.write();
292 index.remove_all_mappings_for_key(pub_key);
293 }
294
295 self.save_index()?;
297
298 let file_path = key_file_path(&self.keys_directory, pub_key);
300 match fs::remove_file(file_path) {
301 Ok(()) => {},
302 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {},
303 Err(e) => return Err(keystore_error("error removing secret key file")(e)),
304 }
305
306 Ok(())
307 }
308
309 async fn get_key(
310 &self,
311 pub_key: PublicKeyCommitment,
312 ) -> Result<Option<AuthSecretKey>, KeyStoreError> {
313 self.get_key_sync(pub_key)
314 }
315
316 async fn get_account_id_by_key_commitment(
317 &self,
318 pub_key_commitment: PublicKeyCommitment,
319 ) -> Result<Option<AccountId>, KeyStoreError> {
320 let index = self.index.read();
321 Ok(index.get_account_id(pub_key_commitment))
322 }
323
324 async fn get_account_key_commitments(
325 &self,
326 account_id: &AccountId,
327 ) -> Result<BTreeSet<PublicKeyCommitment>, KeyStoreError> {
328 let index = self.index.read();
329 index.get_commitments(account_id)
330 }
331}
332
333fn key_file_path(keys_directory: &Path, pub_key: PublicKeyCommitment) -> PathBuf {
338 let filename = hash_pub_key(pub_key.into());
339 keys_directory.join(filename)
340}
341
342#[cfg(unix)]
344fn write_secret_key_file(file_path: &Path, key: &AuthSecretKey) -> Result<(), KeyStoreError> {
345 use std::io::Write;
346 use std::os::unix::fs::OpenOptionsExt;
347 let mut file = fs::OpenOptions::new()
348 .write(true)
349 .create(true)
350 .truncate(true)
351 .mode(0o600)
352 .open(file_path)
353 .map_err(keystore_error("error writing secret key file"))?;
354 file.write_all(&key.to_bytes())
355 .map_err(keystore_error("error writing secret key file"))
356}
357
358#[cfg(not(unix))]
361fn write_secret_key_file(file_path: &Path, key: &AuthSecretKey) -> Result<(), KeyStoreError> {
362 fs::write(file_path, key.to_bytes()).map_err(keystore_error("error writing secret key file"))
363}
364
365fn keystore_error(context: &str) -> impl FnOnce(std::io::Error) -> KeyStoreError {
366 move |err| KeyStoreError::StorageError(format!("{context}: {err:?}"))
367}
368
369fn hash_pub_key(pub_key: Word) -> String {
371 let pub_key = pub_key.to_hex();
372 let mut hasher = DefaultHasher::new();
373 pub_key.hash(&mut hasher);
374 hasher.finish().to_string()
375}