1use std::fs::{self, File};
37use std::io::{Read, Write};
38use std::path::PathBuf;
39
40#[cfg(unix)]
41use std::os::unix::fs::PermissionsExt;
42
43use txgate_core::error::StoreError;
44
45use crate::encryption::{decrypt_key, encrypt_key, EncryptedKey};
46use crate::keys::SecretKey;
47
48pub trait KeyStore: Send + Sync {
78 fn store(&self, name: &str, key: &SecretKey, passphrase: &str) -> Result<(), StoreError>;
91
92 fn load(&self, name: &str, passphrase: &str) -> Result<SecretKey, StoreError>;
104
105 fn list(&self) -> Result<Vec<String>, StoreError>;
113
114 fn delete(&self, name: &str) -> Result<(), StoreError>;
124
125 fn exists(&self, name: &str) -> bool;
134}
135
136pub struct FileKeyStore {
167 keys_dir: PathBuf,
169}
170
171impl FileKeyStore {
172 pub fn new() -> Result<Self, StoreError> {
187 let keys_dir = dirs::home_dir()
188 .ok_or_else(|| {
189 StoreError::IoError(std::io::Error::new(
190 std::io::ErrorKind::NotFound,
191 "Could not determine home directory",
192 ))
193 })?
194 .join(".txgate")
195 .join("keys");
196
197 Self::with_path(keys_dir)
198 }
199
200 pub fn with_path(keys_dir: PathBuf) -> Result<Self, StoreError> {
222 if !keys_dir.exists() {
224 fs::create_dir_all(&keys_dir)?;
225 }
226
227 #[cfg(unix)]
229 {
230 let mut perms = fs::metadata(&keys_dir)?.permissions();
231 perms.set_mode(0o700);
232 fs::set_permissions(&keys_dir, perms)?;
233 }
234
235 Ok(Self { keys_dir })
236 }
237
238 fn key_path(&self, name: &str) -> PathBuf {
240 self.keys_dir.join(format!("{name}.enc"))
241 }
242
243 fn validate_name(name: &str) -> Result<(), StoreError> {
255 if name.is_empty() {
256 return Err(StoreError::InvalidFormat);
257 }
258
259 if name.starts_with('.') {
260 return Err(StoreError::InvalidFormat);
261 }
262
263 if !name
264 .chars()
265 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
266 {
267 return Err(StoreError::InvalidFormat);
268 }
269
270 Ok(())
271 }
272
273 #[must_use]
277 pub const fn keys_dir(&self) -> &PathBuf {
278 &self.keys_dir
279 }
280}
281
282impl KeyStore for FileKeyStore {
283 fn store(&self, name: &str, key: &SecretKey, passphrase: &str) -> Result<(), StoreError> {
284 Self::validate_name(name)?;
285
286 let path = self.key_path(name);
287 if path.exists() {
288 return Err(StoreError::KeyExists {
289 name: name.to_string(),
290 });
291 }
292
293 let encrypted = encrypt_key(key, passphrase)?;
295 let bytes = encrypted.to_bytes();
296
297 let temp_path = self.keys_dir.join(format!(".{name}.tmp"));
299
300 {
302 let mut file = File::create(&temp_path)?;
303 file.write_all(&bytes)?;
304 file.sync_all()?;
305 }
306
307 #[cfg(unix)]
309 {
310 let mut perms = fs::metadata(&temp_path)?.permissions();
311 perms.set_mode(0o600);
312 fs::set_permissions(&temp_path, perms)?;
313 }
314
315 fs::rename(&temp_path, &path)?;
317
318 Ok(())
319 }
320
321 fn load(&self, name: &str, passphrase: &str) -> Result<SecretKey, StoreError> {
322 Self::validate_name(name)?;
323
324 let path = self.key_path(name);
325 if !path.exists() {
326 return Err(StoreError::KeyNotFound {
327 name: name.to_string(),
328 });
329 }
330
331 let mut file = File::open(&path)?;
333 let mut bytes = Vec::new();
334 file.read_to_end(&mut bytes)?;
335
336 let encrypted = EncryptedKey::from_bytes(&bytes)?;
338 decrypt_key(&encrypted, passphrase)
339 }
340
341 fn list(&self) -> Result<Vec<String>, StoreError> {
342 let mut names = Vec::new();
343
344 for entry in fs::read_dir(&self.keys_dir)? {
345 let entry = entry?;
346 let path = entry.path();
347
348 if path.extension().is_some_and(|ext| ext == "enc") {
350 if let Some(stem) = path.file_stem() {
351 if let Some(name) = stem.to_str() {
352 if !name.starts_with('.') {
354 names.push(name.to_string());
355 }
356 }
357 }
358 }
359 }
360
361 names.sort();
362 Ok(names)
363 }
364
365 fn delete(&self, name: &str) -> Result<(), StoreError> {
366 Self::validate_name(name)?;
367
368 let path = self.key_path(name);
369 if !path.exists() {
370 return Err(StoreError::KeyNotFound {
371 name: name.to_string(),
372 });
373 }
374
375 fs::remove_file(&path)?;
376 Ok(())
377 }
378
379 fn exists(&self, name: &str) -> bool {
380 if Self::validate_name(name).is_err() {
381 return false;
382 }
383 self.key_path(name).exists()
384 }
385}
386
387#[cfg(test)]
392mod tests {
393 #![allow(clippy::expect_used)]
394 #![allow(clippy::similar_names)]
395 #![allow(clippy::case_sensitive_file_extension_comparisons)]
396 #![allow(clippy::unwrap_used)]
397
398 use super::*;
399 use std::sync::Arc;
400 use std::thread;
401 use tempfile::TempDir;
402
403 fn create_test_store() -> (FileKeyStore, TempDir) {
405 let temp_dir = TempDir::new().expect("failed to create temp dir");
406 let store =
407 FileKeyStore::with_path(temp_dir.path().to_path_buf()).expect("failed to create store");
408 (store, temp_dir)
409 }
410
411 #[test]
416 fn test_store_load_round_trip() {
417 let (store, _temp) = create_test_store();
418 let key = SecretKey::generate();
419 let passphrase = "test-passphrase-123";
420
421 store
422 .store("test-key", &key, passphrase)
423 .expect("store should succeed");
424 let loaded = store
425 .load("test-key", passphrase)
426 .expect("load should succeed");
427
428 assert_eq!(key.as_bytes(), loaded.as_bytes());
429 }
430
431 #[test]
432 fn test_store_load_multiple_keys() {
433 let (store, _temp) = create_test_store();
434 let passphrase = "common-passphrase";
435
436 let key1 = SecretKey::generate();
437 let key2 = SecretKey::generate();
438 let key3 = SecretKey::generate();
439
440 store
441 .store("key-1", &key1, passphrase)
442 .expect("store should succeed");
443 store
444 .store("key-2", &key2, passphrase)
445 .expect("store should succeed");
446 store
447 .store("key-3", &key3, passphrase)
448 .expect("store should succeed");
449
450 let loaded1 = store
451 .load("key-1", passphrase)
452 .expect("load should succeed");
453 let loaded2 = store
454 .load("key-2", passphrase)
455 .expect("load should succeed");
456 let loaded3 = store
457 .load("key-3", passphrase)
458 .expect("load should succeed");
459
460 assert_eq!(key1.as_bytes(), loaded1.as_bytes());
461 assert_eq!(key2.as_bytes(), loaded2.as_bytes());
462 assert_eq!(key3.as_bytes(), loaded3.as_bytes());
463 }
464
465 #[test]
466 fn test_store_load_different_passphrases() {
467 let (store, _temp) = create_test_store();
468
469 let key1 = SecretKey::generate();
470 let key2 = SecretKey::generate();
471
472 store
473 .store("key-1", &key1, "passphrase-1")
474 .expect("store should succeed");
475 store
476 .store("key-2", &key2, "passphrase-2")
477 .expect("store should succeed");
478
479 let loaded1 = store
480 .load("key-1", "passphrase-1")
481 .expect("load should succeed");
482 let loaded2 = store
483 .load("key-2", "passphrase-2")
484 .expect("load should succeed");
485
486 assert_eq!(key1.as_bytes(), loaded1.as_bytes());
487 assert_eq!(key2.as_bytes(), loaded2.as_bytes());
488 }
489
490 #[test]
495 fn test_list_empty_store() {
496 let (store, _temp) = create_test_store();
497
498 let keys = store.list().expect("list should succeed");
499 assert!(keys.is_empty());
500 }
501
502 #[test]
503 fn test_list_returns_sorted_names() {
504 let (store, _temp) = create_test_store();
505 let passphrase = "test";
506
507 store
509 .store("zebra", &SecretKey::generate(), passphrase)
510 .expect("store should succeed");
511 store
512 .store("apple", &SecretKey::generate(), passphrase)
513 .expect("store should succeed");
514 store
515 .store("mango", &SecretKey::generate(), passphrase)
516 .expect("store should succeed");
517
518 let keys = store.list().expect("list should succeed");
519 assert_eq!(keys, vec!["apple", "mango", "zebra"]);
520 }
521
522 #[test]
523 fn test_list_ignores_temp_files() {
524 let (store, temp) = create_test_store();
525 let passphrase = "test";
526
527 store
528 .store("real-key", &SecretKey::generate(), passphrase)
529 .expect("store should succeed");
530
531 let temp_path = temp.path().join(".temp-key.tmp");
533 fs::write(&temp_path, b"garbage").expect("write should succeed");
534
535 let keys = store.list().expect("list should succeed");
536 assert_eq!(keys, vec!["real-key"]);
537 }
538
539 #[test]
540 fn test_list_ignores_non_enc_files() {
541 let (store, temp) = create_test_store();
542 let passphrase = "test";
543
544 store
545 .store("real-key", &SecretKey::generate(), passphrase)
546 .expect("store should succeed");
547
548 fs::write(temp.path().join("readme.txt"), b"text").expect("write should succeed");
550 fs::write(temp.path().join("backup.bak"), b"backup").expect("write should succeed");
551
552 let keys = store.list().expect("list should succeed");
553 assert_eq!(keys, vec!["real-key"]);
554 }
555
556 #[test]
561 fn test_delete_removes_key() {
562 let (store, _temp) = create_test_store();
563 let passphrase = "test";
564
565 store
566 .store("to-delete", &SecretKey::generate(), passphrase)
567 .expect("store should succeed");
568 assert!(store.exists("to-delete"));
569
570 store.delete("to-delete").expect("delete should succeed");
571 assert!(!store.exists("to-delete"));
572 }
573
574 #[test]
575 fn test_delete_nonexistent_returns_error() {
576 let (store, _temp) = create_test_store();
577
578 let result = store.delete("nonexistent");
579 assert!(matches!(result, Err(StoreError::KeyNotFound { .. })));
580 }
581
582 #[test]
583 fn test_delete_then_store_same_name() {
584 let (store, _temp) = create_test_store();
585 let passphrase = "test";
586
587 let key1 = SecretKey::generate();
588 store
589 .store("reusable", &key1, passphrase)
590 .expect("store should succeed");
591
592 store.delete("reusable").expect("delete should succeed");
593
594 let key2 = SecretKey::generate();
595 store
596 .store("reusable", &key2, passphrase)
597 .expect("store should succeed");
598
599 let loaded = store
600 .load("reusable", passphrase)
601 .expect("load should succeed");
602 assert_eq!(key2.as_bytes(), loaded.as_bytes());
603 }
604
605 #[test]
610 fn test_exists_returns_true_for_existing_key() {
611 let (store, _temp) = create_test_store();
612
613 store
614 .store("existing", &SecretKey::generate(), "test")
615 .expect("store should succeed");
616 assert!(store.exists("existing"));
617 }
618
619 #[test]
620 fn test_exists_returns_false_for_nonexistent_key() {
621 let (store, _temp) = create_test_store();
622 assert!(!store.exists("nonexistent"));
623 }
624
625 #[test]
626 fn test_exists_returns_false_for_invalid_name() {
627 let (store, _temp) = create_test_store();
628 assert!(!store.exists(""));
629 assert!(!store.exists("../etc/passwd"));
630 assert!(!store.exists(".hidden"));
631 }
632
633 #[test]
638 fn test_store_duplicate_returns_error() {
639 let (store, _temp) = create_test_store();
640 let passphrase = "test";
641
642 store
643 .store("duplicate", &SecretKey::generate(), passphrase)
644 .expect("first store should succeed");
645
646 let result = store.store("duplicate", &SecretKey::generate(), passphrase);
647 assert!(matches!(result, Err(StoreError::KeyExists { .. })));
648 }
649
650 #[test]
651 fn test_load_nonexistent_returns_error() {
652 let (store, _temp) = create_test_store();
653
654 let result = store.load("nonexistent", "passphrase");
655 assert!(matches!(result, Err(StoreError::KeyNotFound { .. })));
656 }
657
658 #[test]
659 fn test_load_wrong_passphrase_returns_error() {
660 let (store, _temp) = create_test_store();
661
662 store
663 .store("secure-key", &SecretKey::generate(), "correct-passphrase")
664 .expect("store should succeed");
665
666 let result = store.load("secure-key", "wrong-passphrase");
667 assert!(matches!(result, Err(StoreError::DecryptionFailed)));
668 }
669
670 #[test]
675 fn test_valid_names() {
676 let (store, _temp) = create_test_store();
677 let passphrase = "test";
678
679 let valid_names = vec![
681 "default",
682 "my-key",
683 "my_key",
684 "key123",
685 "KEY",
686 "a",
687 "hot-wallet-1",
688 "cold_storage_backup",
689 ];
690
691 for name in valid_names {
692 let result = store.store(name, &SecretKey::generate(), passphrase);
693 assert!(result.is_ok(), "Name should be valid: {name}");
694 }
695 }
696
697 #[test]
698 fn test_invalid_names_empty() {
699 let (store, _temp) = create_test_store();
700
701 let result = store.store("", &SecretKey::generate(), "test");
702 assert!(matches!(result, Err(StoreError::InvalidFormat)));
703 }
704
705 #[test]
706 fn test_invalid_names_path_traversal() {
707 let (store, _temp) = create_test_store();
708
709 let invalid_names = vec![
710 "../etc/passwd",
711 "..\\windows\\system32",
712 "some/path",
713 "some\\path",
714 ];
715
716 for name in invalid_names {
717 let result = store.store(name, &SecretKey::generate(), "test");
718 assert!(
719 matches!(result, Err(StoreError::InvalidFormat)),
720 "Name should be invalid: {name}"
721 );
722 }
723 }
724
725 #[test]
726 fn test_invalid_names_hidden() {
727 let (store, _temp) = create_test_store();
728
729 let result = store.store(".hidden", &SecretKey::generate(), "test");
730 assert!(matches!(result, Err(StoreError::InvalidFormat)));
731 }
732
733 #[test]
734 fn test_invalid_names_special_characters() {
735 let (store, _temp) = create_test_store();
736
737 let invalid_names = vec![
738 "key with spaces",
739 "key@special",
740 "key#hash",
741 "key$dollar",
742 "key%percent",
743 "key*star",
744 "key!bang",
745 ];
746
747 for name in invalid_names {
748 let result = store.store(name, &SecretKey::generate(), "test");
749 assert!(
750 matches!(result, Err(StoreError::InvalidFormat)),
751 "Name should be invalid: {name}"
752 );
753 }
754 }
755
756 #[cfg(unix)]
761 #[test]
762 fn test_directory_permissions() {
763 let (store, _temp) = create_test_store();
764
765 let metadata = fs::metadata(store.keys_dir()).expect("failed to get metadata");
766 let mode = metadata.permissions().mode();
767
768 assert_eq!(
770 mode & 0o777,
771 0o700,
772 "Directory should have 0700 permissions"
773 );
774 }
775
776 #[cfg(unix)]
777 #[test]
778 fn test_file_permissions() {
779 let (store, _temp) = create_test_store();
780
781 store
782 .store("test-key", &SecretKey::generate(), "test")
783 .expect("store should succeed");
784
785 let path = store.key_path("test-key");
786 let metadata = fs::metadata(&path).expect("failed to get metadata");
787 let mode = metadata.permissions().mode();
788
789 assert_eq!(mode & 0o777, 0o600, "File should have 0600 permissions");
791 }
792
793 #[test]
798 fn test_atomic_write_no_temp_files_left() {
799 let (store, temp) = create_test_store();
800
801 store
802 .store("atomic-test", &SecretKey::generate(), "test")
803 .expect("store should succeed");
804
805 for entry in fs::read_dir(temp.path()).expect("failed to read dir") {
807 let entry = entry.expect("failed to get entry");
808 let name = entry.file_name().to_string_lossy().to_string();
809 assert!(
810 !name.starts_with('.') || !name.ends_with(".tmp"),
811 "Temp file should not exist: {name}"
812 );
813 }
814 }
815
816 #[test]
821 fn test_key_store_is_send_sync() {
822 fn assert_send_sync<T: Send + Sync>() {}
823 assert_send_sync::<FileKeyStore>();
824 }
825
826 #[test]
827 fn test_trait_object_is_send_sync() {
828 fn assert_send_sync<T: Send + Sync>() {}
829 assert_send_sync::<Box<dyn KeyStore>>();
830 }
831
832 #[test]
837 fn test_full_workflow() {
838 let (store, _temp) = create_test_store();
839
840 assert!(store.list().expect("list should succeed").is_empty());
842
843 let key1 = SecretKey::generate();
845 let key2 = SecretKey::generate();
846
847 store
848 .store("wallet-1", &key1, "pass1")
849 .expect("store should succeed");
850 store
851 .store("wallet-2", &key2, "pass2")
852 .expect("store should succeed");
853
854 let keys = store.list().expect("list should succeed");
856 assert_eq!(keys, vec!["wallet-1", "wallet-2"]);
857
858 assert!(store.exists("wallet-1"));
860 assert!(store.exists("wallet-2"));
861 assert!(!store.exists("wallet-3"));
862
863 let loaded1 = store
865 .load("wallet-1", "pass1")
866 .expect("load should succeed");
867 assert_eq!(key1.as_bytes(), loaded1.as_bytes());
868
869 store.delete("wallet-1").expect("delete should succeed");
871 assert!(!store.exists("wallet-1"));
872
873 let keys = store.list().expect("list should succeed");
874 assert_eq!(keys, vec!["wallet-2"]);
875 }
876
877 #[test]
882 fn test_with_path_creates_directory() {
883 let temp = TempDir::new().expect("failed to create temp dir");
884 let custom_path = temp.path().join("custom").join("nested").join("keys");
885
886 assert!(!custom_path.exists());
887
888 let _store = FileKeyStore::with_path(custom_path.clone()).expect("should create directory");
889
890 assert!(custom_path.exists());
891 assert!(custom_path.is_dir());
892 }
893
894 #[test]
895 fn test_keys_dir_getter() {
896 let (store, temp) = create_test_store();
897 assert_eq!(store.keys_dir(), temp.path());
898 }
899
900 #[test]
905 fn test_validate_name_all_edge_cases() {
906 assert!(FileKeyStore::validate_name("").is_err());
910
911 assert!(FileKeyStore::validate_name(".test").is_err());
913
914 assert!(FileKeyStore::validate_name("test/path").is_err());
916 assert!(FileKeyStore::validate_name("test\\path").is_err());
917 assert!(FileKeyStore::validate_name("test.key").is_err());
918 assert!(FileKeyStore::validate_name("test key").is_err());
919 assert!(FileKeyStore::validate_name("test@key").is_err());
920
921 assert!(FileKeyStore::validate_name("test").is_ok());
923 assert!(FileKeyStore::validate_name("test-key").is_ok());
924 assert!(FileKeyStore::validate_name("test_key").is_ok());
925 assert!(FileKeyStore::validate_name("TEST123").is_ok());
926 assert!(FileKeyStore::validate_name("a").is_ok());
927 assert!(FileKeyStore::validate_name("1").is_ok());
928 assert!(FileKeyStore::validate_name("_").is_ok());
929 assert!(FileKeyStore::validate_name("-").is_ok());
930 }
931
932 #[test]
933 fn test_load_with_invalid_name() {
934 let (store, _temp) = create_test_store();
935
936 let result = store.load("../invalid", "passphrase");
937 assert!(matches!(result, Err(StoreError::InvalidFormat)));
938 }
939
940 #[test]
941 fn test_delete_with_invalid_name() {
942 let (store, _temp) = create_test_store();
943
944 let result = store.delete(".invalid");
945 assert!(matches!(result, Err(StoreError::InvalidFormat)));
946 }
947
948 #[test]
949 fn test_store_creates_enc_extension() {
950 let (store, temp) = create_test_store();
951
952 store
953 .store("test", &SecretKey::generate(), "pass")
954 .expect("store should succeed");
955
956 let path = temp.path().join("test.enc");
957 assert!(path.exists());
958 }
959
960 #[test]
961 fn test_list_handles_invalid_utf8_filenames() {
962 let (store, _temp) = create_test_store();
964
965 store
967 .store("normal", &SecretKey::generate(), "pass")
968 .expect("store should succeed");
969
970 let keys = store.list().expect("list should succeed");
972 assert!(keys.contains(&"normal".to_string()));
973 }
974
975 #[test]
976 fn test_key_path_generation() {
977 let temp_dir = TempDir::new().expect("failed to create temp dir");
978 let store =
979 FileKeyStore::with_path(temp_dir.path().to_path_buf()).expect("failed to create store");
980
981 let path = store.key_path("test");
982 assert!(path.to_str().unwrap().ends_with("test.enc"));
983 }
984
985 #[cfg(not(unix))]
986 #[test]
987 fn test_non_unix_permissions_handling() {
988 let temp_dir = TempDir::new().expect("failed to create temp dir");
991 let store =
992 FileKeyStore::with_path(temp_dir.path().to_path_buf()).expect("failed to create store");
993
994 store
995 .store("test", &SecretKey::generate(), "pass")
996 .expect("store should succeed");
997
998 assert!(store.exists("test"));
999 }
1000
1001 #[test]
1006 #[cfg(unix)]
1007 fn test_store_fails_with_readonly_directory() {
1008 use std::os::unix::fs::PermissionsExt;
1009
1010 let temp_dir = TempDir::new().expect("failed to create temp dir");
1012 let store_path = temp_dir.path().to_path_buf();
1013 let store = FileKeyStore::with_path(store_path.clone()).expect("failed to create store");
1014
1015 let mut perms = fs::metadata(&store_path)
1017 .expect("failed to get metadata")
1018 .permissions();
1019 perms.set_mode(0o500); fs::set_permissions(&store_path, perms).expect("failed to set permissions");
1021
1022 let result = store.store("test-key", &SecretKey::generate(), "passphrase");
1024
1025 assert!(result.is_err());
1027
1028 let mut perms = fs::metadata(&store_path)
1030 .expect("failed to get metadata")
1031 .permissions();
1032 perms.set_mode(0o700);
1033 fs::set_permissions(&store_path, perms).expect("failed to set permissions");
1034 }
1035
1036 #[test]
1037 fn test_load_fails_with_nonexistent_directory() {
1038 let temp_dir = TempDir::new().expect("failed to create temp dir");
1040 let nonexistent_path = temp_dir.path().join("does_not_exist");
1041
1042 let store = FileKeyStore {
1044 keys_dir: nonexistent_path,
1045 };
1046
1047 let result = store.load("any-key", "passphrase");
1049
1050 assert!(result.is_err());
1052 assert!(matches!(
1053 result.unwrap_err(),
1054 StoreError::KeyNotFound { .. }
1055 ));
1056 }
1057
1058 #[test]
1059 fn test_load_fails_with_corrupted_file() {
1060 let temp_dir = TempDir::new().expect("failed to create temp dir");
1062 let store =
1063 FileKeyStore::with_path(temp_dir.path().to_path_buf()).expect("failed to create store");
1064
1065 let corrupted_path = temp_dir.path().join("corrupted.enc");
1067 fs::write(&corrupted_path, b"this is not valid encrypted data")
1068 .expect("failed to write corrupted file");
1069
1070 let result = store.load("corrupted", "passphrase");
1072
1073 assert!(result.is_err());
1075 let err = result.unwrap_err();
1076 assert!(
1077 matches!(
1078 err,
1079 StoreError::InvalidFormat | StoreError::DecryptionFailed
1080 ),
1081 "expected InvalidFormat or DecryptionFailed, got {err:?}"
1082 );
1083 }
1084
1085 #[test]
1086 #[cfg(unix)]
1087 fn test_list_fails_with_permission_denied() {
1088 use std::os::unix::fs::PermissionsExt;
1089
1090 let temp_dir = TempDir::new().expect("failed to create temp dir");
1092 let store_path = temp_dir.path().to_path_buf();
1093 let store = FileKeyStore::with_path(store_path.clone()).expect("failed to create store");
1094
1095 store
1097 .store("test", &SecretKey::generate(), "pass")
1098 .expect("store should succeed");
1099
1100 let mut perms = fs::metadata(&store_path)
1102 .expect("failed to get metadata")
1103 .permissions();
1104 perms.set_mode(0o300); fs::set_permissions(&store_path, perms).expect("failed to set permissions");
1106
1107 let result = store.list();
1109
1110 assert!(result.is_err());
1112
1113 let mut perms = fs::metadata(&store_path)
1115 .expect("failed to get metadata")
1116 .permissions();
1117 perms.set_mode(0o700);
1118 fs::set_permissions(&store_path, perms).expect("failed to set permissions");
1119 }
1120
1121 #[test]
1122 #[cfg(unix)]
1123 fn test_delete_fails_with_readonly_directory() {
1124 use std::os::unix::fs::PermissionsExt;
1125
1126 let temp_dir = TempDir::new().expect("failed to create temp dir");
1128 let store_path = temp_dir.path().to_path_buf();
1129 let store = FileKeyStore::with_path(store_path.clone()).expect("failed to create store");
1130
1131 store
1132 .store("test", &SecretKey::generate(), "pass")
1133 .expect("store should succeed");
1134
1135 let mut perms = fs::metadata(&store_path)
1137 .expect("failed to get metadata")
1138 .permissions();
1139 perms.set_mode(0o500); fs::set_permissions(&store_path, perms).expect("failed to set permissions");
1141
1142 let result = store.delete("test");
1144
1145 assert!(result.is_err());
1147
1148 let mut perms = fs::metadata(&store_path)
1150 .expect("failed to get metadata")
1151 .permissions();
1152 perms.set_mode(0o700);
1153 fs::set_permissions(&store_path, perms).expect("failed to set permissions");
1154 }
1155
1156 #[test]
1157 fn test_concurrent_read_operations() {
1158 let temp_dir = TempDir::new().expect("failed to create temp dir");
1160 let store = Arc::new(
1161 FileKeyStore::with_path(temp_dir.path().to_path_buf()).expect("failed to create store"),
1162 );
1163
1164 for i in 0..10 {
1166 let key = SecretKey::generate();
1167 store
1168 .store(&format!("key-{i}"), &key, "pass")
1169 .expect("store should succeed");
1170 }
1171
1172 let mut handles = vec![];
1174 for i in 0..10 {
1175 let store_clone = Arc::clone(&store);
1176 let handle = thread::spawn(move || {
1177 store_clone
1178 .load(&format!("key-{i}"), "pass")
1179 .expect("load should succeed")
1180 });
1181 handles.push(handle);
1182 }
1183
1184 for handle in handles {
1186 let _key = handle.join().expect("thread should not panic");
1187 }
1188 }
1189
1190 #[test]
1191 fn test_concurrent_write_operations() {
1192 let temp_dir = TempDir::new().expect("failed to create temp dir");
1194 let store = Arc::new(
1195 FileKeyStore::with_path(temp_dir.path().to_path_buf()).expect("failed to create store"),
1196 );
1197
1198 let mut handles = vec![];
1200 for i in 0..10 {
1201 let store_clone = Arc::clone(&store);
1202 let handle = thread::spawn(move || {
1203 let key = SecretKey::generate();
1204 store_clone
1205 .store(&format!("concurrent-{i}"), &key, "pass")
1206 .expect("store should succeed");
1207 });
1208 handles.push(handle);
1209 }
1210
1211 for handle in handles {
1213 handle.join().expect("thread should not panic");
1214 }
1215
1216 let keys = store.list().expect("list should succeed");
1218 assert_eq!(keys.len(), 10);
1219 }
1220
1221 #[test]
1222 fn test_concurrent_read_write_operations() {
1223 let temp_dir = TempDir::new().expect("failed to create temp dir");
1225 let store = Arc::new(
1226 FileKeyStore::with_path(temp_dir.path().to_path_buf()).expect("failed to create store"),
1227 );
1228
1229 for i in 0..5 {
1231 store
1232 .store(&format!("initial-{i}"), &SecretKey::generate(), "pass")
1233 .expect("store should succeed");
1234 }
1235
1236 let mut handles = vec![];
1238
1239 for i in 0..5 {
1241 let store_clone = Arc::clone(&store);
1242 let handle = thread::spawn(move || {
1243 for _ in 0..10 {
1244 let _ = store_clone.load(&format!("initial-{i}"), "pass");
1245 }
1246 });
1247 handles.push(handle);
1248 }
1249
1250 for i in 0..5 {
1252 let store_clone = Arc::clone(&store);
1253 let handle = thread::spawn(move || {
1254 let key = SecretKey::generate();
1255 store_clone
1256 .store(&format!("new-{i}"), &key, "pass")
1257 .expect("store should succeed");
1258 });
1259 handles.push(handle);
1260 }
1261
1262 for handle in handles {
1264 handle.join().expect("thread should not panic");
1265 }
1266
1267 let keys = store.list().expect("list should succeed");
1269 assert!(keys.len() >= 10); }
1271
1272 #[test]
1273 fn test_load_with_truncated_file() {
1274 let temp_dir = TempDir::new().expect("failed to create temp dir");
1276 let store =
1277 FileKeyStore::with_path(temp_dir.path().to_path_buf()).expect("failed to create store");
1278
1279 let truncated_path = temp_dir.path().join("truncated.enc");
1281 fs::write(&truncated_path, b"short").expect("failed to write truncated file");
1282
1283 let result = store.load("truncated", "passphrase");
1285
1286 assert!(result.is_err());
1288 assert!(matches!(result.unwrap_err(), StoreError::InvalidFormat));
1289 }
1290}