use argon2::{Algorithm, Argon2, Params, Version};
use hkdf::Hkdf;
use sha2::Sha256;
use unicode_normalization::UnicodeNormalization;
use zeroize::Zeroize;
pub const KEY_LEN: usize = 32;
pub const SALT_LEN: usize = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KdfParams {
pub mem_kib: u32,
pub iterations: u32,
pub parallelism: u32,
}
impl Default for KdfParams {
fn default() -> Self {
Self {
mem_kib: 65536,
iterations: 3,
parallelism: 1,
}
}
}
impl KdfParams {
pub const MAX_MEM_KIB: u32 = 262_144;
pub const MAX_ITERATIONS: u32 = 16;
pub const MAX_PARALLELISM: u32 = 16;
pub fn is_sane(&self) -> bool {
self.parallelism >= 1
&& self.parallelism <= Self::MAX_PARALLELISM
&& self.iterations >= 1
&& self.iterations <= Self::MAX_ITERATIONS
&& self.mem_kib >= 8 * self.parallelism
&& self.mem_kib <= Self::MAX_MEM_KIB
}
}
pub fn derive_master_key(
passphrase: &str,
salt: &[u8; SALT_LEN],
pepper: &[u8],
params: &KdfParams,
) -> [u8; KEY_LEN] {
let mut normalized: String = passphrase.nfkc().collect();
let mut secret = normalized.clone().into_bytes();
normalized.zeroize(); secret.extend_from_slice(pepper);
let argon = Argon2::new(
Algorithm::Argon2id,
Version::V0x13,
Params::new(
params.mem_kib,
params.iterations,
params.parallelism,
Some(KEY_LEN),
)
.expect("parámetros Argon2id válidos"),
);
let mut out = [0u8; KEY_LEN];
argon
.hash_password_into(&secret, salt, &mut out)
.expect("derivación Argon2id no debe fallar con entradas válidas");
secret.zeroize(); out
}
pub fn derive_subkey(master: &[u8; KEY_LEN], info: &[u8]) -> [u8; KEY_LEN] {
let hk = Hkdf::<Sha256>::new(None, master);
let mut out = [0u8; KEY_LEN];
hk.expand(info, &mut out)
.expect("longitud de expansión HKDF válida");
out
}
pub fn derive_stream(master: &[u8; KEY_LEN], info: &[u8], out: &mut [u8]) {
let hk = Hkdf::<Sha256>::new(None, master);
hk.expand(info, out)
.expect("longitud de expansión HKDF dentro del límite (<= 8160 bytes)");
}
#[cfg(test)]
mod tests {
use super::*;
fn cheap() -> KdfParams {
KdfParams {
mem_kib: 64,
iterations: 1,
parallelism: 1,
}
}
#[test]
fn is_sane_bounds_the_cost_ceiling() {
assert!(KdfParams::default().is_sane());
assert!(
KdfParams {
mem_kib: KdfParams::MAX_MEM_KIB,
iterations: 3,
parallelism: 1,
}
.is_sane()
);
assert!(
!KdfParams {
mem_kib: KdfParams::MAX_MEM_KIB + 1,
iterations: 3,
parallelism: 1,
}
.is_sane()
);
assert!(
!KdfParams {
mem_kib: u32::MAX,
iterations: u32::MAX,
parallelism: u32::MAX,
}
.is_sane()
);
}
#[test]
fn different_passphrases_yield_different_keys() {
let salt = [3u8; SALT_LEN];
let a = derive_master_key("password-A", &salt, b"", &cheap());
let b = derive_master_key("password-B", &salt, b"", &cheap());
assert_ne!(a, b);
}
#[test]
fn is_deterministic_for_same_inputs() {
let salt = [3u8; SALT_LEN];
let a = derive_master_key("pw", &salt, b"pep", &cheap());
let b = derive_master_key("pw", &salt, b"pep", &cheap());
assert_eq!(a, b);
}
#[test]
fn nfkc_equivalent_passphrases_yield_same_key() {
let precomposed = "caf\u{00e9}";
let decomposed = "cafe\u{0301}";
assert_ne!(precomposed.as_bytes(), decomposed.as_bytes()); let salt = [3u8; SALT_LEN];
let a = derive_master_key(precomposed, &salt, b"", &cheap());
let b = derive_master_key(decomposed, &salt, b"", &cheap());
assert_eq!(a, b); }
#[test]
fn different_pepper_yields_different_key() {
let salt = [3u8; SALT_LEN];
let a = derive_master_key("pw", &salt, b"pepper-1", &cheap());
let b = derive_master_key("pw", &salt, b"pepper-2", &cheap());
assert_ne!(a, b);
}
#[test]
fn different_salt_yields_different_key() {
let a = derive_master_key("pw", &[1u8; SALT_LEN], b"", &cheap());
let b = derive_master_key("pw", &[2u8; SALT_LEN], b"", &cheap());
assert_ne!(a, b);
}
#[test]
fn subkeys_are_domain_separated() {
let master = [42u8; KEY_LEN];
let k_cipher = derive_subkey(&master, b"cipher");
let k_codebook = derive_subkey(&master, b"codebook");
assert_ne!(k_cipher, k_codebook); assert_eq!(k_cipher, derive_subkey(&master, b"cipher")); }
}