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