1use crate::error::AgentError;
7use crate::storage::keychain::{IdentityDID, KeyAlias, KeyStorage};
8use argon2::{Argon2, Version};
9use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
10use chacha20poly1305::{
11 XChaCha20Poly1305, XNonce,
12 aead::{Aead, KeyInit},
13};
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16use std::fs::{self, File, OpenOptions};
17use std::io::{Read, Write};
18use std::path::PathBuf;
19use std::sync::Mutex;
20use zeroize::Zeroizing;
21
22const XCHACHA_NONCE_LEN: usize = 24;
24const KEY_LEN: usize = 32;
26const SALT_LEN: usize = 16;
28
29const FILE_FORMAT_VERSION: u32 = 1;
31
32#[derive(Debug, Serialize, Deserialize)]
34struct EncryptedFileFormat {
35 version: u32,
36 salt: String, nonce: String, ciphertext: String, }
40
41#[derive(Debug, Serialize, Deserialize, Default)]
43struct KeyData {
44 keys: HashMap<String, (String, String)>,
46}
47
48pub struct EncryptedFileStorage {
53 path: PathBuf,
54 password: Mutex<Option<Zeroizing<String>>>,
56}
57
58impl EncryptedFileStorage {
59 pub fn new(home: &std::path::Path) -> Result<Self, AgentError> {
69 Self::with_path(home.join("keys.enc"))
70 }
71
72 pub fn with_path(path: PathBuf) -> Result<Self, AgentError> {
74 if let Some(parent) = path.parent() {
76 fs::create_dir_all(parent).map_err(|e| {
77 AgentError::StorageError(format!(
78 "Failed to create directory {}: {}",
79 parent.display(),
80 e
81 ))
82 })?;
83 }
84 Ok(Self {
85 path,
86 password: Mutex::new(None),
87 })
88 }
89
90 #[allow(clippy::unwrap_used)] pub fn set_password(&self, password: Zeroizing<String>) {
96 let mut guard = self.password.lock().unwrap();
97 *guard = Some(password);
98 }
99
100 #[allow(clippy::unwrap_used)] fn get_password(&self) -> Result<Zeroizing<String>, AgentError> {
103 self.password
104 .lock()
105 .unwrap()
106 .clone()
107 .ok_or(AgentError::MissingPassphrase)
108 }
109
110 fn derive_key(password: &str, salt: &[u8]) -> Result<Zeroizing<[u8; KEY_LEN]>, AgentError> {
112 let params = crate::crypto::encryption::get_kdf_params()?;
113
114 let argon2 = Argon2::new(argon2::Algorithm::Argon2id, Version::V0x13, params);
115
116 let mut key = Zeroizing::new([0u8; KEY_LEN]);
117 argon2
118 .hash_password_into(password.as_bytes(), salt, key.as_mut())
119 .map_err(|e| AgentError::CryptoError(format!("Argon2 key derivation failed: {}", e)))?;
120
121 Ok(key)
122 }
123
124 fn encrypt(
126 key: &[u8; KEY_LEN],
127 data: &[u8],
128 ) -> Result<(Vec<u8>, [u8; XCHACHA_NONCE_LEN]), AgentError> {
129 let nonce: [u8; XCHACHA_NONCE_LEN] = rand::random();
130 let cipher = XChaCha20Poly1305::new_from_slice(key)
131 .map_err(|e| AgentError::CryptoError(format!("Invalid key: {}", e)))?;
132
133 let ciphertext = cipher
134 .encrypt(XNonce::from_slice(&nonce), data)
135 .map_err(|e| AgentError::CryptoError(format!("Encryption failed: {}", e)))?;
136
137 Ok((ciphertext, nonce))
138 }
139
140 fn decrypt(
142 key: &[u8; KEY_LEN],
143 nonce: &[u8],
144 ciphertext: &[u8],
145 ) -> Result<Vec<u8>, AgentError> {
146 let cipher = XChaCha20Poly1305::new_from_slice(key)
147 .map_err(|e| AgentError::CryptoError(format!("Invalid key: {}", e)))?;
148
149 cipher
150 .decrypt(XNonce::from_slice(nonce), ciphertext)
151 .map_err(|_| AgentError::IncorrectPassphrase)
152 }
153
154 fn read_data(&self) -> Result<KeyData, AgentError> {
156 if !self.path.exists() {
157 return Ok(KeyData::default());
158 }
159
160 let password = self.get_password()?;
161
162 let mut file = File::open(&self.path).map_err(|e| {
163 AgentError::StorageError(format!("Failed to open {}: {}", self.path.display(), e))
164 })?;
165
166 let mut contents = String::new();
167 file.read_to_string(&mut contents).map_err(|e| {
168 AgentError::StorageError(format!("Failed to read {}: {}", self.path.display(), e))
169 })?;
170
171 let encrypted: EncryptedFileFormat = serde_json::from_str(&contents)
172 .map_err(|e| AgentError::StorageError(format!("Invalid file format: {}", e)))?;
173
174 if encrypted.version != FILE_FORMAT_VERSION {
175 return Err(AgentError::StorageError(format!(
176 "Unsupported file format version: {} (expected {})",
177 encrypted.version, FILE_FORMAT_VERSION
178 )));
179 }
180
181 let salt = BASE64
182 .decode(&encrypted.salt)
183 .map_err(|e| AgentError::StorageError(format!("Invalid salt encoding: {}", e)))?;
184 let nonce = BASE64
185 .decode(&encrypted.nonce)
186 .map_err(|e| AgentError::StorageError(format!("Invalid nonce encoding: {}", e)))?;
187 let ciphertext = BASE64
188 .decode(&encrypted.ciphertext)
189 .map_err(|e| AgentError::StorageError(format!("Invalid ciphertext encoding: {}", e)))?;
190
191 let key = Self::derive_key(&password, &salt)?;
192 let plaintext = Self::decrypt(&key, &nonce, &ciphertext)?;
193
194 let data: KeyData = serde_json::from_slice(&plaintext)
195 .map_err(|e| AgentError::StorageError(format!("Failed to parse key data: {}", e)))?;
196
197 Ok(data)
198 }
199
200 fn write_data(&self, data: &KeyData) -> Result<(), AgentError> {
202 let password = self.get_password()?;
203
204 let plaintext = serde_json::to_vec(data).map_err(|e| {
205 AgentError::StorageError(format!("Failed to serialize key data: {}", e))
206 })?;
207
208 let salt: [u8; SALT_LEN] = rand::random();
209 let key = Self::derive_key(&password, &salt)?;
210 let (ciphertext, nonce) = Self::encrypt(&key, &plaintext)?;
211
212 let encrypted = EncryptedFileFormat {
213 version: FILE_FORMAT_VERSION,
214 salt: BASE64.encode(salt),
215 nonce: BASE64.encode(nonce),
216 ciphertext: BASE64.encode(&ciphertext),
217 };
218
219 let contents = serde_json::to_string_pretty(&encrypted).map_err(|e| {
220 AgentError::StorageError(format!("Failed to serialize encrypted data: {}", e))
221 })?;
222
223 let temp_path = self.path.with_extension("tmp");
225
226 {
227 let mut file = OpenOptions::new()
228 .write(true)
229 .create(true)
230 .truncate(true)
231 .open(&temp_path)
232 .map_err(|e| {
233 AgentError::StorageError(format!(
234 "Failed to create temp file {}: {}",
235 temp_path.display(),
236 e
237 ))
238 })?;
239
240 #[cfg(unix)]
242 {
243 use std::os::unix::fs::PermissionsExt;
244 let perms = std::fs::Permissions::from_mode(0o600);
245 file.set_permissions(perms).map_err(|e| {
246 AgentError::StorageError(format!("Failed to set file permissions: {}", e))
247 })?;
248 }
249
250 file.write_all(contents.as_bytes()).map_err(|e| {
251 AgentError::StorageError(format!(
252 "Failed to write to {}: {}",
253 temp_path.display(),
254 e
255 ))
256 })?;
257
258 file.sync_all()
259 .map_err(|e| AgentError::StorageError(format!("Failed to sync file: {}", e)))?;
260 }
261
262 fs::rename(&temp_path, &self.path).map_err(|e| {
264 AgentError::StorageError(format!(
265 "Failed to rename {} to {}: {}",
266 temp_path.display(),
267 self.path.display(),
268 e
269 ))
270 })?;
271
272 Ok(())
273 }
274}
275
276impl KeyStorage for EncryptedFileStorage {
277 fn store_key(
278 &self,
279 alias: &KeyAlias,
280 identity_did: &IdentityDID,
281 encrypted_key_data: &[u8],
282 ) -> Result<(), AgentError> {
283 let mut data = self.read_data()?;
284 data.keys.insert(
285 alias.as_str().to_string(),
286 (
287 identity_did.as_str().to_string(),
288 BASE64.encode(encrypted_key_data),
289 ),
290 );
291 self.write_data(&data)
292 }
293
294 fn load_key(&self, alias: &KeyAlias) -> Result<(IdentityDID, Vec<u8>), AgentError> {
295 let data = self.read_data()?;
296 let (did, key_b64) = data
297 .keys
298 .get(alias.as_str())
299 .ok_or(AgentError::KeyNotFound)?;
300 let key_bytes = BASE64
301 .decode(key_b64)
302 .map_err(|e| AgentError::StorageError(format!("Invalid key encoding: {}", e)))?;
303 Ok((IdentityDID::new_unchecked(did.clone()), key_bytes))
304 }
305
306 fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError> {
307 let mut data = self.read_data()?;
308 data.keys.remove(alias.as_str());
309 self.write_data(&data)
310 }
311
312 fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError> {
313 let data = self.read_data()?;
314 Ok(data
315 .keys
316 .keys()
317 .map(|k| KeyAlias::new_unchecked(k.clone()))
318 .collect())
319 }
320
321 fn list_aliases_for_identity(
322 &self,
323 identity_did: &IdentityDID,
324 ) -> Result<Vec<KeyAlias>, AgentError> {
325 let data = self.read_data()?;
326 let aliases = data
327 .keys
328 .iter()
329 .filter_map(|(alias, (did, _))| {
330 if did == identity_did.as_str() {
331 Some(KeyAlias::new_unchecked(alias.clone()))
332 } else {
333 None
334 }
335 })
336 .collect();
337 Ok(aliases)
338 }
339
340 fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError> {
341 let data = self.read_data()?;
342 data.keys
343 .get(alias.as_str())
344 .map(|(did, _)| IdentityDID::new_unchecked(did.clone()))
345 .ok_or(AgentError::KeyNotFound)
346 }
347
348 fn backend_name(&self) -> &'static str {
349 "encrypted-file"
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356 use tempfile::TempDir;
357
358 fn create_test_storage() -> (EncryptedFileStorage, TempDir) {
359 let temp_dir = TempDir::new().unwrap();
360 let storage = EncryptedFileStorage::new(temp_dir.path()).unwrap();
361 storage.set_password(Zeroizing::new("test_password".to_string()));
362 (storage, temp_dir)
363 }
364
365 #[test]
366 fn test_encrypt_decrypt_roundtrip() {
367 let password = "test_password";
368 let salt: [u8; SALT_LEN] = rand::random();
369 let data = b"test data for encryption";
370
371 let key = EncryptedFileStorage::derive_key(password, &salt).unwrap();
372 let (ciphertext, nonce) = EncryptedFileStorage::encrypt(&key, data).unwrap();
373 let decrypted = EncryptedFileStorage::decrypt(&key, &nonce, &ciphertext).unwrap();
374
375 assert_eq!(data.as_slice(), decrypted.as_slice());
376 }
377
378 #[test]
379 fn test_wrong_password_fails() {
380 let salt: [u8; SALT_LEN] = rand::random();
381 let data = b"test data";
382
383 let key1 = EncryptedFileStorage::derive_key("password1", &salt).unwrap();
384 let (ciphertext, nonce) = EncryptedFileStorage::encrypt(&key1, data).unwrap();
385
386 let key2 = EncryptedFileStorage::derive_key("password2", &salt).unwrap();
387 let result = EncryptedFileStorage::decrypt(&key2, &nonce, &ciphertext);
388
389 assert!(matches!(result, Err(AgentError::IncorrectPassphrase)));
390 }
391
392 #[test]
393 fn test_store_and_load_key() {
394 let (storage, _temp) = create_test_storage();
395 let alias = KeyAlias::new("test-alias").unwrap();
396 let identity_did = IdentityDID::new("did:keri:test123");
397 let encrypted_data = b"encrypted_key_bytes";
398
399 storage
400 .store_key(&alias, &identity_did, encrypted_data)
401 .unwrap();
402
403 let (loaded_did, loaded_data) = storage.load_key(&alias).unwrap();
404 assert_eq!(loaded_did, identity_did);
405 assert_eq!(loaded_data, encrypted_data);
406 }
407
408 #[test]
409 fn test_list_aliases() {
410 let (storage, _temp) = create_test_storage();
411 let did = IdentityDID::new("did:keri:test");
412
413 storage
414 .store_key(&KeyAlias::new("alias1").unwrap(), &did, b"data1")
415 .unwrap();
416 storage
417 .store_key(&KeyAlias::new("alias2").unwrap(), &did, b"data2")
418 .unwrap();
419
420 let mut aliases = storage.list_aliases().unwrap();
421 aliases.sort();
422 assert_eq!(
423 aliases,
424 vec![
425 KeyAlias::new_unchecked("alias1"),
426 KeyAlias::new_unchecked("alias2")
427 ]
428 );
429 }
430
431 #[test]
432 fn test_list_aliases_for_identity() {
433 let (storage, _temp) = create_test_storage();
434 let did1 = IdentityDID::new("did:keri:one");
435 let did2 = IdentityDID::new("did:keri:two");
436
437 storage
438 .store_key(&KeyAlias::new("a1").unwrap(), &did1, b"data1")
439 .unwrap();
440 storage
441 .store_key(&KeyAlias::new("a2").unwrap(), &did1, b"data2")
442 .unwrap();
443 storage
444 .store_key(&KeyAlias::new("b1").unwrap(), &did2, b"data3")
445 .unwrap();
446
447 let mut aliases = storage.list_aliases_for_identity(&did1).unwrap();
448 aliases.sort();
449 assert_eq!(
450 aliases,
451 vec![KeyAlias::new_unchecked("a1"), KeyAlias::new_unchecked("a2")]
452 );
453 }
454
455 #[test]
456 fn test_delete_key() {
457 let (storage, _temp) = create_test_storage();
458 let did = IdentityDID::new("did:keri:test");
459 let alias = KeyAlias::new("alias").unwrap();
460
461 storage.store_key(&alias, &did, b"data").unwrap();
462 assert!(storage.load_key(&alias).is_ok());
463
464 storage.delete_key(&alias).unwrap();
465 assert!(matches!(
466 storage.load_key(&alias),
467 Err(AgentError::KeyNotFound)
468 ));
469 }
470
471 #[test]
472 fn test_get_identity_for_alias() {
473 let (storage, _temp) = create_test_storage();
474 let did = IdentityDID::new("did:keri:test123");
475 let alias = KeyAlias::new("alias").unwrap();
476
477 storage.store_key(&alias, &did, b"data").unwrap();
478
479 let loaded_did = storage.get_identity_for_alias(&alias).unwrap();
480 assert_eq!(loaded_did, did);
481 }
482
483 #[test]
484 fn test_backend_name() {
485 let (storage, _temp) = create_test_storage();
486 assert_eq!(storage.backend_name(), "encrypted-file");
487 }
488
489 #[test]
490 fn test_file_format_version() {
491 let (storage, _temp) = create_test_storage();
492 let did = IdentityDID::new("did:keri:test");
493
494 storage
495 .store_key(&KeyAlias::new("alias").unwrap(), &did, b"data")
496 .unwrap();
497
498 let contents = fs::read_to_string(&storage.path).unwrap();
500 let encrypted: EncryptedFileFormat = serde_json::from_str(&contents).unwrap();
501
502 assert_eq!(encrypted.version, FILE_FORMAT_VERSION);
503 assert!(!encrypted.salt.is_empty());
504 assert!(!encrypted.nonce.is_empty());
505 assert!(!encrypted.ciphertext.is_empty());
506 }
507
508 #[test]
509 fn test_missing_password_error() {
510 let temp_dir = TempDir::new().unwrap();
511 let storage = EncryptedFileStorage::new(temp_dir.path()).unwrap();
512 let did = IdentityDID::new_unchecked("did:test".to_string());
513 let result = storage.store_key(&KeyAlias::new("alias").unwrap(), &did, b"data");
514 assert!(matches!(result, Err(AgentError::MissingPassphrase)));
515 }
516
517 #[test]
518 fn test_key_not_found() {
519 let (storage, _temp) = create_test_storage();
520
521 let result = storage.load_key(&KeyAlias::new("nonexistent").unwrap());
522 assert!(matches!(result, Err(AgentError::KeyNotFound)));
523 }
524}