use aes_gcm::{Aes256Gcm, Key};
use anyhow::{Context, Result};
use argon2::Argon2;
use hmac::{Hmac, Mac};
use sha2::{Digest, Sha256};
use zeroize::Zeroize;
use super::store::{KeyRecord, KeyRecordMap};
type HmacSha256 = Hmac<Sha256>;
pub(crate) fn derive_aes_key_argon2(passphrase: &str, salt: &[u8]) -> Result<Key<Aes256Gcm>> {
let params = argon2::Params::new(19456, 2, 1, Some(32))
.map_err(|e| anyhow::anyhow!("invalid Argon2 params: {e}"))?;
let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
let mut key_bytes = [0u8; 32];
argon2
.hash_password_into(passphrase.as_bytes(), salt, &mut key_bytes)
.map_err(|e| anyhow::anyhow!("Argon2id key derivation failed: {e}"))?;
let key = *Key::<Aes256Gcm>::from_slice(&key_bytes);
key_bytes.zeroize();
Ok(key)
}
pub(crate) fn derive_aes_key_sha256(passphrase: &str) -> Key<Aes256Gcm> {
let digest = Sha256::digest(passphrase.as_bytes());
*Key::<Aes256Gcm>::from_slice(&digest)
}
pub(crate) fn derive_integrity_key(passphrase: &str, salt: &[u8]) -> Result<[u8; 32]> {
let params = argon2::Params::new(19456, 2, 1, Some(64))
.map_err(|e| anyhow::anyhow!("invalid Argon2 params for integrity key: {e}"))?;
let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
let mut key_bytes = [0u8; 64];
argon2
.hash_password_into(passphrase.as_bytes(), salt, &mut key_bytes)
.map_err(|e| anyhow::anyhow!("Argon2id integrity key derivation failed: {e}"))?;
let mut integrity_key = [0u8; 32];
integrity_key.copy_from_slice(&key_bytes[32..64]);
key_bytes.zeroize();
Ok(integrity_key)
}
pub(crate) fn compute_envelope_hmac(
integrity_key: &[u8; 32],
kdf: &str,
salt_b64: &str,
keys: &KeyRecordMap,
) -> Result<String> {
let mac = envelope_mac(integrity_key, kdf, salt_b64, keys)?;
Ok(hex::encode(mac.finalize().into_bytes()))
}
pub(crate) fn verify_envelope_hmac(
integrity_key: &[u8; 32],
kdf: &str,
salt_b64: &str,
keys: &KeyRecordMap,
stored_hmac_hex: &str,
) -> Result<()> {
let expected = hex::decode(stored_hmac_hex)
.context("key store HMAC tag is not valid hex — file may have been tampered with")?;
let mac = envelope_mac(integrity_key, kdf, salt_b64, keys)?;
mac.verify_slice(&expected).map_err(|_| {
anyhow::anyhow!("key store integrity check failed — file may have been tampered with")
})
}
fn envelope_mac(
integrity_key: &[u8; 32],
kdf: &str,
salt_b64: &str,
keys: &KeyRecordMap,
) -> Result<HmacSha256> {
let sorted_keys: std::collections::BTreeMap<&str, &KeyRecord> =
keys.iter().map(|(k, v)| (k.as_str(), v)).collect();
let mut outer = std::collections::BTreeMap::new();
outer.insert(
"kdf",
serde_json::to_value(kdf).expect("string is valid JSON"),
);
outer.insert(
"keys",
serde_json::to_value(&sorted_keys).context("Failed to serialise keys for HMAC")?,
);
outer.insert(
"salt",
serde_json::to_value(salt_b64).expect("string is valid JSON"),
);
let canonical = serde_json::to_vec(&outer).context("Failed to serialise envelope for HMAC")?;
let mut mac = <HmacSha256 as Mac>::new_from_slice(integrity_key)
.expect("HMAC key length is always valid");
mac.update(&canonical);
Ok(mac)
}