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