1use std::path::PathBuf;
39use thiserror::Error;
40use zeroize::Zeroize;
41
42#[derive(Debug, Error)]
48pub enum StorageError {
49 #[error("HostKey not found")]
51 NotFound,
52
53 #[error("Storage backend not available: {0}")]
55 BackendUnavailable(String),
56
57 #[error("ANTQ_HOSTKEY_PASSWORD environment variable not set")]
59 PasswordRequired,
60
61 #[error("Cryptographic operation failed: {0}")]
63 CryptoError(String),
64
65 #[error("I/O error: {0}")]
67 IoError(#[from] std::io::Error),
68
69 #[error("Invalid data format: {0}")]
71 InvalidFormat(String),
72
73 #[error("Keychain error: {0}")]
75 KeychainError(String),
76
77 #[error("Permission denied: {0}")]
79 PermissionDenied(String),
80}
81
82pub type StorageResult<T> = Result<T, StorageError>;
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum StorageSecurityLevel {
92 Secure,
94 Encrypted,
96 Insecure,
98}
99
100impl StorageSecurityLevel {
101 pub fn warning_message(&self) -> Option<&'static str> {
103 match self {
104 Self::Secure | Self::Encrypted => None,
105 Self::Insecure => Some(
106 "⚠️ HostKey stored WITHOUT ENCRYPTION!\n\
107 Anyone with file access can read and impersonate this node.\n\
108 To secure: set ANTQ_HOSTKEY_PASSWORD environment variable.",
109 ),
110 }
111 }
112
113 pub fn is_secure(&self) -> bool {
115 matches!(self, Self::Secure | Self::Encrypted)
116 }
117}
118
119pub trait HostKeyStorage: Send + Sync {
130 fn store(&self, hostkey: &[u8; 32]) -> StorageResult<()>;
138
139 fn load(&self) -> StorageResult<[u8; 32]>;
147
148 fn delete(&self) -> StorageResult<()>;
153
154 fn exists(&self) -> bool;
156
157 fn backend_name(&self) -> &'static str;
159
160 fn security_level(&self) -> StorageSecurityLevel;
162}
163
164const FILE_FORMAT_VERSION: u8 = 1;
170
171const SALT_SIZE: usize = 32;
173
174pub struct EncryptedFileStorage {
184 path: PathBuf,
185}
186
187impl EncryptedFileStorage {
188 pub fn new() -> StorageResult<Self> {
190 let path = Self::default_path()?;
191 Ok(Self { path })
192 }
193
194 pub fn with_path(path: PathBuf) -> Self {
196 Self { path }
197 }
198
199 fn default_path() -> StorageResult<PathBuf> {
204 let config_dir = dirs::config_dir().ok_or_else(|| {
205 StorageError::IoError(std::io::Error::new(
206 std::io::ErrorKind::NotFound,
207 "Could not determine config directory",
208 ))
209 })?;
210
211 let path = config_dir.join("ant-quic").join("hostkey.enc");
212 Ok(path)
213 }
214
215 fn get_password() -> StorageResult<String> {
217 std::env::var("ANTQ_HOSTKEY_PASSWORD").map_err(|_| StorageError::PasswordRequired)
218 }
219
220 fn derive_key_from_password(password: &str, salt: &[u8]) -> StorageResult<[u8; 32]> {
222 use aws_lc_rs::hkdf;
223
224 let hkdf_salt = hkdf::Salt::new(hkdf::HKDF_SHA256, salt);
225 let prk = hkdf_salt.extract(password.as_bytes());
226
227 let mut key = [0u8; 32];
228 let okm = prk
229 .expand(&[b"antq:hostkey-file:v1"], hkdf::HKDF_SHA256)
230 .map_err(|e| StorageError::CryptoError(format!("HKDF expand failed: {e}")))?;
231
232 okm.fill(&mut key)
233 .map_err(|e| StorageError::CryptoError(format!("HKDF fill failed: {e}")))?;
234
235 Ok(key)
236 }
237
238 fn encrypt(key: &[u8; 32], plaintext: &[u8; 32]) -> StorageResult<Vec<u8>> {
240 use aws_lc_rs::aead::{
241 self, Aad, BoundKey, CHACHA20_POLY1305, Nonce, NonceSequence, UnboundKey,
242 };
243
244 let mut nonce_bytes = [0u8; 12];
246 aws_lc_rs::rand::fill(&mut nonce_bytes)
247 .map_err(|e| StorageError::CryptoError(format!("Failed to generate nonce: {e}")))?;
248
249 let unbound_key = UnboundKey::new(&CHACHA20_POLY1305, key)
251 .map_err(|e| StorageError::CryptoError(format!("Failed to create key: {e}")))?;
252
253 struct SingleNonce(Option<[u8; 12]>);
254 impl NonceSequence for SingleNonce {
255 fn advance(&mut self) -> Result<Nonce, aws_lc_rs::error::Unspecified> {
256 self.0
257 .take()
258 .map(Nonce::assume_unique_for_key)
259 .ok_or(aws_lc_rs::error::Unspecified)
260 }
261 }
262
263 let mut sealing_key = aead::SealingKey::new(unbound_key, SingleNonce(Some(nonce_bytes)));
264
265 let mut in_out = plaintext.to_vec();
267 sealing_key
268 .seal_in_place_append_tag(Aad::empty(), &mut in_out)
269 .map_err(|e| StorageError::CryptoError(format!("Encryption failed: {e}")))?;
270
271 let mut result = Vec::with_capacity(12 + in_out.len());
273 result.extend_from_slice(&nonce_bytes);
274 result.extend_from_slice(&in_out);
275 Ok(result)
276 }
277
278 fn decrypt(key: &[u8; 32], ciphertext: &[u8]) -> StorageResult<[u8; 32]> {
280 use aws_lc_rs::aead::{
281 self, Aad, BoundKey, CHACHA20_POLY1305, Nonce, NonceSequence, UnboundKey,
282 };
283
284 if ciphertext.len() < 12 + 16 {
285 return Err(StorageError::InvalidFormat(
286 "Ciphertext too short".to_string(),
287 ));
288 }
289
290 let nonce_bytes: [u8; 12] = ciphertext[..12]
291 .try_into()
292 .map_err(|_| StorageError::InvalidFormat("Invalid nonce".to_string()))?;
293
294 let unbound_key = UnboundKey::new(&CHACHA20_POLY1305, key)
296 .map_err(|e| StorageError::CryptoError(format!("Failed to create key: {e}")))?;
297
298 struct SingleNonce(Option<[u8; 12]>);
299 impl NonceSequence for SingleNonce {
300 fn advance(&mut self) -> Result<Nonce, aws_lc_rs::error::Unspecified> {
301 self.0
302 .take()
303 .map(Nonce::assume_unique_for_key)
304 .ok_or(aws_lc_rs::error::Unspecified)
305 }
306 }
307
308 let mut opening_key = aead::OpeningKey::new(unbound_key, SingleNonce(Some(nonce_bytes)));
309
310 let mut in_out = ciphertext[12..].to_vec();
312 let plaintext = opening_key
313 .open_in_place(Aad::empty(), &mut in_out)
314 .map_err(|_| {
315 StorageError::CryptoError(
316 "Decryption failed - wrong password or corrupted data".to_string(),
317 )
318 })?;
319
320 if plaintext.len() != 32 {
321 return Err(StorageError::InvalidFormat(format!(
322 "Expected 32-byte HostKey, got {} bytes",
323 plaintext.len()
324 )));
325 }
326
327 let mut result = [0u8; 32];
328 result.copy_from_slice(plaintext);
329 Ok(result)
330 }
331}
332
333impl HostKeyStorage for EncryptedFileStorage {
334 fn store(&self, hostkey: &[u8; 32]) -> StorageResult<()> {
335 let password = Self::get_password()?;
336
337 let mut salt = [0u8; SALT_SIZE];
339 aws_lc_rs::rand::fill(&mut salt)
340 .map_err(|e| StorageError::CryptoError(format!("Failed to generate salt: {e}")))?;
341
342 let mut key = Self::derive_key_from_password(&password, &salt)?;
344
345 let ciphertext = Self::encrypt(&key, hostkey)?;
347
348 key.zeroize();
350
351 if let Some(parent) = self.path.parent() {
353 std::fs::create_dir_all(parent)?;
354 }
355
356 let mut file_data = Vec::with_capacity(1 + SALT_SIZE + ciphertext.len());
358 file_data.push(FILE_FORMAT_VERSION);
359 file_data.extend_from_slice(&salt);
360 file_data.extend_from_slice(&ciphertext);
361
362 let temp_path = self.path.with_extension("tmp");
364 std::fs::write(&temp_path, &file_data)?;
365 std::fs::rename(&temp_path, &self.path)?;
366
367 #[cfg(unix)]
369 {
370 use std::os::unix::fs::PermissionsExt;
371 let permissions = std::fs::Permissions::from_mode(0o600);
372 std::fs::set_permissions(&self.path, permissions)?;
373 }
374
375 Ok(())
376 }
377
378 fn load(&self) -> StorageResult<[u8; 32]> {
379 if !self.path.exists() {
380 return Err(StorageError::NotFound);
381 }
382
383 let password = Self::get_password()?;
384 let file_data = std::fs::read(&self.path)?;
385
386 if file_data.is_empty() {
388 return Err(StorageError::InvalidFormat("Empty file".to_string()));
389 }
390
391 let version = file_data[0];
392 if version != FILE_FORMAT_VERSION {
393 return Err(StorageError::InvalidFormat(format!(
394 "Unsupported file format version: {version}"
395 )));
396 }
397
398 if file_data.len() < 1 + SALT_SIZE + 12 + 16 {
399 return Err(StorageError::InvalidFormat("File too short".to_string()));
400 }
401
402 let salt = &file_data[1..1 + SALT_SIZE];
403 let ciphertext = &file_data[1 + SALT_SIZE..];
404
405 let mut key = Self::derive_key_from_password(&password, salt)?;
407 let result = Self::decrypt(&key, ciphertext);
408
409 key.zeroize();
411
412 result
413 }
414
415 fn delete(&self) -> StorageResult<()> {
416 if self.path.exists() {
417 if let Ok(metadata) = std::fs::metadata(&self.path) {
419 let zeros = vec![0u8; metadata.len() as usize];
420 let _ = std::fs::write(&self.path, &zeros);
421 }
422 std::fs::remove_file(&self.path)?;
423 }
424 Ok(())
425 }
426
427 fn exists(&self) -> bool {
428 self.path.exists()
429 }
430
431 fn backend_name(&self) -> &'static str {
432 "EncryptedFile"
433 }
434
435 fn security_level(&self) -> StorageSecurityLevel {
436 StorageSecurityLevel::Encrypted
437 }
438}
439
440pub struct KeyringStorage {
451 service: &'static str,
452 username: &'static str,
453}
454
455impl KeyringStorage {
456 const SERVICE: &'static str = "ant-quic";
457 const USERNAME: &'static str = "hostkey";
458
459 pub fn new() -> StorageResult<Self> {
461 let _ = keyring::Entry::new(Self::SERVICE, Self::USERNAME)
463 .map_err(|e| StorageError::KeychainError(format!("Keyring unavailable: {e}")))?;
464 Ok(Self {
465 service: Self::SERVICE,
466 username: Self::USERNAME,
467 })
468 }
469
470 pub fn is_available() -> bool {
472 keyring::Entry::new(Self::SERVICE, Self::USERNAME).is_ok()
473 }
474
475 fn entry(&self) -> StorageResult<keyring::Entry> {
477 keyring::Entry::new(self.service, self.username)
478 .map_err(|e| StorageError::KeychainError(e.to_string()))
479 }
480}
481
482impl HostKeyStorage for KeyringStorage {
483 fn store(&self, hostkey: &[u8; 32]) -> StorageResult<()> {
484 let entry = self.entry()?;
485 let hex = hex::encode(hostkey);
487 entry
488 .set_password(&hex)
489 .map_err(|e| StorageError::KeychainError(e.to_string()))
490 }
491
492 fn load(&self) -> StorageResult<[u8; 32]> {
493 let entry = self.entry()?;
494 let hex = entry.get_password().map_err(|e| match e {
495 keyring::Error::NoEntry => StorageError::NotFound,
496 _ => StorageError::KeychainError(e.to_string()),
497 })?;
498
499 let bytes = hex::decode(&hex).map_err(|e| StorageError::InvalidFormat(e.to_string()))?;
500
501 if bytes.len() != 32 {
502 return Err(StorageError::InvalidFormat(format!(
503 "Expected 32 bytes, got {}",
504 bytes.len()
505 )));
506 }
507
508 let mut result = [0u8; 32];
509 result.copy_from_slice(&bytes);
510 Ok(result)
511 }
512
513 fn delete(&self) -> StorageResult<()> {
514 let entry = self.entry()?;
515 match entry.delete_credential() {
516 Ok(()) => Ok(()),
517 Err(keyring::Error::NoEntry) => Ok(()), Err(e) => Err(StorageError::KeychainError(e.to_string())),
519 }
520 }
521
522 fn exists(&self) -> bool {
523 self.entry()
524 .map(|e| e.get_password().is_ok())
525 .unwrap_or(false)
526 }
527
528 fn backend_name(&self) -> &'static str {
529 #[cfg(target_os = "macos")]
530 {
531 "macOS-Keychain"
532 }
533 #[cfg(target_os = "linux")]
534 {
535 "Linux-SecretService"
536 }
537 #[cfg(target_os = "windows")]
538 {
539 "Windows-CredentialManager"
540 }
541 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
542 {
543 "Keyring"
544 }
545 }
546
547 fn security_level(&self) -> StorageSecurityLevel {
548 StorageSecurityLevel::Secure
549 }
550}
551
552pub struct PlainFileStorage {
567 path: PathBuf,
568}
569
570impl PlainFileStorage {
571 pub fn new() -> StorageResult<Self> {
573 let path = Self::default_path()?;
574 Ok(Self { path })
575 }
576
577 pub fn with_path(path: PathBuf) -> Self {
579 Self { path }
580 }
581
582 fn default_path() -> StorageResult<PathBuf> {
584 let config_dir = dirs::config_dir().ok_or_else(|| {
585 StorageError::IoError(std::io::Error::new(
586 std::io::ErrorKind::NotFound,
587 "Could not determine config directory",
588 ))
589 })?;
590 Ok(config_dir.join("ant-quic").join("hostkey.key"))
591 }
592}
593
594impl HostKeyStorage for PlainFileStorage {
595 fn store(&self, hostkey: &[u8; 32]) -> StorageResult<()> {
596 if let Some(parent) = self.path.parent() {
598 std::fs::create_dir_all(parent)?;
599 }
600
601 let temp_path = self.path.with_extension("tmp");
603 std::fs::write(&temp_path, hostkey)?;
604 std::fs::rename(&temp_path, &self.path)?;
605
606 #[cfg(unix)]
608 {
609 use std::os::unix::fs::PermissionsExt;
610 let permissions = std::fs::Permissions::from_mode(0o600);
611 std::fs::set_permissions(&self.path, permissions)?;
612 }
613
614 Ok(())
615 }
616
617 fn load(&self) -> StorageResult<[u8; 32]> {
618 if !self.path.exists() {
619 return Err(StorageError::NotFound);
620 }
621
622 let data = std::fs::read(&self.path)?;
623 if data.len() != 32 {
624 return Err(StorageError::InvalidFormat(format!(
625 "Expected 32 bytes, got {}",
626 data.len()
627 )));
628 }
629
630 let mut result = [0u8; 32];
631 result.copy_from_slice(&data);
632 Ok(result)
633 }
634
635 fn delete(&self) -> StorageResult<()> {
636 if self.path.exists() {
637 let _ = std::fs::write(&self.path, [0u8; 32]);
639 std::fs::remove_file(&self.path)?;
640 }
641 Ok(())
642 }
643
644 fn exists(&self) -> bool {
645 self.path.exists()
646 }
647
648 fn backend_name(&self) -> &'static str {
649 "PlainFile-INSECURE"
650 }
651
652 fn security_level(&self) -> StorageSecurityLevel {
653 StorageSecurityLevel::Insecure
654 }
655}
656
657pub struct StorageSelection {
663 pub storage: Box<dyn HostKeyStorage>,
665 pub security_level: StorageSecurityLevel,
667}
668
669pub fn auto_storage() -> StorageResult<StorageSelection> {
680 if KeyringStorage::is_available() {
682 if let Ok(storage) = KeyringStorage::new() {
683 let security_level = storage.security_level();
684 return Ok(StorageSelection {
685 storage: Box::new(storage),
686 security_level,
687 });
688 }
689 }
690
691 if std::env::var("ANTQ_HOSTKEY_PASSWORD").is_ok() {
693 let storage = EncryptedFileStorage::new()?;
694 return Ok(StorageSelection {
695 storage: Box::new(storage),
696 security_level: StorageSecurityLevel::Encrypted,
697 });
698 }
699
700 let storage = PlainFileStorage::new()?;
702 Ok(StorageSelection {
703 storage: Box::new(storage),
704 security_level: StorageSecurityLevel::Insecure,
705 })
706}
707
708#[deprecated(
710 since = "0.15.0",
711 note = "Use auto_storage() which returns StorageSelection"
712)]
713pub fn auto_storage_legacy() -> StorageResult<Box<dyn HostKeyStorage>> {
714 Ok(auto_storage()?.storage)
715}
716
717pub fn encrypted_file_storage() -> StorageResult<EncryptedFileStorage> {
719 EncryptedFileStorage::new()
720}
721
722#[cfg(test)]
727mod tests {
728 use super::*;
729 use std::sync::Mutex;
730 use tempfile::TempDir;
731
732 static ENV_VAR_MUTEX: Mutex<()> = Mutex::new(());
734
735 fn with_password<T, F: FnOnce() -> T>(password: Option<&str>, f: F) -> T {
737 let _guard = ENV_VAR_MUTEX.lock().expect("ENV_VAR_MUTEX poisoned");
738 unsafe {
740 if let Some(pwd) = password {
741 std::env::set_var("ANTQ_HOSTKEY_PASSWORD", pwd);
742 } else {
743 std::env::remove_var("ANTQ_HOSTKEY_PASSWORD");
744 }
745 }
746 let result = f();
747 unsafe {
749 std::env::remove_var("ANTQ_HOSTKEY_PASSWORD");
750 }
751 result
752 }
753
754 #[test]
755 fn test_encrypted_file_storage_roundtrip() {
756 with_password(Some("test-password-12345"), || {
757 let temp_dir = TempDir::new().expect("Failed to create temp dir");
758 let path = temp_dir.path().join("hostkey.enc");
759 let storage = EncryptedFileStorage::with_path(path);
760
761 let hostkey = [0xAB; 32];
762
763 storage.store(&hostkey).expect("Failed to store");
765
766 let loaded = storage.load().expect("Failed to load");
768 assert_eq!(loaded, hostkey);
769 });
770 }
771
772 #[test]
773 fn test_encrypted_file_storage_wrong_password() {
774 let temp_dir = TempDir::new().expect("Failed to create temp dir");
776 let path = temp_dir.path().join("hostkey.enc");
777
778 with_password(Some("correct-password"), || {
779 let storage = EncryptedFileStorage::with_path(path.clone());
780 let hostkey = [0xAB; 32];
781 storage.store(&hostkey).expect("Failed to store");
782 });
783
784 with_password(Some("wrong-password"), || {
786 let storage = EncryptedFileStorage::with_path(path.clone());
787 let result = storage.load();
788 assert!(result.is_err(), "Should fail with wrong password");
789 });
790 }
791
792 #[test]
793 fn test_encrypted_file_storage_missing_password() {
794 with_password(None, || {
795 let temp_dir = TempDir::new().expect("Failed to create temp dir");
796 let path = temp_dir.path().join("hostkey.enc");
797 let storage = EncryptedFileStorage::with_path(path);
798
799 let hostkey = [0xCD; 32];
800 let result = storage.store(&hostkey);
801
802 assert!(matches!(result, Err(StorageError::PasswordRequired)));
803 });
804 }
805
806 #[test]
807 fn test_encrypted_file_storage_not_found() {
808 with_password(Some("test-password"), || {
809 let temp_dir = TempDir::new().expect("Failed to create temp dir");
810 let path = temp_dir.path().join("nonexistent.enc");
811 let storage = EncryptedFileStorage::with_path(path);
812
813 let result = storage.load();
814 assert!(matches!(result, Err(StorageError::NotFound)));
815 });
816 }
817
818 #[test]
819 fn test_encrypted_file_storage_delete() {
820 with_password(Some("test-password"), || {
821 let temp_dir = TempDir::new().expect("Failed to create temp dir");
822 let path = temp_dir.path().join("hostkey.enc");
823 let storage = EncryptedFileStorage::with_path(path.clone());
824
825 let hostkey = [0xEF; 32];
826 storage.store(&hostkey).expect("Failed to store");
827 assert!(path.exists());
828
829 storage.delete().expect("Failed to delete");
830 assert!(!path.exists());
831 });
832 }
833
834 #[test]
835 fn test_key_derivation_deterministic() {
836 let password = "test-password";
837 let salt = [1u8; SALT_SIZE];
838
839 let key1 = EncryptedFileStorage::derive_key_from_password(password, &salt)
840 .expect("Key derivation failed");
841 let key2 = EncryptedFileStorage::derive_key_from_password(password, &salt)
842 .expect("Key derivation failed");
843
844 assert_eq!(key1, key2);
845 }
846
847 #[test]
848 fn test_different_salts_different_keys() {
849 let password = "test-password";
850 let salt1 = [1u8; SALT_SIZE];
851 let salt2 = [2u8; SALT_SIZE];
852
853 let key1 = EncryptedFileStorage::derive_key_from_password(password, &salt1)
854 .expect("Key derivation failed");
855 let key2 = EncryptedFileStorage::derive_key_from_password(password, &salt2)
856 .expect("Key derivation failed");
857
858 assert_ne!(key1, key2);
859 }
860
861 #[test]
862 fn test_encryption_roundtrip() {
863 let key = [0x42; 32];
864 let plaintext = [0xAB; 32];
865
866 let ciphertext =
867 EncryptedFileStorage::encrypt(&key, &plaintext).expect("Encryption failed");
868
869 assert!(ciphertext.len() > 32);
871
872 let decrypted =
873 EncryptedFileStorage::decrypt(&key, &ciphertext).expect("Decryption failed");
874
875 assert_eq!(decrypted, plaintext);
876 }
877
878 #[test]
879 fn test_wrong_key_fails_decryption() {
880 let key1 = [0x42; 32];
881 let key2 = [0x43; 32];
882 let plaintext = [0xAB; 32];
883
884 let ciphertext =
885 EncryptedFileStorage::encrypt(&key1, &plaintext).expect("Encryption failed");
886
887 let result = EncryptedFileStorage::decrypt(&key2, &ciphertext);
888 assert!(result.is_err());
889 }
890
891 #[test]
896 fn test_plain_file_storage_roundtrip() {
897 let temp_dir = TempDir::new().expect("Failed to create temp dir");
898 let path = temp_dir.path().join("hostkey.key");
899 let storage = PlainFileStorage::with_path(path);
900
901 let hostkey = [0xAB; 32];
902
903 storage.store(&hostkey).expect("Failed to store");
905
906 let loaded = storage.load().expect("Failed to load");
908 assert_eq!(loaded, hostkey);
909 }
910
911 #[test]
912 fn test_plain_file_storage_not_found() {
913 let temp_dir = TempDir::new().expect("Failed to create temp dir");
914 let path = temp_dir.path().join("nonexistent.key");
915 let storage = PlainFileStorage::with_path(path);
916
917 let result = storage.load();
918 assert!(matches!(result, Err(StorageError::NotFound)));
919 }
920
921 #[test]
922 fn test_plain_file_storage_delete() {
923 let temp_dir = TempDir::new().expect("Failed to create temp dir");
924 let path = temp_dir.path().join("hostkey.key");
925 let storage = PlainFileStorage::with_path(path.clone());
926
927 let hostkey = [0xEF; 32];
928 storage.store(&hostkey).expect("Failed to store");
929 assert!(path.exists());
930
931 storage.delete().expect("Failed to delete");
932 assert!(!path.exists());
933 }
934
935 #[test]
936 fn test_plain_file_storage_exists() {
937 let temp_dir = TempDir::new().expect("Failed to create temp dir");
938 let path = temp_dir.path().join("hostkey.key");
939 let storage = PlainFileStorage::with_path(path);
940
941 assert!(!storage.exists());
942
943 let hostkey = [0xAB; 32];
944 storage.store(&hostkey).expect("Failed to store");
945 assert!(storage.exists());
946 }
947
948 #[test]
949 fn test_plain_file_storage_security_level() {
950 let temp_dir = TempDir::new().expect("Failed to create temp dir");
951 let path = temp_dir.path().join("hostkey.key");
952 let storage = PlainFileStorage::with_path(path);
953
954 assert_eq!(storage.security_level(), StorageSecurityLevel::Insecure);
955 assert!(storage.security_level().warning_message().is_some());
956 }
957
958 #[cfg(unix)]
959 #[test]
960 fn test_plain_file_storage_permissions() {
961 use std::os::unix::fs::PermissionsExt;
962
963 let temp_dir = TempDir::new().expect("Failed to create temp dir");
964 let path = temp_dir.path().join("hostkey.key");
965 let storage = PlainFileStorage::with_path(path.clone());
966
967 let hostkey = [0xAB; 32];
968 storage.store(&hostkey).expect("Failed to store");
969
970 let metadata = std::fs::metadata(&path).expect("Failed to get metadata");
971 let permissions = metadata.permissions();
972
973 assert_eq!(permissions.mode() & 0o777, 0o600);
975 }
976
977 #[test]
978 fn test_plain_file_storage_invalid_size() {
979 let temp_dir = TempDir::new().expect("Failed to create temp dir");
980 let path = temp_dir.path().join("hostkey.key");
981
982 std::fs::write(&path, [0u8; 16]).expect("Failed to write");
984
985 let storage = PlainFileStorage::with_path(path);
986 let result = storage.load();
987 assert!(matches!(result, Err(StorageError::InvalidFormat(_))));
988 }
989
990 #[test]
995 #[ignore = "Requires system keyring daemon (run manually)"]
996 fn test_keyring_storage_roundtrip() {
997 if !KeyringStorage::is_available() {
998 println!("Keyring not available, skipping test");
999 return;
1000 }
1001
1002 let storage = KeyringStorage::new().expect("Failed to create keyring storage");
1003
1004 let _ = storage.delete();
1006
1007 let hostkey = [0xAB; 32];
1008
1009 storage.store(&hostkey).expect("Failed to store");
1011
1012 let loaded = storage.load().expect("Failed to load");
1014 assert_eq!(loaded, hostkey);
1015
1016 storage.delete().expect("Failed to delete");
1018 }
1019
1020 #[test]
1021 #[ignore = "Requires system keyring daemon (run manually)"]
1022 fn test_keyring_storage_not_found() {
1023 if !KeyringStorage::is_available() {
1024 println!("Keyring not available, skipping test");
1025 return;
1026 }
1027
1028 let storage = KeyringStorage::new().expect("Failed to create keyring storage");
1029
1030 let _ = storage.delete();
1032
1033 let result = storage.load();
1034 assert!(matches!(result, Err(StorageError::NotFound)));
1035 }
1036
1037 #[test]
1038 #[ignore = "Requires system keyring daemon (run manually)"]
1039 fn test_keyring_storage_security_level() {
1040 if !KeyringStorage::is_available() {
1041 println!("Keyring not available, skipping test");
1042 return;
1043 }
1044
1045 let storage = KeyringStorage::new().expect("Failed to create keyring storage");
1046 assert_eq!(storage.security_level(), StorageSecurityLevel::Secure);
1047 assert!(storage.security_level().warning_message().is_none());
1048 }
1049
1050 #[test]
1055 fn test_security_level_warning_messages() {
1056 assert!(StorageSecurityLevel::Secure.warning_message().is_none());
1057 assert!(StorageSecurityLevel::Encrypted.warning_message().is_none());
1058 assert!(StorageSecurityLevel::Insecure.warning_message().is_some());
1059 }
1060
1061 #[test]
1062 fn test_security_level_is_secure() {
1063 assert!(StorageSecurityLevel::Secure.is_secure());
1064 assert!(StorageSecurityLevel::Encrypted.is_secure());
1065 assert!(!StorageSecurityLevel::Insecure.is_secure());
1066 }
1067
1068 #[test]
1073 fn test_auto_storage_fallback_to_plain_file() {
1074 with_password(None, || {
1076 let result = auto_storage();
1079 assert!(result.is_ok());
1080 let selection = result.expect("auto_storage should succeed");
1081 assert!(
1083 selection.security_level == StorageSecurityLevel::Secure
1084 || selection.security_level == StorageSecurityLevel::Insecure
1085 );
1086 });
1087 }
1088
1089 #[test]
1090 fn test_auto_storage_with_password() {
1091 with_password(Some("test-password"), || {
1092 let result = auto_storage();
1094 assert!(result.is_ok());
1095 let selection = result.expect("auto_storage should succeed");
1096 assert!(
1098 selection.security_level == StorageSecurityLevel::Secure
1099 || selection.security_level == StorageSecurityLevel::Encrypted
1100 );
1101 });
1102 }
1103}