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