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::sync::RwLock;
17use miden_tx::utils::{Deserializable, Serializable};
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 let temp_path = std::env::temp_dir().join(INDEX_FILE_NAME);
83
84 let contents = serde_json::to_string_pretty(self).map_err(|err| {
85 KeyStoreError::StorageError(format!("error serializing index: {err:?}"))
86 })?;
87
88 let mut file = fs::File::create(&temp_path)
90 .map_err(keystore_error("error creating temp index file"))?;
91 file.write_all(contents.as_bytes())
92 .map_err(keystore_error("error writing temp index file"))?;
93 file.sync_all().map_err(keystore_error("error syncing temp index file"))?;
94
95 fs::rename(&temp_path, &index_path).map_err(keystore_error("error renaming index file"))
97 }
98
99 fn get_account_id(&self, pub_key_commitment: PublicKeyCommitment) -> Option<AccountId> {
104 let pub_key_hex = Word::from(pub_key_commitment).to_hex();
105
106 for (account_id_hex, commitments) in &self.mappings {
107 if commitments.contains(&pub_key_hex) {
108 return AccountId::from_hex(account_id_hex).ok();
109 }
110 }
111
112 None
113 }
114
115 fn get_commitments(
117 &self,
118 account_id: &AccountId,
119 ) -> Result<BTreeSet<PublicKeyCommitment>, KeyStoreError> {
120 let account_id_hex = account_id.to_hex();
121
122 self.mappings
123 .get(&account_id_hex)
124 .map(|commitments| {
125 commitments
126 .iter()
127 .filter_map(|hex| {
128 Word::try_from(hex.as_str()).ok().map(PublicKeyCommitment::from)
129 })
130 .collect()
131 })
132 .ok_or_else(|| {
133 KeyStoreError::StorageError(format!("account not found {account_id_hex}"))
134 })
135 }
136}
137
138#[derive(Debug)]
147pub struct FilesystemKeyStore {
148 pub keys_directory: PathBuf,
150 index: RwLock<KeyIndex>,
152}
153
154impl Clone for FilesystemKeyStore {
155 fn clone(&self) -> Self {
156 let index = self.index.read().clone();
157 Self {
158 keys_directory: self.keys_directory.clone(),
159 index: RwLock::new(index),
160 }
161 }
162}
163
164impl FilesystemKeyStore {
165 pub fn new(keys_directory: PathBuf) -> Result<Self, KeyStoreError> {
167 if !keys_directory.exists() {
168 fs::create_dir_all(&keys_directory)
169 .map_err(keystore_error("error creating keys directory"))?;
170 }
171
172 let index = KeyIndex::read_from_file(&keys_directory)?;
173
174 Ok(FilesystemKeyStore {
175 keys_directory,
176 index: RwLock::new(index),
177 })
178 }
179
180 fn add_key_without_account(&self, key: &AuthSecretKey) -> Result<(), KeyStoreError> {
184 let pub_key_commitment = key.public_key().to_commitment();
185 let file_path = key_file_path(&self.keys_directory, pub_key_commitment);
186 write_secret_key_file(&file_path, key)
187 }
188
189 pub fn get_key_sync(
191 &self,
192 pub_key: PublicKeyCommitment,
193 ) -> Result<Option<AuthSecretKey>, KeyStoreError> {
194 let file_path = key_file_path(&self.keys_directory, pub_key);
195 match fs::read(&file_path) {
196 Ok(bytes) => {
197 let key = AuthSecretKey::read_from_bytes(&bytes).map_err(|err| {
198 KeyStoreError::DecodingError(format!(
199 "error reading secret key from file: {err:?}"
200 ))
201 })?;
202 Ok(Some(key))
203 },
204 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
205 Err(e) => Err(keystore_error("error reading secret key file")(e)),
206 }
207 }
208
209 fn save_index(&self) -> Result<(), KeyStoreError> {
211 let index = self.index.read();
212 index.write_to_file(&self.keys_directory)
213 }
214}
215
216impl TransactionAuthenticator for FilesystemKeyStore {
217 async fn get_signature(
225 &self,
226 pub_key: PublicKeyCommitment,
227 signing_info: &SigningInputs,
228 ) -> Result<Signature, AuthenticationError> {
229 let message = signing_info.to_commitment();
230
231 let secret_key = self
232 .get_key_sync(pub_key)
233 .map_err(|err| {
234 AuthenticationError::other_with_source("failed to load secret key", err)
235 })?
236 .ok_or(AuthenticationError::UnknownPublicKey(pub_key))?;
237
238 let signature = secret_key.sign(message);
239
240 Ok(signature)
241 }
242
243 async fn get_public_key(
245 &self,
246 pub_key_commitment: PublicKeyCommitment,
247 ) -> Option<Arc<PublicKey>> {
248 self.get_key(pub_key_commitment)
249 .await
250 .ok()
251 .flatten()
252 .map(|key| Arc::new(key.public_key()))
253 }
254}
255
256#[async_trait::async_trait]
257impl Keystore for FilesystemKeyStore {
258 async fn add_key(
259 &self,
260 key: &AuthSecretKey,
261 account_id: AccountId,
262 ) -> Result<(), KeyStoreError> {
263 let pub_key_commitment = key.public_key().to_commitment();
264
265 self.add_key_without_account(key)?;
267
268 {
270 let mut index = self.index.write();
271 index.add_mapping(&account_id, pub_key_commitment);
272 }
273
274 self.save_index()?;
276
277 Ok(())
278 }
279
280 async fn remove_key(&self, pub_key: PublicKeyCommitment) -> Result<(), KeyStoreError> {
281 {
283 let mut index = self.index.write();
284 index.remove_all_mappings_for_key(pub_key);
285 }
286
287 self.save_index()?;
289
290 let file_path = key_file_path(&self.keys_directory, pub_key);
292 match fs::remove_file(file_path) {
293 Ok(()) => {},
294 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {},
295 Err(e) => return Err(keystore_error("error removing secret key file")(e)),
296 }
297
298 Ok(())
299 }
300
301 async fn get_key(
302 &self,
303 pub_key: PublicKeyCommitment,
304 ) -> Result<Option<AuthSecretKey>, KeyStoreError> {
305 self.get_key_sync(pub_key)
306 }
307
308 async fn get_account_id_by_key_commitment(
309 &self,
310 pub_key_commitment: PublicKeyCommitment,
311 ) -> Result<Option<AccountId>, KeyStoreError> {
312 let index = self.index.read();
313 Ok(index.get_account_id(pub_key_commitment))
314 }
315
316 async fn get_account_key_commitments(
317 &self,
318 account_id: &AccountId,
319 ) -> Result<BTreeSet<PublicKeyCommitment>, KeyStoreError> {
320 let index = self.index.read();
321 index.get_commitments(account_id)
322 }
323}
324
325fn key_file_path(keys_directory: &Path, pub_key: PublicKeyCommitment) -> PathBuf {
330 let filename = hash_pub_key(pub_key.into());
331 keys_directory.join(filename)
332}
333
334#[cfg(unix)]
336fn write_secret_key_file(file_path: &Path, key: &AuthSecretKey) -> Result<(), KeyStoreError> {
337 use std::io::Write;
338 use std::os::unix::fs::OpenOptionsExt;
339 let mut file = fs::OpenOptions::new()
340 .write(true)
341 .create(true)
342 .truncate(true)
343 .mode(0o600)
344 .open(file_path)
345 .map_err(keystore_error("error writing secret key file"))?;
346 file.write_all(&key.to_bytes())
347 .map_err(keystore_error("error writing secret key file"))
348}
349
350#[cfg(not(unix))]
353fn write_secret_key_file(file_path: &Path, key: &AuthSecretKey) -> Result<(), KeyStoreError> {
354 fs::write(file_path, key.to_bytes()).map_err(keystore_error("error writing secret key file"))
355}
356
357fn keystore_error(context: &str) -> impl FnOnce(std::io::Error) -> KeyStoreError {
358 move |err| KeyStoreError::StorageError(format!("{context}: {err:?}"))
359}
360
361fn hash_pub_key(pub_key: Word) -> String {
363 let pub_key = pub_key.to_hex();
364 let mut hasher = DefaultHasher::new();
365 pub_key.hash(&mut hasher);
366 hasher.finish().to_string()
367}