156 lines
5.0 KiB
Rust
156 lines
5.0 KiB
Rust
//! 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<Vec<u8>>,
|
|
) -> anyhow::Result<FfiDerivedKey> {
|
|
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<u8> {
|
|
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<u8>,
|
|
) -> 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<Vec<u8>> {
|
|
#[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);
|
|
}
|
|
}
|