1use crate::error::AgentError;
7use crate::storage::keychain::{IdentityDID, KeyAlias, KeyRole, 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;
16#[allow(clippy::disallowed_types)]
17use std::fs::{self, File, OpenOptions};
19use std::io::{Read, Write};
20use std::path::PathBuf;
21use std::sync::Mutex;
22use zeroize::Zeroizing;
23
24const XCHACHA_NONCE_LEN: usize = 24;
26const KEY_LEN: usize = 32;
28const SALT_LEN: usize = 16;
30
31const FILE_FORMAT_VERSION: u32 = 1;
33
34#[derive(Debug, Serialize, Deserialize)]
36struct EncryptedFileFormat {
37 version: u32,
38 salt: String, nonce: String, ciphertext: String, }
42
43#[derive(Debug, Serialize, Deserialize)]
45#[serde(untagged)]
46enum KeyEntry {
47 WithRole(String, String, String),
49 Legacy(String, String),
51}
52
53#[derive(Debug, Serialize, Deserialize, Default)]
55struct KeyData {
56 keys: HashMap<String, KeyEntry>,
58}
59
60pub struct EncryptedFileStorage {
65 path: PathBuf,
66 password: Mutex<Option<Zeroizing<String>>>,
68}
69
70#[allow(clippy::disallowed_methods)] #[allow(clippy::disallowed_types)]
72impl EncryptedFileStorage {
73 pub fn new(home: &std::path::Path) -> Result<Self, AgentError> {
83 Self::with_path(home.join("keys.enc"))
84 }
85
86 pub fn with_path(path: PathBuf) -> Result<Self, AgentError> {
88 if let Some(parent) = path.parent() {
90 fs::create_dir_all(parent).map_err(|e| {
91 AgentError::StorageError(format!(
92 "Failed to create directory {}: {}",
93 parent.display(),
94 e
95 ))
96 })?;
97 }
98 Ok(Self {
99 path,
100 password: Mutex::new(None),
101 })
102 }
103
104 #[allow(clippy::unwrap_used)] pub fn set_password(&self, password: Zeroizing<String>) {
110 let mut guard = self.password.lock().unwrap();
111 *guard = Some(password);
112 }
113
114 #[allow(clippy::unwrap_used)] fn get_password(&self) -> Result<Zeroizing<String>, AgentError> {
117 self.password
118 .lock()
119 .unwrap()
120 .clone()
121 .ok_or(AgentError::MissingPassphrase)
122 }
123
124 fn derive_key(password: &str, salt: &[u8]) -> Result<Zeroizing<[u8; KEY_LEN]>, AgentError> {
126 let params = crate::crypto::encryption::get_kdf_params()?;
127
128 let argon2 = Argon2::new(argon2::Algorithm::Argon2id, Version::V0x13, params);
129
130 let mut key = Zeroizing::new([0u8; KEY_LEN]);
131 argon2
132 .hash_password_into(password.as_bytes(), salt, key.as_mut())
133 .map_err(|e| AgentError::CryptoError(format!("Argon2 key derivation failed: {}", e)))?;
134
135 Ok(key)
136 }
137
138 fn encrypt(
140 key: &[u8; KEY_LEN],
141 data: &[u8],
142 ) -> Result<(Vec<u8>, [u8; XCHACHA_NONCE_LEN]), AgentError> {
143 let nonce: [u8; XCHACHA_NONCE_LEN] = rand::random();
144 let cipher = XChaCha20Poly1305::new_from_slice(key)
145 .map_err(|e| AgentError::CryptoError(format!("Invalid key: {}", e)))?;
146
147 let ciphertext = cipher
148 .encrypt(XNonce::from_slice(&nonce), data)
149 .map_err(|e| AgentError::CryptoError(format!("Encryption failed: {}", e)))?;
150
151 Ok((ciphertext, nonce))
152 }
153
154 fn decrypt(
156 key: &[u8; KEY_LEN],
157 nonce: &[u8],
158 ciphertext: &[u8],
159 ) -> Result<Vec<u8>, AgentError> {
160 let cipher = XChaCha20Poly1305::new_from_slice(key)
161 .map_err(|e| AgentError::CryptoError(format!("Invalid key: {}", e)))?;
162
163 cipher
164 .decrypt(XNonce::from_slice(nonce), ciphertext)
165 .map_err(|_| AgentError::IncorrectPassphrase)
166 }
167
168 fn read_data(&self) -> Result<KeyData, AgentError> {
170 if !self.path.exists() {
171 return Ok(KeyData::default());
172 }
173
174 let password = self.get_password()?;
175
176 let mut file = File::open(&self.path).map_err(|e| {
177 AgentError::StorageError(format!("Failed to open {}: {}", self.path.display(), e))
178 })?;
179
180 let mut contents = String::new();
181 file.read_to_string(&mut contents).map_err(|e| {
182 AgentError::StorageError(format!("Failed to read {}: {}", self.path.display(), e))
183 })?;
184
185 let encrypted: EncryptedFileFormat = serde_json::from_str(&contents)
186 .map_err(|e| AgentError::StorageError(format!("Invalid file format: {}", e)))?;
187
188 if encrypted.version != FILE_FORMAT_VERSION {
189 return Err(AgentError::StorageError(format!(
190 "Unsupported file format version: {} (expected {})",
191 encrypted.version, FILE_FORMAT_VERSION
192 )));
193 }
194
195 let salt = BASE64
196 .decode(&encrypted.salt)
197 .map_err(|e| AgentError::StorageError(format!("Invalid salt encoding: {}", e)))?;
198 let nonce = BASE64
199 .decode(&encrypted.nonce)
200 .map_err(|e| AgentError::StorageError(format!("Invalid nonce encoding: {}", e)))?;
201 let ciphertext = BASE64
202 .decode(&encrypted.ciphertext)
203 .map_err(|e| AgentError::StorageError(format!("Invalid ciphertext encoding: {}", e)))?;
204
205 let key = Self::derive_key(&password, &salt)?;
206 let plaintext = Self::decrypt(&key, &nonce, &ciphertext)?;
207
208 let data: KeyData = serde_json::from_slice(&plaintext)
209 .map_err(|e| AgentError::StorageError(format!("Failed to parse key data: {}", e)))?;
210
211 Ok(data)
212 }
213
214 fn write_data(&self, data: &KeyData) -> Result<(), AgentError> {
216 let password = self.get_password()?;
217
218 let plaintext = serde_json::to_vec(data).map_err(|e| {
219 AgentError::StorageError(format!("Failed to serialize key data: {}", e))
220 })?;
221
222 let salt: [u8; SALT_LEN] = rand::random();
223 let key = Self::derive_key(&password, &salt)?;
224 let (ciphertext, nonce) = Self::encrypt(&key, &plaintext)?;
225
226 let encrypted = EncryptedFileFormat {
227 version: FILE_FORMAT_VERSION,
228 salt: BASE64.encode(salt),
229 nonce: BASE64.encode(nonce),
230 ciphertext: BASE64.encode(&ciphertext),
231 };
232
233 let contents = serde_json::to_string_pretty(&encrypted).map_err(|e| {
234 AgentError::StorageError(format!("Failed to serialize encrypted data: {}", e))
235 })?;
236
237 let temp_path = self.path.with_extension("tmp");
239
240 {
241 let mut file = OpenOptions::new()
242 .write(true)
243 .create(true)
244 .truncate(true)
245 .open(&temp_path)
246 .map_err(|e| {
247 AgentError::StorageError(format!(
248 "Failed to create temp file {}: {}",
249 temp_path.display(),
250 e
251 ))
252 })?;
253
254 #[cfg(unix)]
256 {
257 use std::os::unix::fs::PermissionsExt;
258 let perms = std::fs::Permissions::from_mode(0o600);
259 file.set_permissions(perms).map_err(|e| {
260 AgentError::StorageError(format!("Failed to set file permissions: {}", e))
261 })?;
262 }
263
264 file.write_all(contents.as_bytes()).map_err(|e| {
265 AgentError::StorageError(format!(
266 "Failed to write to {}: {}",
267 temp_path.display(),
268 e
269 ))
270 })?;
271
272 file.sync_all()
273 .map_err(|e| AgentError::StorageError(format!("Failed to sync file: {}", e)))?;
274 }
275
276 fs::rename(&temp_path, &self.path).map_err(|e| {
278 AgentError::StorageError(format!(
279 "Failed to rename {} to {}: {}",
280 temp_path.display(),
281 self.path.display(),
282 e
283 ))
284 })?;
285
286 Ok(())
287 }
288}
289
290#[allow(clippy::disallowed_methods)] #[allow(clippy::disallowed_types)]
292impl KeyStorage for EncryptedFileStorage {
293 fn store_key(
294 &self,
295 alias: &KeyAlias,
296 identity_did: &IdentityDID,
297 role: KeyRole,
298 encrypted_key_data: &[u8],
299 ) -> Result<(), AgentError> {
300 let mut data = self.read_data()?;
301 data.keys.insert(
302 alias.as_str().to_string(),
303 KeyEntry::WithRole(
304 identity_did.as_str().to_string(),
305 role.to_string(),
306 BASE64.encode(encrypted_key_data),
307 ),
308 );
309 self.write_data(&data)
310 }
311
312 fn load_key(&self, alias: &KeyAlias) -> Result<(IdentityDID, KeyRole, Vec<u8>), AgentError> {
313 let data = self.read_data()?;
314 let entry = data
315 .keys
316 .get(alias.as_str())
317 .ok_or(AgentError::KeyNotFound)?;
318 match entry {
319 KeyEntry::WithRole(did, role_str, b64) => {
320 let role = role_str.parse::<KeyRole>().unwrap_or(KeyRole::Primary);
321 let key_bytes = BASE64.decode(b64).map_err(|e| {
322 AgentError::StorageError(format!("Invalid key encoding: {}", e))
323 })?;
324 Ok((IdentityDID::new_unchecked(did.clone()), role, key_bytes))
325 }
326 KeyEntry::Legacy(did, b64) => {
327 let key_bytes = BASE64.decode(b64).map_err(|e| {
328 AgentError::StorageError(format!("Invalid key encoding: {}", e))
329 })?;
330 Ok((
331 IdentityDID::new_unchecked(did.clone()),
332 KeyRole::Primary,
333 key_bytes,
334 ))
335 }
336 }
337 }
338
339 fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError> {
340 let mut data = self.read_data()?;
341 data.keys.remove(alias.as_str());
342 self.write_data(&data)
343 }
344
345 fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError> {
346 let data = self.read_data()?;
347 Ok(data
348 .keys
349 .keys()
350 .map(|k| KeyAlias::new_unchecked(k.clone()))
351 .collect())
352 }
353
354 fn list_aliases_for_identity(
355 &self,
356 identity_did: &IdentityDID,
357 ) -> Result<Vec<KeyAlias>, AgentError> {
358 let data = self.read_data()?;
359 let aliases = data
360 .keys
361 .iter()
362 .filter_map(|(alias, entry)| {
363 let did_str = match entry {
364 KeyEntry::WithRole(did, _, _) | KeyEntry::Legacy(did, _) => did,
365 };
366 if did_str == identity_did.as_str() {
367 Some(KeyAlias::new_unchecked(alias.clone()))
368 } else {
369 None
370 }
371 })
372 .collect();
373 Ok(aliases)
374 }
375
376 fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError> {
377 let data = self.read_data()?;
378 data.keys
379 .get(alias.as_str())
380 .map(|entry| {
381 let did_str = match entry {
382 KeyEntry::WithRole(did, _, _) | KeyEntry::Legacy(did, _) => did,
383 };
384 IdentityDID::new_unchecked(did_str.clone())
385 })
386 .ok_or(AgentError::KeyNotFound)
387 }
388
389 fn backend_name(&self) -> &'static str {
390 "encrypted-file"
391 }
392}
393
394#[cfg(test)]
395#[allow(clippy::disallowed_methods)]
396#[allow(clippy::disallowed_types)]
397mod tests {
398 use super::*;
399 use tempfile::TempDir;
400
401 fn create_test_storage() -> (EncryptedFileStorage, TempDir) {
402 let temp_dir = TempDir::new().unwrap();
403 let storage = EncryptedFileStorage::new(temp_dir.path()).unwrap();
404 storage.set_password(Zeroizing::new("test_password".to_string()));
405 (storage, temp_dir)
406 }
407
408 #[test]
409 fn test_encrypt_decrypt_roundtrip() {
410 let password = "test_password";
411 let salt: [u8; SALT_LEN] = rand::random();
412 let data = b"test data for encryption";
413
414 let key = EncryptedFileStorage::derive_key(password, &salt).unwrap();
415 let (ciphertext, nonce) = EncryptedFileStorage::encrypt(&key, data).unwrap();
416 let decrypted = EncryptedFileStorage::decrypt(&key, &nonce, &ciphertext).unwrap();
417
418 assert_eq!(data.as_slice(), decrypted.as_slice());
419 }
420
421 #[test]
422 fn test_wrong_password_fails() {
423 let salt: [u8; SALT_LEN] = rand::random();
424 let data = b"test data";
425
426 let key1 = EncryptedFileStorage::derive_key("password1", &salt).unwrap();
427 let (ciphertext, nonce) = EncryptedFileStorage::encrypt(&key1, data).unwrap();
428
429 let key2 = EncryptedFileStorage::derive_key("password2", &salt).unwrap();
430 let result = EncryptedFileStorage::decrypt(&key2, &nonce, &ciphertext);
431
432 assert!(matches!(result, Err(AgentError::IncorrectPassphrase)));
433 }
434
435 #[test]
436 fn test_store_and_load_key() {
437 let (storage, _temp) = create_test_storage();
438 let alias = KeyAlias::new("test-alias").unwrap();
439 let identity_did = IdentityDID::new_unchecked("did:keri:test123");
440 let encrypted_data = b"encrypted_key_bytes";
441
442 storage
443 .store_key(&alias, &identity_did, KeyRole::Primary, encrypted_data)
444 .unwrap();
445
446 let (loaded_did, loaded_role, loaded_data) = storage.load_key(&alias).unwrap();
447 assert_eq!(loaded_did, identity_did);
448 assert_eq!(loaded_role, KeyRole::Primary);
449 assert_eq!(loaded_data, encrypted_data);
450 }
451
452 #[test]
453 fn test_list_aliases() {
454 let (storage, _temp) = create_test_storage();
455 let did = IdentityDID::new_unchecked("did:keri:test");
456
457 storage
458 .store_key(
459 &KeyAlias::new("alias1").unwrap(),
460 &did,
461 KeyRole::Primary,
462 b"data1",
463 )
464 .unwrap();
465 storage
466 .store_key(
467 &KeyAlias::new("alias2").unwrap(),
468 &did,
469 KeyRole::Primary,
470 b"data2",
471 )
472 .unwrap();
473
474 let mut aliases = storage.list_aliases().unwrap();
475 aliases.sort();
476 assert_eq!(
477 aliases,
478 vec![
479 KeyAlias::new_unchecked("alias1"),
480 KeyAlias::new_unchecked("alias2")
481 ]
482 );
483 }
484
485 #[test]
486 fn test_list_aliases_for_identity() {
487 let (storage, _temp) = create_test_storage();
488 let did1 = IdentityDID::new_unchecked("did:keri:one");
489 let did2 = IdentityDID::new_unchecked("did:keri:two");
490
491 storage
492 .store_key(
493 &KeyAlias::new("a1").unwrap(),
494 &did1,
495 KeyRole::Primary,
496 b"data1",
497 )
498 .unwrap();
499 storage
500 .store_key(
501 &KeyAlias::new("a2").unwrap(),
502 &did1,
503 KeyRole::Primary,
504 b"data2",
505 )
506 .unwrap();
507 storage
508 .store_key(
509 &KeyAlias::new("b1").unwrap(),
510 &did2,
511 KeyRole::Primary,
512 b"data3",
513 )
514 .unwrap();
515
516 let mut aliases = storage.list_aliases_for_identity(&did1).unwrap();
517 aliases.sort();
518 assert_eq!(
519 aliases,
520 vec![KeyAlias::new_unchecked("a1"), KeyAlias::new_unchecked("a2")]
521 );
522 }
523
524 #[test]
525 fn test_delete_key() {
526 let (storage, _temp) = create_test_storage();
527 let did = IdentityDID::new_unchecked("did:keri:test");
528 let alias = KeyAlias::new("alias").unwrap();
529
530 storage
531 .store_key(&alias, &did, KeyRole::Primary, b"data")
532 .unwrap();
533 assert!(storage.load_key(&alias).is_ok());
534
535 storage.delete_key(&alias).unwrap();
536 assert!(matches!(
537 storage.load_key(&alias),
538 Err(AgentError::KeyNotFound)
539 ));
540 }
541
542 #[test]
543 fn test_get_identity_for_alias() {
544 let (storage, _temp) = create_test_storage();
545 let did = IdentityDID::new_unchecked("did:keri:test123");
546 let alias = KeyAlias::new("alias").unwrap();
547
548 storage
549 .store_key(&alias, &did, KeyRole::Primary, b"data")
550 .unwrap();
551
552 let loaded_did = storage.get_identity_for_alias(&alias).unwrap();
553 assert_eq!(loaded_did, did);
554 }
555
556 #[test]
557 fn test_backend_name() {
558 let (storage, _temp) = create_test_storage();
559 assert_eq!(storage.backend_name(), "encrypted-file");
560 }
561
562 #[test]
563 fn test_file_format_version() {
564 let (storage, _temp) = create_test_storage();
565 let did = IdentityDID::new_unchecked("did:keri:test");
566
567 storage
568 .store_key(
569 &KeyAlias::new("alias").unwrap(),
570 &did,
571 KeyRole::Primary,
572 b"data",
573 )
574 .unwrap();
575
576 let contents = fs::read_to_string(&storage.path).unwrap();
578 let encrypted: EncryptedFileFormat = serde_json::from_str(&contents).unwrap();
579
580 assert_eq!(encrypted.version, FILE_FORMAT_VERSION);
581 assert!(!encrypted.salt.is_empty());
582 assert!(!encrypted.nonce.is_empty());
583 assert!(!encrypted.ciphertext.is_empty());
584 }
585
586 #[test]
587 fn test_missing_password_error() {
588 let temp_dir = TempDir::new().unwrap();
589 let storage = EncryptedFileStorage::new(temp_dir.path()).unwrap();
590 let did = IdentityDID::new_unchecked("did:test".to_string());
591 let result = storage.store_key(
592 &KeyAlias::new("alias").unwrap(),
593 &did,
594 KeyRole::Primary,
595 b"data",
596 );
597 assert!(matches!(result, Err(AgentError::MissingPassphrase)));
598 }
599
600 #[test]
601 fn test_key_not_found() {
602 let (storage, _temp) = create_test_storage();
603
604 let result = storage.load_key(&KeyAlias::new("nonexistent").unwrap());
605 assert!(matches!(result, Err(AgentError::KeyNotFound)));
606 }
607
608 #[test]
609 fn test_legacy_key_data_migration() {
610 let old_json = r#"{"keys":{"my-key":["did:keri:Eabc","dGVzdA=="]}}"#;
612 let data: KeyData = serde_json::from_str(old_json).unwrap();
613 let entry = data.keys.get("my-key").unwrap();
614 match entry {
615 KeyEntry::Legacy(did, _b64) => assert_eq!(did, "did:keri:Eabc"),
616 KeyEntry::WithRole(..) => panic!("should deserialize as Legacy"),
617 }
618 }
619
620 #[test]
621 fn test_new_key_data_format() {
622 let new_json = r#"{"keys":{"my-key":["did:keri:Eabc","primary","dGVzdA=="]}}"#;
623 let data: KeyData = serde_json::from_str(new_json).unwrap();
624 let entry = data.keys.get("my-key").unwrap();
625 match entry {
626 KeyEntry::WithRole(did, role, _b64) => {
627 assert_eq!(did, "did:keri:Eabc");
628 assert_eq!(role, "primary");
629 }
630 KeyEntry::Legacy(..) => panic!("should deserialize as WithRole"),
631 }
632 }
633}