miden_client/keystore/
fs_keystore.rs

1use alloc::string::String;
2use std::fs::OpenOptions;
3use std::hash::{DefaultHasher, Hash, Hasher};
4use std::io::{BufRead, BufReader, BufWriter, Write};
5use std::path::PathBuf;
6use std::string::ToString;
7use std::sync::{Arc, RwLock};
8use std::vec::Vec;
9
10use miden_objects::account::AuthSecretKey;
11use miden_objects::{Felt, Word};
12use miden_tx::AuthenticationError;
13use miden_tx::auth::{SigningInputs, TransactionAuthenticator};
14use miden_tx::utils::{Deserializable, Serializable};
15use rand::{Rng, SeedableRng};
16
17use super::KeyStoreError;
18
19/// A filesystem-based keystore that stores keys in separate files and provides transaction
20/// authentication functionality. The public key is hashed and the result is used as the filename
21/// and the contents of the file are the serialized public and secret key.
22///
23/// The keystore requires an RNG component for generating Falcon signatures at the moment of
24/// transaction signing.
25#[derive(Debug, Clone)]
26pub struct FilesystemKeyStore<R: Rng + Send + Sync> {
27    /// The random number generator used to generate signatures.
28    rng: Arc<RwLock<R>>,
29    /// The directory where the keys are stored and read from.
30    keys_directory: PathBuf,
31}
32
33impl<R: Rng + Send + Sync> FilesystemKeyStore<R> {
34    pub fn with_rng(keys_directory: PathBuf, rng: R) -> Result<Self, KeyStoreError> {
35        if !keys_directory.exists() {
36            std::fs::create_dir_all(&keys_directory).map_err(|err| {
37                KeyStoreError::StorageError(format!("error creating keys directory: {err:?}"))
38            })?;
39        }
40
41        Ok(FilesystemKeyStore {
42            keys_directory,
43            rng: Arc::new(RwLock::new(rng)),
44        })
45    }
46
47    /// Adds a secret key to the keystore.
48    pub fn add_key(&self, key: &AuthSecretKey) -> Result<(), KeyStoreError> {
49        let pub_key = match key {
50            AuthSecretKey::RpoFalcon512(k) => Word::from(k.public_key()),
51        };
52
53        let filename = hash_pub_key(pub_key);
54
55        let file_path = self.keys_directory.join(filename);
56        let file = OpenOptions::new()
57            .write(true)
58            .create(true)
59            .truncate(true)
60            .open(file_path)
61            .map_err(|err| {
62                KeyStoreError::StorageError(format!("error opening secret key file: {err:?}"))
63            })?;
64
65        let mut writer = BufWriter::new(file);
66        let key_pair_hex = hex::encode(key.to_bytes());
67        writer.write_all(key_pair_hex.as_bytes()).map_err(|err| {
68            KeyStoreError::StorageError(format!("error writing secret key file: {err:?}"))
69        })?;
70
71        Ok(())
72    }
73
74    /// Retrieves a secret key from the keystore given its public key.
75    pub fn get_key(&self, pub_key: Word) -> Result<Option<AuthSecretKey>, KeyStoreError> {
76        let filename = hash_pub_key(pub_key);
77
78        let file_path = self.keys_directory.join(filename);
79        if !file_path.exists() {
80            return Ok(None);
81        }
82
83        let file = OpenOptions::new().read(true).open(file_path).map_err(|err| {
84            KeyStoreError::StorageError(format!("error opening secret key file: {err:?}"))
85        })?;
86        let mut reader = BufReader::new(file);
87        let mut key_pair_hex = String::new();
88        reader.read_line(&mut key_pair_hex).map_err(|err| {
89            KeyStoreError::StorageError(format!("error reading secret key file: {err:?}"))
90        })?;
91
92        let secret_key_bytes = hex::decode(key_pair_hex.trim()).map_err(|err| {
93            KeyStoreError::DecodingError(format!("error decoding secret key hex: {err:?}"))
94        })?;
95        let secret_key =
96            AuthSecretKey::read_from_bytes(secret_key_bytes.as_slice()).map_err(|err| {
97                KeyStoreError::DecodingError(format!(
98                    "error reading secret key from bytes: {err:?}"
99                ))
100            })?;
101
102        Ok(Some(secret_key))
103    }
104}
105
106// Provide a default implementation for `StdRng` so you can call FilesystemKeyStore::new() without
107// type annotations.
108impl FilesystemKeyStore<rand::rngs::StdRng> {
109    /// Creates a new [`FilesystemKeyStore`] using [`rand::rngs::StdRng`] as the RNG.
110    pub fn new(keys_directory: PathBuf) -> Result<Self, KeyStoreError> {
111        use rand::rngs::StdRng;
112        let rng = StdRng::from_os_rng();
113
114        FilesystemKeyStore::with_rng(keys_directory, rng)
115    }
116}
117
118impl<R: Rng + Send + Sync> TransactionAuthenticator for FilesystemKeyStore<R> {
119    /// Gets a signature over a message, given a public key.
120    ///
121    /// The public key should correspond to one of the keys tracked by the keystore.
122    ///
123    /// # Errors
124    /// If the public key isn't found in the store, [`AuthenticationError::UnknownPublicKey`] is
125    /// returned.
126    async fn get_signature(
127        &self,
128        pub_key: Word,
129        signing_info: &SigningInputs,
130    ) -> Result<Vec<Felt>, AuthenticationError> {
131        let mut rng = self.rng.write().expect("poisoned lock");
132
133        let message = signing_info.to_commitment();
134
135        let secret_key = self
136            .get_key(pub_key)
137            .map_err(|err| AuthenticationError::other(err.to_string()))?;
138
139        let AuthSecretKey::RpoFalcon512(k) =
140            secret_key.ok_or(AuthenticationError::UnknownPublicKey(pub_key.to_hex()))?;
141
142        miden_tx::auth::signatures::get_falcon_signature(&k, message, &mut *rng)
143    }
144}
145
146/// Hashes a public key to a string representation.
147fn hash_pub_key(pub_key: Word) -> String {
148    let pub_key = pub_key.to_hex();
149    let mut hasher = DefaultHasher::new();
150    pub_key.hash(&mut hasher);
151    hasher.finish().to_string()
152}