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 async fn get_signature(
231 &self,
232 pub_key: PublicKeyCommitment,
233 signing_info: &SigningInputs,
234 ) -> Result<Signature, AuthenticationError> {
235 let message = signing_info.to_commitment();
236
237 let secret_key = self
238 .get_key_sync(pub_key)
239 .map_err(|err| {
240 AuthenticationError::other_with_source("failed to load secret key", err)
241 })?
242 .ok_or(AuthenticationError::UnknownPublicKey(pub_key))?;
243
244 let signature = secret_key.sign(message);
245
246 Ok(signature)
247 }
248
249 async fn get_public_key(
251 &self,
252 pub_key_commitment: PublicKeyCommitment,
253 ) -> Option<Arc<PublicKey>> {
254 self.get_key(pub_key_commitment)
255 .await
256 .ok()
257 .flatten()
258 .map(|key| Arc::new(key.public_key()))
259 }
260}
261
262#[async_trait::async_trait]
263impl Keystore for FilesystemKeyStore {
264 async fn add_key(
265 &self,
266 key: &AuthSecretKey,
267 account_id: AccountId,
268 ) -> Result<(), KeyStoreError> {
269 let pub_key_commitment = key.public_key().to_commitment();
270
271 self.add_key_without_account(key)?;
273
274 {
276 let mut index = self.index.write();
277 index.add_mapping(&account_id, pub_key_commitment);
278 }
279
280 self.save_index()?;
282
283 Ok(())
284 }
285
286 async fn remove_key(&self, pub_key: PublicKeyCommitment) -> Result<(), KeyStoreError> {
287 {
289 let mut index = self.index.write();
290 index.remove_all_mappings_for_key(pub_key);
291 }
292
293 self.save_index()?;
295
296 let file_path = key_file_path(&self.keys_directory, pub_key);
298 match fs::remove_file(file_path) {
299 Ok(()) => {},
300 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {},
301 Err(e) => return Err(keystore_error("error removing secret key file")(e)),
302 }
303
304 Ok(())
305 }
306
307 async fn get_key(
308 &self,
309 pub_key: PublicKeyCommitment,
310 ) -> Result<Option<AuthSecretKey>, KeyStoreError> {
311 self.get_key_sync(pub_key)
312 }
313
314 async fn get_account_id_by_key_commitment(
315 &self,
316 pub_key_commitment: PublicKeyCommitment,
317 ) -> Result<Option<AccountId>, KeyStoreError> {
318 let index = self.index.read();
319 Ok(index.get_account_id(pub_key_commitment))
320 }
321
322 async fn get_account_key_commitments(
323 &self,
324 account_id: &AccountId,
325 ) -> Result<BTreeSet<PublicKeyCommitment>, KeyStoreError> {
326 let index = self.index.read();
327 index.get_commitments(account_id)
328 }
329}
330
331fn key_file_path(keys_directory: &Path, pub_key_commitment: PublicKeyCommitment) -> PathBuf {
336 let filename = Word::from(pub_key_commitment).to_hex();
337 keys_directory.join(filename)
338}
339
340#[cfg(unix)]
342fn write_secret_key_file(file_path: &Path, key: &AuthSecretKey) -> Result<(), KeyStoreError> {
343 use std::io::Write;
344 use std::os::unix::fs::OpenOptionsExt;
345 let mut file = fs::OpenOptions::new()
346 .write(true)
347 .create(true)
348 .truncate(true)
349 .mode(0o600)
350 .open(file_path)
351 .map_err(keystore_error("error writing secret key file"))?;
352 file.write_all(&key.to_bytes())
353 .map_err(keystore_error("error writing secret key file"))
354}
355
356#[cfg(not(unix))]
359fn write_secret_key_file(file_path: &Path, key: &AuthSecretKey) -> Result<(), KeyStoreError> {
360 fs::write(file_path, key.to_bytes()).map_err(keystore_error("error writing secret key file"))
361}
362
363fn keystore_error(context: &str) -> impl FnOnce(std::io::Error) -> KeyStoreError {
364 move |err| KeyStoreError::StorageError(format!("{context}: {err:?}"))
365}