//! OS Keychain integration for secure credential storage //! //! Provides cross-platform secure storage using: //! - macOS: Keychain Services (TouchID/FaceID support) //! - Windows: Credential Manager (Windows Hello support) //! - Linux: Secret Service (GNOME Keyring, KDE Wallet) use keyring::Entry; use serde::{Deserialize, Serialize}; use sha2::{Sha256, Digest}; use zeroize::Zeroize; use crate::{Error, Result}; /// Service name for keychain entries const SERVICE_NAME: &str = "com.synor.wallet"; /// Username prefix for keychain entries const KEY_PREFIX: &str = "synor-wallet"; /// Keychain key types #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum KeychainKeyType { /// Master unlock key (derived from password, stored for biometric unlock) MasterUnlock, /// Session key (temporary, for "remember me" during session) Session, /// Backup encryption key (for encrypted backups) Backup, } impl KeychainKeyType { fn as_str(&self) -> &'static str { match self { Self::MasterUnlock => "master-unlock", Self::Session => "session", Self::Backup => "backup", } } } /// Stored keychain data #[derive(Debug, Clone, Serialize, Deserialize)] pub struct KeychainData { /// Key hash for verification pub key_hash: String, /// Encrypted payload (depends on key type) pub payload: String, /// Creation timestamp pub created_at: i64, /// Wallet identifier (for multi-wallet support) pub wallet_id: String, } /// Keychain service for the desktop wallet pub struct KeychainService { /// Wallet identifier (derived from first address) wallet_id: String, } impl KeychainService { /// Create a new keychain service for a wallet pub fn new(wallet_id: &str) -> Self { Self { wallet_id: wallet_id.to_string(), } } /// Create from wallet address (uses hash of address as ID) pub fn from_address(address: &str) -> Self { let mut hasher = Sha256::new(); hasher.update(address.as_bytes()); let hash = hasher.finalize(); let wallet_id = hex::encode(&hash[..8]); Self::new(&wallet_id) } /// Get the keychain entry name for a key type fn entry_name(&self, key_type: KeychainKeyType) -> String { format!("{}-{}-{}", KEY_PREFIX, self.wallet_id, key_type.as_str()) } /// Check if the OS keychain is available pub fn is_available() -> bool { let test_entry = Entry::new(SERVICE_NAME, "availability-test"); match test_entry { Ok(_) => true, Err(_) => false, } } /// Store a key in the OS keychain pub fn store_key(&self, key_type: KeychainKeyType, password_hash: &[u8]) -> Result<()> { let entry_name = self.entry_name(key_type); let entry = Entry::new(SERVICE_NAME, &entry_name) .map_err(|e| Error::Keychain(format!("Failed to create keychain entry: {}", e)))?; // Create keychain data let data = KeychainData { key_hash: hex::encode(Sha256::digest(password_hash)), payload: hex::encode(password_hash), created_at: current_timestamp(), wallet_id: self.wallet_id.clone(), }; let json = serde_json::to_string(&data) .map_err(|e| Error::Serialization(e.to_string()))?; entry.set_password(&json) .map_err(|e| Error::Keychain(format!("Failed to store in keychain: {}", e)))?; Ok(()) } /// Retrieve a key from the OS keychain pub fn retrieve_key(&self, key_type: KeychainKeyType) -> Result> { let entry_name = self.entry_name(key_type); let entry = Entry::new(SERVICE_NAME, &entry_name) .map_err(|e| Error::Keychain(format!("Failed to access keychain entry: {}", e)))?; let json = entry.get_password() .map_err(|e| Error::Keychain(format!("Key not found in keychain: {}", e)))?; let data: KeychainData = serde_json::from_str(&json) .map_err(|e| Error::Serialization(e.to_string()))?; // Verify wallet ID matches if data.wallet_id != self.wallet_id { return Err(Error::Keychain("Wallet ID mismatch".to_string())); } let key = hex::decode(&data.payload) .map_err(|e| Error::Keychain(format!("Invalid key format: {}", e)))?; Ok(key) } /// Check if a key exists in the keychain pub fn has_key(&self, key_type: KeychainKeyType) -> bool { let entry_name = self.entry_name(key_type); if let Ok(entry) = Entry::new(SERVICE_NAME, &entry_name) { entry.get_password().is_ok() } else { false } } /// Delete a key from the keychain pub fn delete_key(&self, key_type: KeychainKeyType) -> Result<()> { let entry_name = self.entry_name(key_type); let entry = Entry::new(SERVICE_NAME, &entry_name) .map_err(|e| Error::Keychain(format!("Failed to access keychain entry: {}", e)))?; entry.delete_credential() .map_err(|e| Error::Keychain(format!("Failed to delete from keychain: {}", e)))?; Ok(()) } /// Delete all keys for this wallet pub fn delete_all_keys(&self) -> Result<()> { let key_types = [ KeychainKeyType::MasterUnlock, KeychainKeyType::Session, KeychainKeyType::Backup, ]; for key_type in key_types { // Ignore errors for keys that don't exist let _ = self.delete_key(key_type); } Ok(()) } /// Store master unlock key for biometric access /// This allows unlocking the wallet using TouchID/FaceID/Windows Hello pub fn enable_biometric_unlock(&self, password: &str) -> Result<()> { // Derive a key from the password let mut hasher = Sha256::new(); hasher.update(password.as_bytes()); hasher.update(self.wallet_id.as_bytes()); let key = hasher.finalize(); self.store_key(KeychainKeyType::MasterUnlock, &key)?; Ok(()) } /// Retrieve master unlock key for biometric access pub fn get_biometric_unlock_key(&self) -> Result> { self.retrieve_key(KeychainKeyType::MasterUnlock) } /// Verify if a password matches the stored biometric key pub fn verify_biometric_key(&self, password: &str) -> Result { let stored_key = self.retrieve_key(KeychainKeyType::MasterUnlock)?; // Derive key from password and compare let mut hasher = Sha256::new(); hasher.update(password.as_bytes()); hasher.update(self.wallet_id.as_bytes()); let derived_key = hasher.finalize(); Ok(stored_key == derived_key.as_slice()) } /// Disable biometric unlock pub fn disable_biometric_unlock(&self) -> Result<()> { self.delete_key(KeychainKeyType::MasterUnlock) } /// Check if biometric unlock is enabled pub fn is_biometric_enabled(&self) -> bool { self.has_key(KeychainKeyType::MasterUnlock) } } /// Password-derived key for additional security layer #[derive(Zeroize)] #[zeroize(drop)] pub struct DerivedKey { key: [u8; 32], } impl DerivedKey { /// Derive a key from password and salt using SHA-256 /// For Argon2-based derivation, use the crypto module's encrypt_seed pub fn derive(password: &str, salt: &[u8]) -> Self { // Use HMAC-SHA256 for simple key derivation use hmac::{Hmac, Mac}; type HmacSha256 = Hmac; let mut mac = HmacSha256::new_from_slice(salt) .expect("HMAC can take key of any size"); mac.update(password.as_bytes()); let result = mac.finalize(); let mut key = [0u8; 32]; key.copy_from_slice(&result.into_bytes()); Self { key } } /// Get the key bytes pub fn as_bytes(&self) -> &[u8; 32] { &self.key } } /// Get current Unix timestamp fn current_timestamp() -> i64 { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_secs() as i64) .unwrap_or(0) } #[cfg(test)] mod tests { use super::*; #[test] fn test_keychain_service_creation() { let service = KeychainService::new("test-wallet"); assert_eq!(service.wallet_id, "test-wallet"); } #[test] fn test_from_address() { let service = KeychainService::from_address("synor1abc123"); assert!(!service.wallet_id.is_empty()); assert_eq!(service.wallet_id.len(), 16); // 8 bytes = 16 hex chars } #[test] fn test_entry_name() { let service = KeychainService::new("test"); let name = service.entry_name(KeychainKeyType::MasterUnlock); assert_eq!(name, "synor-wallet-test-master-unlock"); } #[test] fn test_derived_key() { let key1 = DerivedKey::derive("password123", b"salt"); let key2 = DerivedKey::derive("password123", b"salt"); let key3 = DerivedKey::derive("different", b"salt"); // Same password + salt = same key assert_eq!(key1.as_bytes(), key2.as_bytes()); // Different password = different key assert_ne!(key1.as_bytes(), key3.as_bytes()); } }