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
295#[allow(clippy::disallowed_methods)] #[allow(clippy::disallowed_types)]
297impl KeyStorage for EncryptedFileStorage {
298 fn store_key(
299 &self,
300 alias: &KeyAlias,
301 identity_did: &IdentityDID,
302 role: KeyRole,
303 encrypted_key_data: &[u8],
304 ) -> Result<(), AgentError> {
305 let mut data = self.read_data()?;
306 data.keys.insert(
307 alias.as_str().to_string(),
308 KeyEntry::WithRole(
309 identity_did.as_str().to_string(),
310 role.to_string(),
311 BASE64.encode(encrypted_key_data),
312 ),
313 );
314 self.write_data(&data)
315 }
316
317 fn load_key(&self, alias: &KeyAlias) -> Result<(IdentityDID, KeyRole, Vec<u8>), AgentError> {
318 let data = self.read_data()?;
319 let entry = data
320 .keys
321 .get(alias.as_str())
322 .ok_or(AgentError::KeyNotFound)?;
323 match entry {
324 KeyEntry::WithRole(did, role_str, b64) => {
325 let role = role_str.parse::<KeyRole>().unwrap_or(KeyRole::Primary);
326 let key_bytes = BASE64.decode(b64).map_err(|e| {
327 AgentError::StorageError(format!("Invalid key encoding: {}", e))
328 })?;
329 Ok((IdentityDID::new_unchecked(did.clone()), role, key_bytes))
330 }
331 KeyEntry::Legacy(did, b64) => {
332 let key_bytes = BASE64.decode(b64).map_err(|e| {
333 AgentError::StorageError(format!("Invalid key encoding: {}", e))
334 })?;
335 Ok((
336 IdentityDID::new_unchecked(did.clone()),
337 KeyRole::Primary,
338 key_bytes,
339 ))
340 }
341 }
342 }
343
344 fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError> {
345 let mut data = self.read_data()?;
346 data.keys.remove(alias.as_str());
347 self.write_data(&data)
348 }
349
350 fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError> {
351 let data = self.read_data()?;
352 Ok(data
353 .keys
354 .keys()
355 .map(|k| KeyAlias::new_unchecked(k.clone()))
356 .collect())
357 }
358
359 fn list_aliases_for_identity(
360 &self,
361 identity_did: &IdentityDID,
362 ) -> Result<Vec<KeyAlias>, AgentError> {
363 let data = self.read_data()?;
364 let aliases = data
365 .keys
366 .iter()
367 .filter_map(|(alias, entry)| {
368 let did_str = match entry {
369 KeyEntry::WithRole(did, _, _) | KeyEntry::Legacy(did, _) => did,
370 };
371 if did_str == identity_did.as_str() {
372 Some(KeyAlias::new_unchecked(alias.clone()))
373 } else {
374 None
375 }
376 })
377 .collect();
378 Ok(aliases)
379 }
380
381 fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError> {
382 let data = self.read_data()?;
383 data.keys
384 .get(alias.as_str())
385 .map(|entry| {
386 let did_str = match entry {
387 KeyEntry::WithRole(did, _, _) | KeyEntry::Legacy(did, _) => did,
388 };
389 IdentityDID::new_unchecked(did_str.clone())
390 })
391 .ok_or(AgentError::KeyNotFound)
392 }
393
394 fn backend_name(&self) -> &'static str {
395 "encrypted-file"
396 }
397}
398
399#[cfg(test)]
400#[allow(clippy::disallowed_methods)]
401#[allow(clippy::disallowed_types)]
402mod tests {
403 use super::*;
404 use tempfile::TempDir;
405
406 fn create_test_storage() -> (EncryptedFileStorage, TempDir) {
407 let temp_dir = TempDir::new().unwrap();
408 let storage = EncryptedFileStorage::new(temp_dir.path()).unwrap();
409 storage.set_password(Zeroizing::new("test_password".to_string()));
410 (storage, temp_dir)
411 }
412
413 #[test]
414 fn test_encrypt_decrypt_roundtrip() {
415 let password = "test_password";
416 let mut salt = [0u8; SALT_LEN];
417 rand::rngs::OsRng.fill_bytes(&mut salt);
418 let data = b"test data for encryption";
419
420 let key = EncryptedFileStorage::derive_key(password, &salt).unwrap();
421 let (ciphertext, nonce) = EncryptedFileStorage::encrypt(&key, data).unwrap();
422 let decrypted = EncryptedFileStorage::decrypt(&key, &nonce, &ciphertext).unwrap();
423
424 assert_eq!(data.as_slice(), decrypted.as_slice());
425 }
426
427 #[test]
428 fn test_wrong_password_fails() {
429 let mut salt = [0u8; SALT_LEN];
430 rand::rngs::OsRng.fill_bytes(&mut salt);
431 let data = b"test data";
432
433 let key1 = EncryptedFileStorage::derive_key("password1", &salt).unwrap();
434 let (ciphertext, nonce) = EncryptedFileStorage::encrypt(&key1, data).unwrap();
435
436 let key2 = EncryptedFileStorage::derive_key("password2", &salt).unwrap();
437 let result = EncryptedFileStorage::decrypt(&key2, &nonce, &ciphertext);
438
439 assert!(matches!(result, Err(AgentError::IncorrectPassphrase)));
440 }
441
442 #[test]
443 fn test_store_and_load_key() {
444 let (storage, _temp) = create_test_storage();
445 let alias = KeyAlias::new("test-alias").unwrap();
446 let identity_did = IdentityDID::new_unchecked("did:keri:test123");
447 let encrypted_data = b"encrypted_key_bytes";
448
449 storage
450 .store_key(&alias, &identity_did, KeyRole::Primary, encrypted_data)
451 .unwrap();
452
453 let (loaded_did, loaded_role, loaded_data) = storage.load_key(&alias).unwrap();
454 assert_eq!(loaded_did, identity_did);
455 assert_eq!(loaded_role, KeyRole::Primary);
456 assert_eq!(loaded_data, encrypted_data);
457 }
458
459 #[test]
460 fn test_list_aliases() {
461 let (storage, _temp) = create_test_storage();
462 let did = IdentityDID::new_unchecked("did:keri:test");
463
464 storage
465 .store_key(
466 &KeyAlias::new("alias1").unwrap(),
467 &did,
468 KeyRole::Primary,
469 b"data1",
470 )
471 .unwrap();
472 storage
473 .store_key(
474 &KeyAlias::new("alias2").unwrap(),
475 &did,
476 KeyRole::Primary,
477 b"data2",
478 )
479 .unwrap();
480
481 let mut aliases = storage.list_aliases().unwrap();
482 aliases.sort();
483 assert_eq!(
484 aliases,
485 vec![
486 KeyAlias::new_unchecked("alias1"),
487 KeyAlias::new_unchecked("alias2")
488 ]
489 );
490 }
491
492 #[test]
493 fn test_list_aliases_for_identity() {
494 let (storage, _temp) = create_test_storage();
495 let did1 = IdentityDID::new_unchecked("did:keri:one");
496 let did2 = IdentityDID::new_unchecked("did:keri:two");
497
498 storage
499 .store_key(
500 &KeyAlias::new("a1").unwrap(),
501 &did1,
502 KeyRole::Primary,
503 b"data1",
504 )
505 .unwrap();
506 storage
507 .store_key(
508 &KeyAlias::new("a2").unwrap(),
509 &did1,
510 KeyRole::Primary,
511 b"data2",
512 )
513 .unwrap();
514 storage
515 .store_key(
516 &KeyAlias::new("b1").unwrap(),
517 &did2,
518 KeyRole::Primary,
519 b"data3",
520 )
521 .unwrap();
522
523 let mut aliases = storage.list_aliases_for_identity(&did1).unwrap();
524 aliases.sort();
525 assert_eq!(
526 aliases,
527 vec![KeyAlias::new_unchecked("a1"), KeyAlias::new_unchecked("a2")]
528 );
529 }
530
531 #[test]
532 fn test_delete_key() {
533 let (storage, _temp) = create_test_storage();
534 let did = IdentityDID::new_unchecked("did:keri:test");
535 let alias = KeyAlias::new("alias").unwrap();
536
537 storage
538 .store_key(&alias, &did, KeyRole::Primary, b"data")
539 .unwrap();
540 assert!(storage.load_key(&alias).is_ok());
541
542 storage.delete_key(&alias).unwrap();
543 assert!(matches!(
544 storage.load_key(&alias),
545 Err(AgentError::KeyNotFound)
546 ));
547 }
548
549 #[test]
550 fn test_get_identity_for_alias() {
551 let (storage, _temp) = create_test_storage();
552 let did = IdentityDID::new_unchecked("did:keri:test123");
553 let alias = KeyAlias::new("alias").unwrap();
554
555 storage
556 .store_key(&alias, &did, KeyRole::Primary, b"data")
557 .unwrap();
558
559 let loaded_did = storage.get_identity_for_alias(&alias).unwrap();
560 assert_eq!(loaded_did, did);
561 }
562
563 #[test]
564 fn test_backend_name() {
565 let (storage, _temp) = create_test_storage();
566 assert_eq!(storage.backend_name(), "encrypted-file");
567 }
568
569 #[test]
570 fn test_file_format_version() {
571 let (storage, _temp) = create_test_storage();
572 let did = IdentityDID::new_unchecked("did:keri:test");
573
574 storage
575 .store_key(
576 &KeyAlias::new("alias").unwrap(),
577 &did,
578 KeyRole::Primary,
579 b"data",
580 )
581 .unwrap();
582
583 let contents = fs::read_to_string(&storage.path).unwrap();
585 let encrypted: EncryptedFileFormat = serde_json::from_str(&contents).unwrap();
586
587 assert_eq!(encrypted.version, FILE_FORMAT_VERSION);
588 assert!(!encrypted.salt.is_empty());
589 assert!(!encrypted.nonce.is_empty());
590 assert!(!encrypted.ciphertext.is_empty());
591 }
592
593 #[test]
594 fn test_missing_password_error() {
595 let temp_dir = TempDir::new().unwrap();
596 let storage = EncryptedFileStorage::new(temp_dir.path()).unwrap();
597 let did = IdentityDID::new_unchecked("did:test".to_string());
598 let result = storage.store_key(
599 &KeyAlias::new("alias").unwrap(),
600 &did,
601 KeyRole::Primary,
602 b"data",
603 );
604 assert!(matches!(result, Err(AgentError::MissingPassphrase)));
605 }
606
607 #[test]
608 fn test_key_not_found() {
609 let (storage, _temp) = create_test_storage();
610
611 let result = storage.load_key(&KeyAlias::new("nonexistent").unwrap());
612 assert!(matches!(result, Err(AgentError::KeyNotFound)));
613 }
614
615 #[test]
616 fn test_legacy_key_data_migration() {
617 let old_json = r#"{"keys":{"my-key":["did:keri:Eabc","dGVzdA=="]}}"#;
619 let data: KeyData = serde_json::from_str(old_json).unwrap();
620 let entry = data.keys.get("my-key").unwrap();
621 match entry {
622 KeyEntry::Legacy(did, _b64) => assert_eq!(did, "did:keri:Eabc"),
623 KeyEntry::WithRole(..) => panic!("should deserialize as Legacy"),
624 }
625 }
626
627 #[test]
628 fn test_new_key_data_format() {
629 let new_json = r#"{"keys":{"my-key":["did:keri:Eabc","primary","dGVzdA=="]}}"#;
630 let data: KeyData = serde_json::from_str(new_json).unwrap();
631 let entry = data.keys.get("my-key").unwrap();
632 match entry {
633 KeyEntry::WithRole(did, role, _b64) => {
634 assert_eq!(did, "did:keri:Eabc");
635 assert_eq!(role, "primary");
636 }
637 KeyEntry::Legacy(..) => panic!("should deserialize as WithRole"),
638 }
639 }
640}