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 rand::RngCore;
15use serde::{Deserialize, Serialize};
16use std::collections::BTreeMap;
17#[allow(clippy::disallowed_types)]
18use std::fs::{self, File, OpenOptions};
20use std::io::{Read, Write};
21use std::path::PathBuf;
22use std::sync::Mutex;
23use zeroize::Zeroizing;
24
25const XCHACHA_NONCE_LEN: usize = 24;
27const KEY_LEN: usize = 32;
29const SALT_LEN: usize = 16;
31
32const FILE_FORMAT_VERSION: u32 = 1;
34
35#[derive(Debug, Serialize, Deserialize)]
37struct EncryptedFileFormat {
38 version: u32,
39 salt: String, nonce: String, ciphertext: String, }
43
44#[derive(Debug, Serialize, Deserialize)]
46#[serde(untagged)]
47enum KeyEntry {
48 WithRole(String, String, String),
50 Legacy(String, String),
52}
53
54#[derive(Debug, Serialize, Deserialize, Default)]
56struct KeyData {
57 keys: BTreeMap<String, KeyEntry>,
61}
62
63pub struct EncryptedFileStorage {
68 path: PathBuf,
69 password: Mutex<Option<Zeroizing<String>>>,
71}
72
73#[allow(clippy::disallowed_methods)] #[allow(clippy::disallowed_types)]
75impl EncryptedFileStorage {
76 pub fn new(home: &std::path::Path) -> Result<Self, AgentError> {
86 Self::with_path(home.join("keys.enc"))
87 }
88
89 pub fn with_path(path: PathBuf) -> Result<Self, AgentError> {
91 if let Some(parent) = path.parent() {
93 fs::create_dir_all(parent).map_err(|e| {
94 AgentError::StorageError(format!(
95 "Failed to create directory {}: {}",
96 parent.display(),
97 e
98 ))
99 })?;
100 }
101 Ok(Self {
102 path,
103 password: Mutex::new(None),
104 })
105 }
106
107 #[allow(clippy::unwrap_used)] pub fn set_password(&self, password: Zeroizing<String>) {
113 let mut guard = self.password.lock().unwrap();
114 *guard = Some(password);
115 }
116
117 #[allow(clippy::unwrap_used)] fn get_password(&self) -> Result<Zeroizing<String>, AgentError> {
120 self.password
121 .lock()
122 .unwrap()
123 .clone()
124 .ok_or(AgentError::MissingPassphrase)
125 }
126
127 fn derive_key(password: &str, salt: &[u8]) -> Result<Zeroizing<[u8; KEY_LEN]>, AgentError> {
129 let params = crate::crypto::encryption::get_kdf_params()?;
130
131 let argon2 = Argon2::new(argon2::Algorithm::Argon2id, Version::V0x13, params);
132
133 let mut key = Zeroizing::new([0u8; KEY_LEN]);
134 argon2
135 .hash_password_into(password.as_bytes(), salt, key.as_mut())
136 .map_err(|e| AgentError::CryptoError(format!("Argon2 key derivation failed: {}", e)))?;
137
138 Ok(key)
139 }
140
141 fn encrypt(
143 key: &[u8; KEY_LEN],
144 data: &[u8],
145 ) -> Result<(Vec<u8>, [u8; XCHACHA_NONCE_LEN]), AgentError> {
146 let mut nonce = [0u8; XCHACHA_NONCE_LEN];
147 rand::rngs::OsRng.fill_bytes(&mut nonce);
148 let cipher = XChaCha20Poly1305::new_from_slice(key)
149 .map_err(|e| AgentError::CryptoError(format!("Invalid key: {}", e)))?;
150
151 let ciphertext = cipher
152 .encrypt(XNonce::from_slice(&nonce), data)
153 .map_err(|e| AgentError::CryptoError(format!("Encryption failed: {}", e)))?;
154
155 Ok((ciphertext, nonce))
156 }
157
158 fn decrypt(
160 key: &[u8; KEY_LEN],
161 nonce: &[u8],
162 ciphertext: &[u8],
163 ) -> Result<Vec<u8>, AgentError> {
164 let cipher = XChaCha20Poly1305::new_from_slice(key)
165 .map_err(|e| AgentError::CryptoError(format!("Invalid key: {}", e)))?;
166
167 cipher
168 .decrypt(XNonce::from_slice(nonce), ciphertext)
169 .map_err(|_| AgentError::IncorrectPassphrase)
170 }
171
172 fn read_data(&self) -> Result<KeyData, AgentError> {
174 if !self.path.exists() {
175 return Ok(KeyData::default());
176 }
177
178 let password = self.get_password()?;
179
180 let mut file = File::open(&self.path).map_err(|e| {
181 AgentError::StorageError(format!("Failed to open {}: {}", self.path.display(), e))
182 })?;
183
184 let mut contents = String::new();
185 file.read_to_string(&mut contents).map_err(|e| {
186 AgentError::StorageError(format!("Failed to read {}: {}", self.path.display(), e))
187 })?;
188
189 let encrypted: EncryptedFileFormat = serde_json::from_str(&contents)
190 .map_err(|e| AgentError::StorageError(format!("Invalid file format: {}", e)))?;
191
192 if encrypted.version != FILE_FORMAT_VERSION {
193 return Err(AgentError::StorageError(format!(
194 "Unsupported file format version: {} (expected {})",
195 encrypted.version, FILE_FORMAT_VERSION
196 )));
197 }
198
199 let salt = BASE64
200 .decode(&encrypted.salt)
201 .map_err(|e| AgentError::StorageError(format!("Invalid salt encoding: {}", e)))?;
202 let nonce = BASE64
203 .decode(&encrypted.nonce)
204 .map_err(|e| AgentError::StorageError(format!("Invalid nonce encoding: {}", e)))?;
205 let ciphertext = BASE64
206 .decode(&encrypted.ciphertext)
207 .map_err(|e| AgentError::StorageError(format!("Invalid ciphertext encoding: {}", e)))?;
208
209 let key = Self::derive_key(&password, &salt)?;
210 let plaintext = Self::decrypt(&key, &nonce, &ciphertext)?;
211
212 let data: KeyData = serde_json::from_slice(&plaintext)
213 .map_err(|e| AgentError::StorageError(format!("Failed to parse key data: {}", e)))?;
214
215 Ok(data)
216 }
217
218 fn write_data(&self, data: &KeyData) -> Result<(), AgentError> {
220 let password = self.get_password()?;
221
222 let plaintext = serde_json::to_vec(data).map_err(|e| {
223 AgentError::StorageError(format!("Failed to serialize key data: {}", e))
224 })?;
225
226 let mut salt = [0u8; SALT_LEN];
227 rand::rngs::OsRng.fill_bytes(&mut salt);
228 let key = Self::derive_key(&password, &salt)?;
229 let (ciphertext, nonce) = Self::encrypt(&key, &plaintext)?;
230
231 let encrypted = EncryptedFileFormat {
232 version: FILE_FORMAT_VERSION,
233 salt: BASE64.encode(salt),
234 nonce: BASE64.encode(nonce),
235 ciphertext: BASE64.encode(&ciphertext),
236 };
237
238 let contents = serde_json::to_string_pretty(&encrypted).map_err(|e| {
239 AgentError::StorageError(format!("Failed to serialize encrypted data: {}", e))
240 })?;
241
242 let temp_path = self.path.with_extension("tmp");
244
245 {
246 let mut file = OpenOptions::new()
247 .write(true)
248 .create(true)
249 .truncate(true)
250 .open(&temp_path)
251 .map_err(|e| {
252 AgentError::StorageError(format!(
253 "Failed to create temp file {}: {}",
254 temp_path.display(),
255 e
256 ))
257 })?;
258
259 #[cfg(unix)]
261 {
262 use std::os::unix::fs::PermissionsExt;
263 let perms = std::fs::Permissions::from_mode(0o600);
264 file.set_permissions(perms).map_err(|e| {
265 AgentError::StorageError(format!("Failed to set file permissions: {}", e))
266 })?;
267 }
268
269 file.write_all(contents.as_bytes()).map_err(|e| {
270 AgentError::StorageError(format!(
271 "Failed to write to {}: {}",
272 temp_path.display(),
273 e
274 ))
275 })?;
276
277 file.sync_all()
278 .map_err(|e| AgentError::StorageError(format!("Failed to sync file: {}", e)))?;
279 }
280
281 fs::rename(&temp_path, &self.path).map_err(|e| {
283 AgentError::StorageError(format!(
284 "Failed to rename {} to {}: {}",
285 temp_path.display(),
286 self.path.display(),
287 e
288 ))
289 })?;
290
291 Ok(())
292 }
293}
294
295fn parse_stored_did(did: &str, alias: &KeyAlias) -> Result<IdentityDID, AgentError> {
300 IdentityDID::parse(did).map_err(|e| {
301 AgentError::SecurityError(format!(
302 "key '{}': malformed identity DID in keystore: {e}",
303 alias.as_str()
304 ))
305 })
306}
307
308#[allow(clippy::disallowed_methods)] #[allow(clippy::disallowed_types)]
310impl KeyStorage for EncryptedFileStorage {
311 fn store_key(
312 &self,
313 alias: &KeyAlias,
314 identity_did: &IdentityDID,
315 role: KeyRole,
316 encrypted_key_data: &[u8],
317 ) -> Result<(), AgentError> {
318 let mut data = self.read_data()?;
319 data.keys.insert(
320 alias.as_str().to_string(),
321 KeyEntry::WithRole(
322 identity_did.as_str().to_string(),
323 role.to_string(),
324 BASE64.encode(encrypted_key_data),
325 ),
326 );
327 self.write_data(&data)
328 }
329
330 fn load_key(&self, alias: &KeyAlias) -> Result<(IdentityDID, KeyRole, Vec<u8>), AgentError> {
331 let data = self.read_data()?;
332 let entry = data
333 .keys
334 .get(alias.as_str())
335 .ok_or(AgentError::KeyNotFound)?;
336 match entry {
337 KeyEntry::WithRole(did, role_str, b64) => {
338 let role = KeyRole::from_persisted(role_str).map_err(|e| {
339 AgentError::SecurityError(format!("key '{}': {e}", alias.as_str()))
340 })?;
341 let key_bytes = BASE64.decode(b64).map_err(|e| {
342 AgentError::StorageError(format!("Invalid key encoding: {}", e))
343 })?;
344 Ok((parse_stored_did(did, alias)?, role, key_bytes))
345 }
346 KeyEntry::Legacy(did, b64) => {
347 let key_bytes = BASE64.decode(b64).map_err(|e| {
348 AgentError::StorageError(format!("Invalid key encoding: {}", e))
349 })?;
350 Ok((parse_stored_did(did, alias)?, KeyRole::Primary, key_bytes))
351 }
352 }
353 }
354
355 fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError> {
356 let mut data = self.read_data()?;
357 data.keys.remove(alias.as_str());
358 self.write_data(&data)
359 }
360
361 fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError> {
362 let data = self.read_data()?;
363 Ok(data
364 .keys
365 .keys()
366 .map(|k| KeyAlias::new_unchecked(k.clone()))
367 .collect())
368 }
369
370 fn list_aliases_for_identity(
371 &self,
372 identity_did: &IdentityDID,
373 ) -> Result<Vec<KeyAlias>, AgentError> {
374 let data = self.read_data()?;
375 let aliases = data
376 .keys
377 .iter()
378 .filter_map(|(alias, entry)| {
379 let did_str = match entry {
380 KeyEntry::WithRole(did, _, _) | KeyEntry::Legacy(did, _) => did,
381 };
382 if did_str == identity_did.as_str() {
383 Some(KeyAlias::new_unchecked(alias.clone()))
384 } else {
385 None
386 }
387 })
388 .collect();
389 Ok(aliases)
390 }
391
392 fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError> {
393 let data = self.read_data()?;
394 let entry = data
395 .keys
396 .get(alias.as_str())
397 .ok_or(AgentError::KeyNotFound)?;
398 let did_str = match entry {
399 KeyEntry::WithRole(did, _, _) | KeyEntry::Legacy(did, _) => did,
400 };
401 parse_stored_did(did_str, alias)
402 }
403
404 fn backend_name(&self) -> &'static str {
405 "encrypted-file"
406 }
407}
408
409#[cfg(test)]
410#[allow(clippy::disallowed_methods)]
411#[allow(clippy::disallowed_types)]
412mod tests {
413 use super::*;
414 use tempfile::TempDir;
415
416 fn create_test_storage() -> (EncryptedFileStorage, TempDir) {
417 let temp_dir = TempDir::new().unwrap();
418 let storage = EncryptedFileStorage::new(temp_dir.path()).unwrap();
419 storage.set_password(Zeroizing::new("test_password".to_string()));
420 (storage, temp_dir)
421 }
422
423 #[test]
424 fn test_encrypt_decrypt_roundtrip() {
425 let password = "test_password";
426 let mut salt = [0u8; SALT_LEN];
427 rand::rngs::OsRng.fill_bytes(&mut salt);
428 let data = b"test data for encryption";
429
430 let key = EncryptedFileStorage::derive_key(password, &salt).unwrap();
431 let (ciphertext, nonce) = EncryptedFileStorage::encrypt(&key, data).unwrap();
432 let decrypted = EncryptedFileStorage::decrypt(&key, &nonce, &ciphertext).unwrap();
433
434 assert_eq!(data.as_slice(), decrypted.as_slice());
435 }
436
437 #[test]
438 fn test_wrong_password_fails() {
439 let mut salt = [0u8; SALT_LEN];
440 rand::rngs::OsRng.fill_bytes(&mut salt);
441 let data = b"test data";
442
443 let key1 = EncryptedFileStorage::derive_key("password1", &salt).unwrap();
444 let (ciphertext, nonce) = EncryptedFileStorage::encrypt(&key1, data).unwrap();
445
446 let key2 = EncryptedFileStorage::derive_key("password2", &salt).unwrap();
447 let result = EncryptedFileStorage::decrypt(&key2, &nonce, &ciphertext);
448
449 assert!(matches!(result, Err(AgentError::IncorrectPassphrase)));
450 }
451
452 #[test]
453 fn test_store_and_load_key() {
454 let (storage, _temp) = create_test_storage();
455 let alias = KeyAlias::new("test-alias").unwrap();
456 let identity_did = IdentityDID::parse("did:keri:test123").unwrap();
457 let encrypted_data = b"encrypted_key_bytes";
458
459 storage
460 .store_key(&alias, &identity_did, KeyRole::Primary, encrypted_data)
461 .unwrap();
462
463 let (loaded_did, loaded_role, loaded_data) = storage.load_key(&alias).unwrap();
464 assert_eq!(loaded_did, identity_did);
465 assert_eq!(loaded_role, KeyRole::Primary);
466 assert_eq!(loaded_data, encrypted_data);
467 }
468
469 #[test]
470 fn keychain_file_has_no_plaintext_secret() {
471 use crate::crypto::signer::encrypt_keypair;
472 use base64::{Engine, engine::general_purpose::STANDARD as B64};
473 use ring::signature::Ed25519KeyPair;
474
475 fn contains(haystack: &[u8], needle: &[u8]) -> bool {
476 !needle.is_empty() && haystack.windows(needle.len()).any(|w| w == needle)
477 }
478
479 let (storage, temp) = create_test_storage();
480 let alias = KeyAlias::new("secret").unwrap();
481 let identity_did = IdentityDID::parse("did:keri:test123").unwrap();
482
483 let rng = ring::rand::SystemRandom::new();
485 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
486 let pkcs8_bytes = pkcs8.as_ref().to_vec();
487 let seed = pkcs8_bytes[16..48].to_vec(); let encrypted = encrypt_keypair(&pkcs8_bytes, "Test-passphrase1!").unwrap();
490 storage
491 .store_key(&alias, &identity_did, KeyRole::Primary, &encrypted)
492 .unwrap();
493
494 let raw = std::fs::read(temp.path().join("keys.enc")).unwrap();
495 assert!(!contains(&raw, &seed), "raw seed found in keystore file");
496 assert!(
497 !contains(&raw, &pkcs8_bytes),
498 "raw private key found in keystore file"
499 );
500
501 let parsed: serde_json::Value = serde_json::from_slice(&raw).unwrap();
504 let ciphertext = B64.decode(parsed["ciphertext"].as_str().unwrap()).unwrap();
505 assert!(
506 !contains(&ciphertext, &seed),
507 "raw seed recoverable from the base64-decoded ciphertext (encoded, not encrypted)"
508 );
509 assert!(
510 !contains(&ciphertext, &pkcs8_bytes),
511 "raw private key recoverable from the base64-decoded ciphertext"
512 );
513 }
514
515 #[test]
516 fn load_rejects_tampered_identity_did() {
517 let (storage, _temp) = create_test_storage();
518 let alias = KeyAlias::new("tampered").unwrap();
519
520 let mut data = KeyData::default();
523 data.keys.insert(
524 alias.as_str().to_string(),
525 KeyEntry::WithRole(
526 "not-a-did".to_string(),
527 KeyRole::Primary.to_string(),
528 BASE64.encode(b"x"),
529 ),
530 );
531 storage.write_data(&data).unwrap();
532
533 assert!(matches!(
534 storage.load_key(&alias),
535 Err(AgentError::SecurityError(_))
536 ));
537 assert!(matches!(
538 storage.get_identity_for_alias(&alias),
539 Err(AgentError::SecurityError(_))
540 ));
541 }
542
543 #[test]
544 fn test_list_aliases() {
545 let (storage, _temp) = create_test_storage();
546 let did = IdentityDID::parse("did:keri:test").unwrap();
547
548 storage
549 .store_key(
550 &KeyAlias::new("alias1").unwrap(),
551 &did,
552 KeyRole::Primary,
553 b"data1",
554 )
555 .unwrap();
556 storage
557 .store_key(
558 &KeyAlias::new("alias2").unwrap(),
559 &did,
560 KeyRole::Primary,
561 b"data2",
562 )
563 .unwrap();
564
565 let mut aliases = storage.list_aliases().unwrap();
566 aliases.sort();
567 assert_eq!(
568 aliases,
569 vec![
570 KeyAlias::new_unchecked("alias1"),
571 KeyAlias::new_unchecked("alias2")
572 ]
573 );
574 }
575
576 #[test]
577 fn test_list_aliases_for_identity() {
578 let (storage, _temp) = create_test_storage();
579 let did1 = IdentityDID::parse("did:keri:one").unwrap();
580 let did2 = IdentityDID::parse("did:keri:two").unwrap();
581
582 storage
583 .store_key(
584 &KeyAlias::new("a1").unwrap(),
585 &did1,
586 KeyRole::Primary,
587 b"data1",
588 )
589 .unwrap();
590 storage
591 .store_key(
592 &KeyAlias::new("a2").unwrap(),
593 &did1,
594 KeyRole::Primary,
595 b"data2",
596 )
597 .unwrap();
598 storage
599 .store_key(
600 &KeyAlias::new("b1").unwrap(),
601 &did2,
602 KeyRole::Primary,
603 b"data3",
604 )
605 .unwrap();
606
607 let mut aliases = storage.list_aliases_for_identity(&did1).unwrap();
608 aliases.sort();
609 assert_eq!(
610 aliases,
611 vec![KeyAlias::new_unchecked("a1"), KeyAlias::new_unchecked("a2")]
612 );
613 }
614
615 #[test]
616 fn test_delete_key() {
617 let (storage, _temp) = create_test_storage();
618 let did = IdentityDID::parse("did:keri:test").unwrap();
619 let alias = KeyAlias::new("alias").unwrap();
620
621 storage
622 .store_key(&alias, &did, KeyRole::Primary, b"data")
623 .unwrap();
624 assert!(storage.load_key(&alias).is_ok());
625
626 storage.delete_key(&alias).unwrap();
627 assert!(matches!(
628 storage.load_key(&alias),
629 Err(AgentError::KeyNotFound)
630 ));
631 }
632
633 #[test]
634 fn test_get_identity_for_alias() {
635 let (storage, _temp) = create_test_storage();
636 let did = IdentityDID::parse("did:keri:test123").unwrap();
637 let alias = KeyAlias::new("alias").unwrap();
638
639 storage
640 .store_key(&alias, &did, KeyRole::Primary, b"data")
641 .unwrap();
642
643 let loaded_did = storage.get_identity_for_alias(&alias).unwrap();
644 assert_eq!(loaded_did, did);
645 }
646
647 #[test]
648 fn test_backend_name() {
649 let (storage, _temp) = create_test_storage();
650 assert_eq!(storage.backend_name(), "encrypted-file");
651 }
652
653 #[test]
654 fn test_file_format_version() {
655 let (storage, _temp) = create_test_storage();
656 let did = IdentityDID::parse("did:keri:test").unwrap();
657
658 storage
659 .store_key(
660 &KeyAlias::new("alias").unwrap(),
661 &did,
662 KeyRole::Primary,
663 b"data",
664 )
665 .unwrap();
666
667 let contents = fs::read_to_string(&storage.path).unwrap();
669 let encrypted: EncryptedFileFormat = serde_json::from_str(&contents).unwrap();
670
671 assert_eq!(encrypted.version, FILE_FORMAT_VERSION);
672 assert!(!encrypted.salt.is_empty());
673 assert!(!encrypted.nonce.is_empty());
674 assert!(!encrypted.ciphertext.is_empty());
675 }
676
677 #[test]
678 fn test_missing_password_error() {
679 let temp_dir = TempDir::new().unwrap();
680 let storage = EncryptedFileStorage::new(temp_dir.path()).unwrap();
681 let did = IdentityDID::parse("did:keri:test").unwrap();
682 let result = storage.store_key(
683 &KeyAlias::new("alias").unwrap(),
684 &did,
685 KeyRole::Primary,
686 b"data",
687 );
688 assert!(matches!(result, Err(AgentError::MissingPassphrase)));
689 }
690
691 #[test]
692 fn test_key_not_found() {
693 let (storage, _temp) = create_test_storage();
694
695 let result = storage.load_key(&KeyAlias::new("nonexistent").unwrap());
696 assert!(matches!(result, Err(AgentError::KeyNotFound)));
697 }
698
699 #[test]
700 fn test_legacy_key_data_migration() {
701 let old_json = r#"{"keys":{"my-key":["did:keri:Eabc","dGVzdA=="]}}"#;
703 let data: KeyData = serde_json::from_str(old_json).unwrap();
704 let entry = data.keys.get("my-key").unwrap();
705 match entry {
706 KeyEntry::Legacy(did, _b64) => assert_eq!(did, "did:keri:Eabc"),
707 KeyEntry::WithRole(..) => panic!("should deserialize as Legacy"),
708 }
709 }
710
711 #[test]
712 fn test_new_key_data_format() {
713 let new_json = r#"{"keys":{"my-key":["did:keri:Eabc","primary","dGVzdA=="]}}"#;
714 let data: KeyData = serde_json::from_str(new_json).unwrap();
715 let entry = data.keys.get("my-key").unwrap();
716 match entry {
717 KeyEntry::WithRole(did, role, _b64) => {
718 assert_eq!(did, "did:keri:Eabc");
719 assert_eq!(role, "primary");
720 }
721 KeyEntry::Legacy(..) => panic!("should deserialize as WithRole"),
722 }
723 }
724}