Skip to main content

txgate_crypto/
store.rs

1//! Key storage traits and implementations.
2//!
3//! This module provides secure key storage functionality with:
4//! - A trait-based interface for pluggable storage backends
5//! - A file-based implementation storing encrypted keys in `~/.txgate/keys/`
6//! - Automatic encryption/decryption using ChaCha20-Poly1305 with Argon2id
7//!
8//! # Security Properties
9//!
10//! - **Encryption at rest**: All keys are encrypted before storage
11//! - **Restricted permissions**: Files are created with 0600 (owner read/write only)
12//! - **Atomic writes**: Uses temp file + rename to prevent corruption
13//! - **Thread safety**: Implementations are `Send + Sync`
14//!
15//! # Example
16//!
17//! ```no_run
18//! use txgate_crypto::store::{KeyStore, FileKeyStore};
19//! use txgate_crypto::keys::SecretKey;
20//!
21//! // Create a key store
22//! let store = FileKeyStore::new().expect("failed to create key store");
23//!
24//! // Store a key
25//! let key = SecretKey::generate();
26//! store.store("my-wallet", &key, "secure-passphrase").expect("failed to store key");
27//!
28//! // List stored keys
29//! let keys = store.list().expect("failed to list keys");
30//! println!("Stored keys: {:?}", keys);
31//!
32//! // Load the key back
33//! let loaded = store.load("my-wallet", "secure-passphrase").expect("failed to load key");
34//! ```
35
36use 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
48// ============================================================================
49// KeyStore Trait
50// ============================================================================
51
52/// Trait for secure key storage.
53///
54/// Implementations must ensure:
55/// - Keys are encrypted at rest
56/// - File permissions prevent unauthorized access
57/// - Atomic operations prevent corruption
58/// - Thread-safe operations (`Send + Sync`)
59///
60/// # Example
61///
62/// ```no_run
63/// use txgate_crypto::store::{KeyStore, FileKeyStore};
64/// use txgate_crypto::keys::SecretKey;
65///
66/// fn example<S: KeyStore>(store: &S) -> Result<(), txgate_core::error::StoreError> {
67///     let key = SecretKey::generate();
68///     store.store("my-key", &key, "passphrase")?;
69///
70///     if store.exists("my-key") {
71///         let loaded = store.load("my-key", "passphrase")?;
72///     }
73///
74///     Ok(())
75/// }
76/// ```
77pub trait KeyStore: Send + Sync {
78    /// Store a secret key with the given name.
79    ///
80    /// # Arguments
81    /// * `name` - A unique identifier for the key (e.g., "default", "hot-wallet")
82    /// * `key` - The secret key to store
83    /// * `passphrase` - The passphrase to encrypt the key with
84    ///
85    /// # Errors
86    /// - `StoreError::KeyExists` if a key with this name already exists
87    /// - `StoreError::EncryptionFailed` if encryption fails
88    /// - `StoreError::IoError` if file operations fail
89    /// - `StoreError::InvalidFormat` if the name is invalid
90    fn store(&self, name: &str, key: &SecretKey, passphrase: &str) -> Result<(), StoreError>;
91
92    /// Load a secret key by name.
93    ///
94    /// # Arguments
95    /// * `name` - The identifier of the key to load
96    /// * `passphrase` - The passphrase to decrypt the key with
97    ///
98    /// # Errors
99    /// - `StoreError::KeyNotFound` if no key exists with this name
100    /// - `StoreError::DecryptionFailed` if the passphrase is wrong
101    /// - `StoreError::IoError` if file operations fail
102    /// - `StoreError::InvalidFormat` if the name or file format is invalid
103    fn load(&self, name: &str, passphrase: &str) -> Result<SecretKey, StoreError>;
104
105    /// List all stored key names.
106    ///
107    /// # Returns
108    /// A vector of key names (without the `.enc` extension), sorted alphabetically.
109    ///
110    /// # Errors
111    /// - `StoreError::IoError` if directory operations fail
112    fn list(&self) -> Result<Vec<String>, StoreError>;
113
114    /// Delete a key by name.
115    ///
116    /// # Arguments
117    /// * `name` - The identifier of the key to delete
118    ///
119    /// # Errors
120    /// - `StoreError::KeyNotFound` if no key exists with this name
121    /// - `StoreError::IoError` if file operations fail
122    /// - `StoreError::InvalidFormat` if the name is invalid
123    fn delete(&self, name: &str) -> Result<(), StoreError>;
124
125    /// Check if a key exists.
126    ///
127    /// # Arguments
128    /// * `name` - The identifier of the key to check
129    ///
130    /// # Returns
131    /// `true` if a key with this name exists, `false` otherwise.
132    /// Returns `false` if the name is invalid.
133    fn exists(&self, name: &str) -> bool;
134}
135
136// ============================================================================
137// FileKeyStore Implementation
138// ============================================================================
139
140/// File-based key storage in `~/.txgate/keys/`.
141///
142/// This implementation stores encrypted keys as individual files with the `.enc`
143/// extension. Each key is encrypted using the encryption module's
144/// ChaCha20-Poly1305 AEAD encryption with Argon2id key derivation.
145///
146/// # Security
147///
148/// - Directory permissions are set to 0700 (owner only)
149/// - File permissions are set to 0600 (owner read/write only)
150/// - Atomic writes prevent corruption on crash
151/// - Key names are validated to prevent path traversal attacks
152///
153/// # Example
154///
155/// ```no_run
156/// use txgate_crypto::store::{KeyStore, FileKeyStore};
157///
158/// // Use the default path (~/.txgate/keys/)
159/// let store = FileKeyStore::new().expect("failed to create key store");
160///
161/// // Or use a custom path
162/// use std::path::PathBuf;
163/// let custom_store = FileKeyStore::with_path(PathBuf::from("/custom/path"))
164///     .expect("failed to create key store");
165/// ```
166pub struct FileKeyStore {
167    /// Directory where keys are stored.
168    keys_dir: PathBuf,
169}
170
171impl FileKeyStore {
172    /// Create a new `FileKeyStore` with the default path (`~/.txgate/keys/`).
173    ///
174    /// # Errors
175    /// - `StoreError::IoError` if the home directory cannot be determined
176    /// - `StoreError::IoError` if directory creation fails
177    /// - `StoreError::PermissionDenied` if permissions cannot be set
178    ///
179    /// # Example
180    ///
181    /// ```no_run
182    /// use txgate_crypto::store::FileKeyStore;
183    ///
184    /// let store = FileKeyStore::new().expect("failed to create key store");
185    /// ```
186    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    /// Create a `FileKeyStore` with a custom path.
201    ///
202    /// This is useful for testing or when you want to store keys in a
203    /// non-standard location.
204    ///
205    /// # Arguments
206    /// * `keys_dir` - The directory to store keys in
207    ///
208    /// # Errors
209    /// - `StoreError::IoError` if directory creation fails
210    /// - `StoreError::PermissionDenied` if permissions cannot be set
211    ///
212    /// # Example
213    ///
214    /// ```no_run
215    /// use txgate_crypto::store::FileKeyStore;
216    /// use std::path::PathBuf;
217    ///
218    /// let store = FileKeyStore::with_path(PathBuf::from("/tmp/test-keys"))
219    ///     .expect("failed to create key store");
220    /// ```
221    pub fn with_path(keys_dir: PathBuf) -> Result<Self, StoreError> {
222        // Create directory if it doesn't exist
223        if !keys_dir.exists() {
224            fs::create_dir_all(&keys_dir)?;
225        }
226
227        // Set directory permissions to 0700 (owner only)
228        #[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    /// Get the path to a key file.
239    fn key_path(&self, name: &str) -> PathBuf {
240        self.keys_dir.join(format!("{name}.enc"))
241    }
242
243    /// Validate a key name.
244    ///
245    /// Valid names:
246    /// - Are not empty
247    /// - Contain only ASCII alphanumeric characters, hyphens, and underscores
248    /// - Do not start with a dot (reserved for temp files)
249    ///
250    /// This validation prevents:
251    /// - Path traversal attacks (e.g., "../../../etc/passwd")
252    /// - Hidden file creation
253    /// - Shell injection through filenames
254    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    /// Get the directory path where keys are stored.
274    ///
275    /// This is useful for debugging or informational purposes.
276    #[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        // Encrypt the key
294        let encrypted = encrypt_key(key, passphrase)?;
295        let bytes = encrypted.to_bytes();
296
297        // Atomic write: write to temp file, then rename
298        let temp_path = self.keys_dir.join(format!(".{name}.tmp"));
299
300        // Write to temp file
301        {
302            let mut file = File::create(&temp_path)?;
303            file.write_all(&bytes)?;
304            file.sync_all()?;
305        }
306
307        // Set file permissions to 0600 (owner read/write only) before rename
308        #[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        // Atomic rename
316        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        // Read encrypted data
332        let mut file = File::open(&path)?;
333        let mut bytes = Vec::new();
334        file.read_to_end(&mut bytes)?;
335
336        // Decrypt
337        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            // Only include .enc files
349            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                        // Skip temp files (starting with '.')
353                        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// ============================================================================
388// Tests
389// ============================================================================
390
391#[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    /// Create a test key store in a temporary directory.
404    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    // ------------------------------------------------------------------------
412    // Store/Load Round-Trip Tests
413    // ------------------------------------------------------------------------
414
415    #[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    // ------------------------------------------------------------------------
491    // List Tests
492    // ------------------------------------------------------------------------
493
494    #[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 keys in non-alphabetical order
508        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        // Create a temp file manually (simulating interrupted write)
532        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        // Create non-.enc files
549        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    // ------------------------------------------------------------------------
557    // Delete Tests
558    // ------------------------------------------------------------------------
559
560    #[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    // ------------------------------------------------------------------------
606    // Exists Tests
607    // ------------------------------------------------------------------------
608
609    #[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    // ------------------------------------------------------------------------
634    // Error Handling Tests
635    // ------------------------------------------------------------------------
636
637    #[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    // ------------------------------------------------------------------------
671    // Name Validation Tests
672    // ------------------------------------------------------------------------
673
674    #[test]
675    fn test_valid_names() {
676        let (store, _temp) = create_test_store();
677        let passphrase = "test";
678
679        // All these names should be valid
680        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    // ------------------------------------------------------------------------
757    // File Permission Tests (Unix only)
758    // ------------------------------------------------------------------------
759
760    #[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        // Check that only owner has access (0700)
769        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        // Check that only owner has read/write (0600)
790        assert_eq!(mode & 0o777, 0o600, "File should have 0600 permissions");
791    }
792
793    // ------------------------------------------------------------------------
794    // Atomic Write Tests
795    // ------------------------------------------------------------------------
796
797    #[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        // Check that no temp files exist
806        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    // ------------------------------------------------------------------------
817    // Thread Safety Tests
818    // ------------------------------------------------------------------------
819
820    #[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    // ------------------------------------------------------------------------
833    // Integration Tests
834    // ------------------------------------------------------------------------
835
836    #[test]
837    fn test_full_workflow() {
838        let (store, _temp) = create_test_store();
839
840        // Initially empty
841        assert!(store.list().expect("list should succeed").is_empty());
842
843        // Store some keys
844        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        // List shows both
855        let keys = store.list().expect("list should succeed");
856        assert_eq!(keys, vec!["wallet-1", "wallet-2"]);
857
858        // Exists works
859        assert!(store.exists("wallet-1"));
860        assert!(store.exists("wallet-2"));
861        assert!(!store.exists("wallet-3"));
862
863        // Load works
864        let loaded1 = store
865            .load("wallet-1", "pass1")
866            .expect("load should succeed");
867        assert_eq!(key1.as_bytes(), loaded1.as_bytes());
868
869        // Delete works
870        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    // ------------------------------------------------------------------------
878    // Custom Path Tests
879    // ------------------------------------------------------------------------
880
881    #[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    // ------------------------------------------------------------------------
901    // Additional Coverage Tests
902    // ------------------------------------------------------------------------
903
904    #[test]
905    fn test_validate_name_all_edge_cases() {
906        // Test the validation logic directly
907
908        // Empty name
909        assert!(FileKeyStore::validate_name("").is_err());
910
911        // Starts with dot
912        assert!(FileKeyStore::validate_name(".test").is_err());
913
914        // Contains invalid characters
915        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        // Valid names
922        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        // This test ensures list() handles edge cases gracefully
963        let (store, _temp) = create_test_store();
964
965        // Store a normal key
966        store
967            .store("normal", &SecretKey::generate(), "pass")
968            .expect("store should succeed");
969
970        // List should work even if there are unusual files
971        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        // On non-Unix systems, permission setting is skipped
989        // This test just ensures the code doesn't panic
990        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    // ------------------------------------------------------------------------
1002    // I/O Error Tests
1003    // ------------------------------------------------------------------------
1004
1005    #[test]
1006    #[cfg(unix)]
1007    fn test_store_fails_with_readonly_directory() {
1008        use std::os::unix::fs::PermissionsExt;
1009
1010        // Arrange: Create a temporary directory and make it read-only
1011        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        // Make directory read-only (no write permission)
1016        let mut perms = fs::metadata(&store_path)
1017            .expect("failed to get metadata")
1018            .permissions();
1019        perms.set_mode(0o500); // r-x------
1020        fs::set_permissions(&store_path, perms).expect("failed to set permissions");
1021
1022        // Act: Try to store a key in a readonly directory
1023        let result = store.store("test-key", &SecretKey::generate(), "passphrase");
1024
1025        // Assert: Should fail (either IoError or KeyExists, depending on OS behavior)
1026        assert!(result.is_err());
1027
1028        // Cleanup: Restore permissions so temp_dir can be deleted
1029        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        // Arrange: Create a store with a path that doesn't exist
1039        let temp_dir = TempDir::new().expect("failed to create temp dir");
1040        let nonexistent_path = temp_dir.path().join("does_not_exist");
1041
1042        // Don't create the directory - just create a store pointing to it
1043        let store = FileKeyStore {
1044            keys_dir: nonexistent_path,
1045        };
1046
1047        // Act: Try to load a key from nonexistent directory
1048        let result = store.load("any-key", "passphrase");
1049
1050        // Assert: Should fail with KeyNotFound (because validation passes but file doesn't exist)
1051        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        // Arrange: Create a key file with invalid encrypted data
1061        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        // Write corrupted data directly to a file
1066        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        // Act: Try to load the corrupted key
1071        let result = store.load("corrupted", "passphrase");
1072
1073        // Assert: Should fail with InvalidFormat or DecryptionFailed
1074        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        // Arrange: Create a store directory
1091        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 a key first
1096        store
1097            .store("test", &SecretKey::generate(), "pass")
1098            .expect("store should succeed");
1099
1100        // Remove read permission from directory
1101        let mut perms = fs::metadata(&store_path)
1102            .expect("failed to get metadata")
1103            .permissions();
1104        perms.set_mode(0o300); // -wx------ (no read)
1105        fs::set_permissions(&store_path, perms).expect("failed to set permissions");
1106
1107        // Act: Try to list keys without read permission
1108        let result = store.list();
1109
1110        // Assert: Should fail (behavior may vary on different Unix systems)
1111        assert!(result.is_err());
1112
1113        // Cleanup: Restore permissions so temp_dir can be deleted
1114        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        // Arrange: Create a store and add a key
1127        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        // Make directory read-only (no write permission)
1136        let mut perms = fs::metadata(&store_path)
1137            .expect("failed to get metadata")
1138            .permissions();
1139        perms.set_mode(0o500); // r-x------
1140        fs::set_permissions(&store_path, perms).expect("failed to set permissions");
1141
1142        // Act: Try to delete a key from readonly directory
1143        let result = store.delete("test");
1144
1145        // Assert: Should fail (behavior may vary on different Unix systems)
1146        assert!(result.is_err());
1147
1148        // Cleanup: Restore permissions
1149        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        // Arrange: Create a store with multiple keys
1159        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        // Store multiple keys
1165        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        // Act: Spawn multiple threads that read different keys concurrently
1173        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        // Assert: All threads should complete successfully
1185        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        // Arrange: Create a store
1193        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        // Act: Spawn multiple threads that write different keys concurrently
1199        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        // Assert: All threads should complete successfully
1212        for handle in handles {
1213            handle.join().expect("thread should not panic");
1214        }
1215
1216        // Verify all keys were stored
1217        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        // Arrange: Create a store with some initial keys
1224        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        // Store initial keys
1230        for i in 0..5 {
1231            store
1232                .store(&format!("initial-{i}"), &SecretKey::generate(), "pass")
1233                .expect("store should succeed");
1234        }
1235
1236        // Act: Spawn mix of reader and writer threads
1237        let mut handles = vec![];
1238
1239        // Readers
1240        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        // Writers
1251        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        // Assert: All threads should complete successfully
1263        for handle in handles {
1264            handle.join().expect("thread should not panic");
1265        }
1266
1267        // Verify final state
1268        let keys = store.list().expect("list should succeed");
1269        assert!(keys.len() >= 10); // At least initial + new keys
1270    }
1271
1272    #[test]
1273    fn test_load_with_truncated_file() {
1274        // Arrange: Create a store and a file with incomplete encrypted data
1275        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        // Write truncated encrypted data (too short to be valid)
1280        let truncated_path = temp_dir.path().join("truncated.enc");
1281        fs::write(&truncated_path, b"short").expect("failed to write truncated file");
1282
1283        // Act: Try to load the truncated key
1284        let result = store.load("truncated", "passphrase");
1285
1286        // Assert: Should fail with InvalidFormat
1287        assert!(result.is_err());
1288        assert!(matches!(result.unwrap_err(), StoreError::InvalidFormat));
1289    }
1290}