keystore_rs/
file.rs

1use crate::{create_signing_key, KeyStore};
2use aes_gcm::{
3    aead::{Aead, AeadCore, KeyInit, OsRng},
4    Aes256Gcm, Nonce,
5};
6use anyhow::{anyhow, bail, Context, Result};
7use dotenvy::dotenv;
8use ed25519_consensus::SigningKey;
9use hex;
10use serde::{Deserialize, Serialize};
11use serde_json;
12use std::collections::HashMap;
13use std::{env, fs, path::PathBuf};
14
15#[derive(Serialize, Deserialize, Default)]
16struct EncryptedKeyStoreFile {
17    keys: HashMap<String, EncryptedKey>,
18}
19
20#[derive(Serialize, Deserialize)]
21struct EncryptedKey {
22    nonce: Vec<u8>,
23    ciphertext: Vec<u8>,
24}
25
26pub struct FileStore {
27    file_path: PathBuf,
28    cipher: Aes256Gcm,
29}
30
31impl FileStore {
32    pub fn new<P: Into<PathBuf>>(file_path: P) -> Result<Self> {
33        let file_path = file_path.into();
34        let expanded_path = if file_path.to_string_lossy().starts_with("~/") {
35            if let Some(home) = dirs::home_dir() {
36                home.join(file_path.to_string_lossy()[2..].to_string())
37            } else {
38                return Err(anyhow!("Could not determine home directory"));
39            }
40        } else {
41            file_path
42        };
43
44        let cipher = load_symmetric_key()?;
45        Ok(FileStore {
46            file_path: expanded_path,
47            cipher,
48        })
49    }
50
51    fn read_store(&self) -> Result<EncryptedKeyStoreFile> {
52        if !self.file_path.exists() {
53            if let Some(parent) = self.file_path.parent() {
54                fs::create_dir_all(parent).context("Failed to create keystore directory")?;
55            }
56            return Ok(EncryptedKeyStoreFile::default());
57        }
58
59        let content = fs::read_to_string(&self.file_path).context(format!(
60            "Failed to read keystore file at {:?}",
61            self.file_path
62        ))?;
63
64        serde_json::from_str(&content).context(format!(
65            "Failed to parse keystore file at {:?}. Content: {}",
66            self.file_path, content
67        ))
68    }
69
70    fn write_store(&self, store: &EncryptedKeyStoreFile) -> Result<()> {
71        // we have to ensure the directory exists
72        if let Some(parent) = self.file_path.parent() {
73            fs::create_dir_all(parent).context("Failed to create keystore directory")?;
74        }
75
76        let content =
77            serde_json::to_string_pretty(store).context("Failed to serialize keystore")?;
78
79        fs::write(&self.file_path, content).context("Failed to write keystore file")
80    }
81
82    fn encrypt_key(&self, key: &SigningKey) -> Result<EncryptedKey> {
83        let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
84
85        let ciphertext = self
86            .cipher
87            .encrypt(&nonce, key.to_bytes().as_ref())
88            .map_err(|e| anyhow!("Failed to encrypt key: {}", e))?;
89
90        Ok(EncryptedKey {
91            nonce: nonce.to_vec(),
92            ciphertext,
93        })
94    }
95
96    fn decrypt_key(&self, encrypted: &EncryptedKey) -> Result<SigningKey> {
97        let nonce = Nonce::from_slice(&encrypted.nonce);
98
99        let plaintext = self
100            .cipher
101            .decrypt(nonce, encrypted.ciphertext.as_ref())
102            .map_err(|e| anyhow!("Failed to decrypt key: {}", e))?;
103
104        if plaintext.len() != 32 {
105            bail!("Decrypted data has incorrect length for ed25519 key");
106        }
107
108        let mut key_array = [0u8; 32];
109        key_array.copy_from_slice(&plaintext);
110        Ok(SigningKey::from(key_array))
111    }
112}
113
114impl KeyStore for FileStore {
115    fn add_signing_key(&self, id: &str, signing_key: &SigningKey) -> Result<()> {
116        let mut store = self.read_store()?;
117        let encrypted = self.encrypt_key(signing_key)?;
118        store.keys.insert(id.to_string(), encrypted);
119        self.write_store(&store)
120    }
121
122    fn get_signing_key(&self, id: &str) -> Result<SigningKey> {
123        let store = self.read_store()?;
124
125        let encrypted = store
126            .keys
127            .get(id)
128            .ok_or_else(|| anyhow!("No key found for id: {}", id))?;
129
130        self.decrypt_key(encrypted)
131    }
132
133    fn get_or_create_signing_key(&self, id: &str) -> Result<SigningKey> {
134        match self.get_signing_key(id) {
135            Ok(key) => Ok(key),
136            Err(_) => {
137                let new_key = create_signing_key();
138                self.add_signing_key(id, &new_key)?;
139                Ok(new_key)
140            }
141        }
142    }
143}
144
145fn load_symmetric_key() -> Result<Aes256Gcm> {
146    dotenv().ok();
147    let key = env::var("SYMMETRIC_KEY").context("Failed to load symmetric key")?;
148    let cipher = Aes256Gcm::new_from_slice(&hex::decode(key)?)
149        .map_err(|e| anyhow!("Failed to create symmetric key: {}", e))?;
150    Ok(cipher)
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use tempfile::tempdir;
157
158    fn setup_test_env() {
159        std::env::set_var(
160            "SYMMETRIC_KEY",
161            "44a28a80b65029c1f4d4dd9e867cd91bb4c5ea07f232f395d64fb327f1c45c1c",
162        );
163    }
164
165    #[test]
166    fn test_store_and_retrieve_key() {
167        setup_test_env();
168        let temp_dir = tempdir().unwrap();
169        let file_path = temp_dir.path().join("test_keystore.json");
170        let store = FileStore::new(file_path).unwrap();
171
172        let id = "test_key";
173        let original_key = create_signing_key();
174        store.add_signing_key(id, &original_key).unwrap();
175
176        let retrieved_key = store.get_signing_key(id).unwrap();
177        assert_eq!(original_key.to_bytes(), retrieved_key.to_bytes());
178    }
179
180    #[test]
181    fn test_get_nonexistent_key() {
182        setup_test_env();
183        let temp_dir = tempdir().unwrap();
184        let file_path = temp_dir.path().join("test_keystore.json");
185        let store = FileStore::new(file_path).unwrap();
186
187        let result = store.get_signing_key("nonexistent");
188        assert!(result.is_err());
189    }
190
191    #[test]
192    fn test_get_or_create_key() {
193        setup_test_env();
194        let temp_dir = tempdir().unwrap();
195        let file_path = temp_dir.path().join("test_keystore.json");
196        let store = FileStore::new(file_path).unwrap();
197
198        let id = "test_key";
199
200        let key1 = store.get_or_create_signing_key(id).unwrap();
201        let key2 = store.get_or_create_signing_key(id).unwrap();
202
203        assert_eq!(key1.to_bytes(), key2.to_bytes());
204    }
205}