forest/key_management/
keystore.rs

1// Copyright 2019-2025 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use std::{
5    fmt::Display,
6    fs::{File, create_dir},
7    io::{BufReader, BufWriter, ErrorKind, Read, Write},
8    path::{Path, PathBuf},
9};
10
11use crate::{
12    shim::crypto::SignatureType,
13    utils::{encoding::from_slice_with_fallback, io::create_new_sensitive_file},
14};
15use ahash::{HashMap, HashMapExt};
16use argon2::{
17    Argon2, ParamsBuilder, PasswordHasher, RECOMMENDED_SALT_LEN, password_hash::SaltString,
18};
19use base64::{Engine, prelude::BASE64_STANDARD};
20use crypto_secretbox::{
21    KeyInit, SecretBox, XSalsa20Poly1305,
22    aead::{Aead, generic_array::GenericArray},
23};
24use rand::RngCore;
25use serde::{Deserialize, Serialize};
26use thiserror::Error;
27use tracing::{error, warn};
28
29use super::errors::Error;
30
31const NONCE_SIZE: usize = SecretBox::<Box<dyn std::any::Any>>::NONCE_SIZE;
32
33pub const KEYSTORE_NAME: &str = "keystore.json";
34pub const ENCRYPTED_KEYSTORE_NAME: &str = "keystore";
35
36/// Environmental variable which holds the `KeyStore` encryption phrase.
37pub const FOREST_KEYSTORE_PHRASE_ENV: &str = "FOREST_KEYSTORE_PHRASE";
38
39type SaltByteArray = [u8; RECOMMENDED_SALT_LEN];
40
41/// `KeyInfo` structure, this contains the type of key (stored as a string) and
42/// the private key. Note how the private key is stored as a byte vector
43#[cfg_attr(test, derive(derive_quickcheck_arbitrary::Arbitrary))]
44#[derive(Clone, PartialEq, Debug, Eq, Serialize, Deserialize)]
45pub struct KeyInfo {
46    key_type: SignatureType,
47    // Vec<u8> is used because The private keys for BLS and SECP256K1 are not of the same type
48    private_key: Vec<u8>,
49}
50
51#[derive(Clone, PartialEq, Debug, Eq, Serialize, Deserialize)]
52pub struct PersistentKeyInfo {
53    key_type: SignatureType,
54    private_key: String,
55}
56
57impl KeyInfo {
58    /// Return a new `KeyInfo` given the key type and private key
59    pub fn new(key_type: SignatureType, private_key: Vec<u8>) -> Self {
60        KeyInfo {
61            key_type,
62            private_key,
63        }
64    }
65
66    /// Return a reference to the key's signature type
67    pub fn key_type(&self) -> &SignatureType {
68        &self.key_type
69    }
70
71    /// Return a reference to the private key
72    pub fn private_key(&self) -> &Vec<u8> {
73        &self.private_key
74    }
75}
76
77/// `KeyStore` structure, this contains a set of `KeyInfos` indexed by address.
78#[derive(Clone, PartialEq, Debug, Eq)]
79pub struct KeyStore {
80    key_info: HashMap<String, KeyInfo>,
81    persistence: Option<PersistentKeyStore>,
82    encryption: Option<EncryptedKeyStore>,
83}
84
85pub enum KeyStoreConfig {
86    Memory,
87    Persistent(PathBuf),
88    Encrypted(PathBuf, String),
89}
90
91/// Persistent `KeyStore` in JSON clear text in `KEYSTORE_LOCATION`
92#[derive(Clone, PartialEq, Debug, Eq)]
93struct PersistentKeyStore {
94    file_path: PathBuf,
95}
96
97/// Encrypted `KeyStore`
98/// `Argon2id` hash key derivation
99/// `XSalsa20Poly1305` authenticated encryption
100/// CBOR encoding
101#[derive(Clone, PartialEq, Debug, Eq)]
102struct EncryptedKeyStore {
103    salt: SaltByteArray,
104    encryption_key: Vec<u8>,
105}
106
107#[derive(Debug, Error)]
108pub enum EncryptedKeyStoreError {
109    /// An error occurred while encrypting keys
110    #[error("Error encrypting data")]
111    EncryptionError,
112}
113
114impl KeyStore {
115    pub fn new(config: KeyStoreConfig) -> Result<Self, Error> {
116        match config {
117            KeyStoreConfig::Memory => Ok(Self {
118                key_info: HashMap::new(),
119                persistence: None,
120                encryption: None,
121            }),
122            KeyStoreConfig::Persistent(location) => {
123                let file_path = location.join(KEYSTORE_NAME);
124
125                match File::open(&file_path) {
126                    Ok(file) => {
127                        let reader = BufReader::new(file);
128
129                        // Existing cleartext JSON keystore
130                        let persisted_key_info: HashMap<String, PersistentKeyInfo> =
131                            serde_json::from_reader(reader)
132                                .inspect_err(|error| {
133                                    error!(%error, "failed to deserialize keyfile, initializing new keystore at: {}.", file_path.display());
134                                })
135                                .unwrap_or_default();
136
137                        let mut key_info = HashMap::new();
138                        for (key, value) in persisted_key_info.iter() {
139                            key_info.insert(
140                                key.to_string(),
141                                KeyInfo {
142                                    private_key: BASE64_STANDARD
143                                        .decode(value.private_key.clone())
144                                        .map_err(|error| Error::Other(error.to_string()))?,
145                                    key_type: value.key_type,
146                                },
147                            );
148                        }
149
150                        Ok(Self {
151                            key_info,
152                            persistence: Some(PersistentKeyStore { file_path }),
153                            encryption: None,
154                        })
155                    }
156                    Err(e) => {
157                        if e.kind() == ErrorKind::NotFound {
158                            warn!(
159                                "Keystore does not exist, initializing new keystore at: {}",
160                                file_path.display()
161                            );
162                            Ok(Self {
163                                key_info: HashMap::new(),
164                                persistence: Some(PersistentKeyStore { file_path }),
165                                encryption: None,
166                            })
167                        } else {
168                            Err(Error::Other(e.to_string()))
169                        }
170                    }
171                }
172            }
173            KeyStoreConfig::Encrypted(location, passphrase) => {
174                if !location.exists() {
175                    create_dir(location.clone())?;
176                }
177
178                let file_path = location.join(Path::new(ENCRYPTED_KEYSTORE_NAME));
179
180                if !file_path.exists() {
181                    File::create(file_path.clone())?;
182                }
183
184                match File::open(&file_path) {
185                    Ok(file) => {
186                        let mut reader = BufReader::new(file);
187                        let mut buf = vec![];
188                        let read_bytes = reader.read_to_end(&mut buf)?;
189
190                        if read_bytes == 0 {
191                            // New encrypted keystore if file exists but is zero bytes (i.e., touch)
192                            warn!(
193                                "Keystore does not exist, initializing new keystore at {:?}",
194                                file_path
195                            );
196
197                            let (salt, encryption_key) =
198                                EncryptedKeyStore::derive_key(&passphrase, None).map_err(
199                                    |error| {
200                                        error!("Failed to create key from passphrase");
201                                        Error::Other(error.to_string())
202                                    },
203                                )?;
204                            Ok(Self {
205                                key_info: HashMap::new(),
206                                persistence: Some(PersistentKeyStore { file_path }),
207                                encryption: Some(EncryptedKeyStore {
208                                    salt,
209                                    encryption_key,
210                                }),
211                            })
212                        } else {
213                            // Existing encrypted keystore
214                            // Split off data from prepended salt
215                            let data = buf.split_off(RECOMMENDED_SALT_LEN);
216                            let mut prev_salt = [0; RECOMMENDED_SALT_LEN];
217                            prev_salt.copy_from_slice(&buf);
218                            let (salt, encryption_key) =
219                                EncryptedKeyStore::derive_key(&passphrase, Some(prev_salt))
220                                    .map_err(|error| {
221                                        error!("Failed to create key from passphrase");
222                                        Error::Other(error.to_string())
223                                    })?;
224
225                            let decrypted_data = EncryptedKeyStore::decrypt(&encryption_key, &data)
226                                .map_err(|error| Error::Other(error.to_string()))?;
227
228                            let key_info = from_slice_with_fallback(&decrypted_data)
229                                .inspect_err(|error| {
230                                    error!(%error, "Failed to deserialize keyfile, initializing new");
231                                })
232                                .unwrap_or_default();
233
234                            Ok(Self {
235                                key_info,
236                                persistence: Some(PersistentKeyStore { file_path }),
237                                encryption: Some(EncryptedKeyStore {
238                                    salt,
239                                    encryption_key,
240                                }),
241                            })
242                        }
243                    }
244                    Err(_) => {
245                        warn!("Encrypted keystore does not exist, initializing new keystore");
246
247                        let (salt, encryption_key) =
248                            EncryptedKeyStore::derive_key(&passphrase, None).map_err(|error| {
249                                error!("Failed to create key from passphrase");
250                                Error::Other(error.to_string())
251                            })?;
252
253                        Ok(Self {
254                            key_info: HashMap::new(),
255                            persistence: Some(PersistentKeyStore { file_path }),
256                            encryption: Some(EncryptedKeyStore {
257                                salt,
258                                encryption_key,
259                            }),
260                        })
261                    }
262                }
263            }
264        }
265    }
266
267    pub fn flush(&self) -> anyhow::Result<()> {
268        match &self.persistence {
269            Some(persistent_keystore) => {
270                let file = create_new_sensitive_file(&persistent_keystore.file_path)?;
271
272                let mut writer = BufWriter::new(file);
273
274                match &self.encryption {
275                    Some(encrypted_keystore) => {
276                        // Flush For EncryptedKeyStore
277                        let data = serde_ipld_dagcbor::to_vec(&self.key_info).map_err(|e| {
278                            Error::Other(format!("failed to serialize and write key info: {e}"))
279                        })?;
280
281                        let encrypted_data =
282                            EncryptedKeyStore::encrypt(&encrypted_keystore.encryption_key, &data)?;
283                        let mut salt_vec = encrypted_keystore.salt.to_vec();
284                        salt_vec.extend(encrypted_data);
285                        writer.write_all(&salt_vec)?;
286
287                        Ok(())
288                    }
289                    None => {
290                        let mut key_info: HashMap<String, PersistentKeyInfo> = HashMap::new();
291                        for (key, value) in self.key_info.iter() {
292                            key_info.insert(
293                                key.to_string(),
294                                PersistentKeyInfo {
295                                    private_key: BASE64_STANDARD.encode(value.private_key.clone()),
296                                    key_type: value.key_type,
297                                },
298                            );
299                        }
300
301                        // Flush for PersistentKeyStore
302                        serde_json::to_writer_pretty(writer, &key_info).map_err(|e| {
303                            Error::Other(format!("failed to serialize and write key info: {e}"))
304                        })?;
305
306                        Ok(())
307                    }
308                }
309            }
310            None => {
311                // NoOp for MemKeyStore
312                Ok(())
313            }
314        }
315    }
316
317    /// Return all of the keys that are stored in the `KeyStore`
318    pub fn list(&self) -> Vec<String> {
319        self.key_info.keys().cloned().collect()
320    }
321
322    /// Return `KeyInfo` that corresponds to a given key
323    pub fn get(&self, k: &str) -> Result<KeyInfo, Error> {
324        self.key_info.get(k).cloned().ok_or(Error::KeyInfo)
325    }
326
327    /// Save a key/`KeyInfo` pair to the `KeyStore`
328    pub fn put(&mut self, key: &str, key_info: KeyInfo) -> Result<(), Error> {
329        if self.key_info.contains_key(key) {
330            return Err(Error::KeyExists);
331        }
332        self.key_info.insert(key.to_string(), key_info);
333
334        if self.persistence.is_some() {
335            self.flush().map_err(|err| Error::Other(err.to_string()))?;
336        }
337
338        Ok(())
339    }
340
341    /// Remove the key and corresponding `KeyInfo` from the `KeyStore`
342    pub fn remove(&mut self, key: &str) -> anyhow::Result<KeyInfo> {
343        let key_out = self.key_info.remove(key).ok_or(Error::KeyInfo)?;
344
345        if self.persistence.is_some() {
346            self.flush()?;
347        }
348
349        Ok(key_out)
350    }
351}
352
353impl EncryptedKeyStore {
354    fn derive_key(
355        passphrase: &str,
356        prev_salt: Option<SaltByteArray>,
357    ) -> anyhow::Result<(SaltByteArray, Vec<u8>)> {
358        let salt = match prev_salt {
359            Some(prev_salt) => prev_salt,
360            None => {
361                let mut salt = [0; RECOMMENDED_SALT_LEN];
362                crate::utils::rand::forest_os_rng().fill_bytes(&mut salt);
363                salt
364            }
365        };
366
367        let mut param_builder = ParamsBuilder::new();
368        // #define crypto_pwhash_argon2id_MEMLIMIT_INTERACTIVE 67108864U
369        // see <https://github.com/jedisct1/libsodium/blob/089f850608737f9d969157092988cb274fe7f8d4/src/libsodium/include/sodium/crypto_pwhash_argon2id.h#L70>
370        const CRYPTO_PWHASH_ARGON2ID_MEMLIMIT_INTERACTIVE: u32 = 67108864;
371        // #define crypto_pwhash_argon2id_OPSLIMIT_INTERACTIVE 2U
372        // see <https://github.com/jedisct1/libsodium/blob/089f850608737f9d969157092988cb274fe7f8d4/src/libsodium/include/sodium/crypto_pwhash_argon2id.h#L66>
373        const CRYPTO_PWHASH_ARGON2ID_OPSLIMIT_INTERACTIVE: u32 = 2;
374        param_builder
375            .m_cost(CRYPTO_PWHASH_ARGON2ID_MEMLIMIT_INTERACTIVE / 1024)
376            .t_cost(CRYPTO_PWHASH_ARGON2ID_OPSLIMIT_INTERACTIVE);
377        // https://docs.rs/sodiumoxide/latest/sodiumoxide/crypto/secretbox/xsalsa20poly1305/constant.KEYBYTES.html
378        // KEYBYTES = 0x20
379        // param_builder.output_len(32)?;
380        let hasher = Argon2::new(
381            argon2::Algorithm::Argon2id,
382            argon2::Version::V0x13,
383            param_builder.build().map_err(map_err_to_anyhow)?,
384        );
385        let salt_string = SaltString::encode_b64(&salt).map_err(map_err_to_anyhow)?;
386        let pw_hash = hasher
387            .hash_password(passphrase.as_bytes(), &salt_string)
388            .map_err(map_err_to_anyhow)?;
389        if let Some(hash) = pw_hash.hash {
390            Ok((salt, hash.as_bytes().to_vec()))
391        } else {
392            anyhow::bail!(EncryptedKeyStoreError::EncryptionError)
393        }
394    }
395
396    fn encrypt(encryption_key: &[u8], msg: &[u8]) -> anyhow::Result<Vec<u8>> {
397        let mut nonce = [0; NONCE_SIZE];
398        crate::utils::rand::forest_os_rng().fill_bytes(&mut nonce);
399        let nonce = GenericArray::from_slice(&nonce);
400        let key = GenericArray::from_slice(encryption_key);
401        let cipher = XSalsa20Poly1305::new(key);
402        let mut ciphertext = cipher.encrypt(nonce, msg).map_err(map_err_to_anyhow)?;
403        ciphertext.extend(nonce.iter());
404        Ok(ciphertext)
405    }
406
407    #[allow(clippy::indexing_slicing)]
408    fn decrypt(encryption_key: &[u8], msg: &[u8]) -> anyhow::Result<Vec<u8>> {
409        anyhow::ensure!(msg.len() > NONCE_SIZE);
410        let cyphertext_len = msg.len() - NONCE_SIZE;
411        let ciphertext = &msg[..cyphertext_len];
412        let nonce = GenericArray::from_slice(&msg[cyphertext_len..]);
413        let key = GenericArray::from_slice(encryption_key);
414        let cipher = XSalsa20Poly1305::new(key);
415        let plaintext = cipher
416            .decrypt(nonce, ciphertext)
417            .map_err(map_err_to_anyhow)?;
418        Ok(plaintext)
419    }
420}
421
422fn map_err_to_anyhow<T: Display>(e: T) -> anyhow::Error {
423    anyhow::Error::msg(e.to_string())
424}
425
426#[cfg(test)]
427mod test {
428    use base64::{Engine, prelude::BASE64_STANDARD};
429
430    use super::*;
431    use crate::key_management::wallet;
432
433    const PASSPHRASE: &str = "foobarbaz";
434
435    #[test]
436    fn test_generate_key() {
437        let (salt, encryption_key) = EncryptedKeyStore::derive_key(PASSPHRASE, None).unwrap();
438        let (second_salt, second_key) =
439            EncryptedKeyStore::derive_key(PASSPHRASE, Some(salt)).unwrap();
440
441        assert_eq!(
442            encryption_key, second_key,
443            "Derived key must be deterministic"
444        );
445        assert_eq!(salt, second_salt, "Salts must match");
446    }
447
448    #[test]
449    fn test_encrypt_message() {
450        let (_, private_key) = EncryptedKeyStore::derive_key(PASSPHRASE, None).unwrap();
451        let message = "foo is coming";
452        let ciphertext = EncryptedKeyStore::encrypt(&private_key, message.as_bytes()).unwrap();
453        let second_pass = EncryptedKeyStore::encrypt(&private_key, message.as_bytes()).unwrap();
454        assert_ne!(
455            ciphertext, second_pass,
456            "Ciphertexts use secure initialization vectors"
457        );
458    }
459
460    #[test]
461    fn test_decrypt_message() {
462        let (_, private_key) = EncryptedKeyStore::derive_key(PASSPHRASE, None).unwrap();
463        let message = "foo is coming";
464        let ciphertext = EncryptedKeyStore::encrypt(&private_key, message.as_bytes()).unwrap();
465        let plaintext = EncryptedKeyStore::decrypt(&private_key, &ciphertext).unwrap();
466        assert_eq!(plaintext, message.as_bytes());
467    }
468
469    #[test]
470    fn test_read_old_encrypted_keystore() {
471        let dir: PathBuf = "src/key_management/tests/keystore_encrypted_old".into();
472        assert!(dir.exists());
473        let ks = KeyStore::new(KeyStoreConfig::Encrypted(dir, PASSPHRASE.to_string())).unwrap();
474        assert!(ks.persistence.is_some());
475    }
476
477    #[test]
478    fn test_read_write_encrypted_keystore() {
479        let keystore_location = tempfile::tempdir().unwrap().keep();
480        let ks = KeyStore::new(KeyStoreConfig::Encrypted(
481            keystore_location.clone(),
482            PASSPHRASE.to_string(),
483        ))
484        .unwrap();
485        ks.flush().unwrap();
486
487        let ks_read = KeyStore::new(KeyStoreConfig::Encrypted(
488            keystore_location,
489            PASSPHRASE.to_string(),
490        ))
491        .unwrap();
492
493        assert_eq!(ks, ks_read);
494    }
495
496    #[test]
497    fn test_read_write_keystore() {
498        let keystore_location = tempfile::tempdir().unwrap().keep();
499        let mut ks = KeyStore::new(KeyStoreConfig::Persistent(keystore_location.clone())).unwrap();
500
501        let key = wallet::generate_key(SignatureType::Bls).unwrap();
502
503        let addr = format!("wallet-{}", key.address);
504        ks.put(&addr, key.key_info).unwrap();
505        ks.flush().unwrap();
506
507        let default = ks.get(&addr).unwrap();
508
509        // Manually parse keystore.json
510        let keystore_file = keystore_location.join(KEYSTORE_NAME);
511        let reader = BufReader::new(File::open(keystore_file).unwrap());
512        let persisted_keystore: HashMap<String, PersistentKeyInfo> =
513            serde_json::from_reader(reader).unwrap();
514
515        let default_key_info = persisted_keystore.get(&addr).unwrap();
516        let actual = BASE64_STANDARD
517            .decode(default_key_info.private_key.clone())
518            .unwrap();
519
520        assert_eq!(
521            default.private_key, actual,
522            "persisted key matches key from key store"
523        );
524
525        // Read existing keystore.json
526        let ks_read = KeyStore::new(KeyStoreConfig::Persistent(keystore_location)).unwrap();
527        assert_eq!(ks, ks_read);
528    }
529}