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 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}