//! Cryptographic operations — Argon2id key derivation and OS keychain access. //! //! These functions wrap [`chronogrid_crypto`] for FFI consumption. //! Key derivation is stateless and does **not** require an active engine. //! Keychain operations are platform-gated: on Android they return an error //! because credential storage is delegated to the Flutter KeyStore bridge. use super::types::FfiDerivedKey; /// Derives a 256-bit master key from a passphrase using Argon2id. /// /// If `salt` is `None`, a fresh 16-byte random salt is generated. /// If `salt` is provided, it **must** be exactly 16 bytes. /// /// Returns the derived key and the salt that was used (needed to re-derive /// the same key later). pub fn crypto_derive_master_key( passphrase: String, salt: Option>, ) -> anyhow::Result { let salt_ref = salt.as_deref(); let derived = chronogrid_crypto::kdf::derive_master_key(&passphrase, salt_ref) .map_err(|e| anyhow::anyhow!("key derivation failed: {e}"))?; Ok(FfiDerivedKey { key: derived.key().to_vec(), salt: derived.salt().to_vec(), }) } /// Generates a cryptographically random 16-byte salt. /// /// This is a convenience wrapper; [`crypto_derive_master_key`] already /// generates a salt internally when `salt` is `None`. pub fn crypto_generate_salt() -> Vec { chronogrid_crypto::kdf::generate_salt().to_vec() } /// Stores raw key material in the OS credential store. /// /// The bytes are Base64-encoded internally for credential-manager /// compatibility. /// /// # Platform Support /// /// * **Windows**: Windows Credential Manager (DPAPI) /// * **Linux**: SecretService D-Bus API (libsecret) /// * **macOS**: macOS Keychain /// * **Android**: Returns an error — use Flutter KeyStore bridge instead. pub fn crypto_store_credential( service: String, account: String, key_material: Vec, ) -> anyhow::Result<()> { #[cfg(not(target_os = "android"))] { chronogrid_crypto::keychain::store_credential(&service, &account, &key_material) .map_err(|e| anyhow::anyhow!("keychain store failed: {e}")) } #[cfg(target_os = "android")] { let _ = (service, account, key_material); anyhow::bail!( "OS keychain is not available on Android; \ use the Flutter KeyStore bridge instead" ) } } /// Retrieves raw key material from the OS credential store. /// /// # Errors /// /// Returns an error if the credential is not found or the keychain /// backend fails. On Android, always returns an error. pub fn crypto_retrieve_credential( service: String, account: String, ) -> anyhow::Result> { #[cfg(not(target_os = "android"))] { chronogrid_crypto::keychain::retrieve_credential(&service, &account) .map_err(|e| anyhow::anyhow!("keychain retrieve failed: {e}")) } #[cfg(target_os = "android")] { let _ = (service, account); anyhow::bail!( "OS keychain is not available on Android; \ use the Flutter KeyStore bridge instead" ) } } /// Deletes a credential from the OS credential store. /// /// Idempotent — succeeds even if the credential does not exist. /// On Android, always returns an error. pub fn crypto_delete_credential( service: String, account: String, ) -> anyhow::Result<()> { #[cfg(not(target_os = "android"))] { chronogrid_crypto::keychain::delete_credential(&service, &account) .map_err(|e| anyhow::anyhow!("keychain delete failed: {e}")) } #[cfg(target_os = "android")] { let _ = (service, account); anyhow::bail!( "OS keychain is not available on Android; \ use the Flutter KeyStore bridge instead" ) } } // ── Tests ──────────────────────────────────────────────────────────────────── #[cfg(test)] mod tests { use super::*; #[test] fn derive_key_with_random_salt() { let dk = crypto_derive_master_key("correct horse battery staple".into(), None) .expect("derivation should succeed"); assert_eq!(dk.key.len(), 32); assert_eq!(dk.salt.len(), 16); } #[test] fn derive_key_deterministic_with_same_salt() { let salt = vec![0xABu8; 16]; let dk1 = crypto_derive_master_key("hunter2".into(), Some(salt.clone())).unwrap(); let dk2 = crypto_derive_master_key("hunter2".into(), Some(salt)).unwrap(); assert_eq!(dk1.key, dk2.key, "same passphrase + salt must yield same key"); } #[test] fn reject_invalid_salt_length() { let bad_salt = vec![0u8; 10]; let result = crypto_derive_master_key("pw".into(), Some(bad_salt)); assert!(result.is_err()); } #[test] fn generate_salt_returns_16_bytes() { let salt = crypto_generate_salt(); assert_eq!(salt.len(), 16); } }