use hkdf::Hkdf;
use sha2::Sha512;
use crate::CryptoError;
pub const HASH_LEN: usize = 64;
pub fn hkdf_sha512(
salt: &[u8],
ikm: &[u8],
info: &[u8],
length: usize,
) -> Result<Vec<u8>, CryptoError> {
let salt_opt = if salt.is_empty() { None } else { Some(salt) };
let hk = Hkdf::<Sha512>::new(salt_opt, ikm);
let mut okm = vec![0u8; length];
hk.expand(info, &mut okm).map_err(|_| {
CryptoError::Hkdf(format!(
"invalid output length {length} (max {})",
255 * HASH_LEN
))
})?;
Ok(okm)
}
#[cfg(test)]
mod tests {
use super::*;
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
fn unhex(s: &str) -> Vec<u8> {
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
.collect()
}
#[test]
fn rfc5869_sha512_kat() {
let ikm = [0x0bu8; 22];
let salt: Vec<u8> = (0u8..=0x0c).collect();
let info = unhex("f0f1f2f3f4f5f6f7f8f9");
let okm = hkdf_sha512(&salt, &ikm, &info, 42).unwrap();
assert_eq!(
hex(&okm),
"832390086cda71fb47625bb5ceb168e4c8e26a1a16ed34d9fc7fe92c1481579338da362cb8d9f925d7cb"
);
}
#[test]
fn deterministic() {
let okm1 = hkdf_sha512(b"salt", b"ikm-bytes", b"info", 32).unwrap();
let okm2 = hkdf_sha512(b"salt", b"ikm-bytes", b"info", 32).unwrap();
assert_eq!(okm1, okm2);
assert_eq!(okm1.len(), 32);
}
#[test]
fn domain_separated_by_info() {
let a = hkdf_sha512(b"salt", b"ikm", b"mosslet/user_key-wrap/v1", 32).unwrap();
let b = hkdf_sha512(b"salt", b"ikm", b"mosslet/user_key-wrap/v2", 32).unwrap();
assert_ne!(a, b);
}
#[test]
fn distinct_salt_ikm_produce_distinct_output() {
let base = hkdf_sha512(b"salt", b"ikm", b"info", 32).unwrap();
assert_ne!(base, hkdf_sha512(b"salt2", b"ikm", b"info", 32).unwrap());
assert_ne!(base, hkdf_sha512(b"salt", b"ikm2", b"info", 32).unwrap());
}
#[test]
fn empty_salt_is_none_semantics() {
let empty = hkdf_sha512(b"", b"ikm", b"info", 32).unwrap();
let zeros = hkdf_sha512(&[0u8; HASH_LEN], b"ikm", b"info", 32).unwrap();
assert_eq!(empty, zeros);
}
#[test]
fn rejects_too_long_output() {
assert!(hkdf_sha512(b"salt", b"ikm", b"info", 255 * HASH_LEN + 1).is_err());
assert!(hkdf_sha512(b"salt", b"ikm", b"info", 255 * HASH_LEN).is_ok());
}
use proptest::prelude::*;
proptest! {
#[test]
fn hkdf_is_deterministic(salt: Vec<u8>, ikm: Vec<u8>, info: Vec<u8>) {
prop_assert_eq!(
hkdf_sha512(&salt, &ikm, &info, 32).unwrap(),
hkdf_sha512(&salt, &ikm, &info, 32).unwrap()
);
}
}
}