use alloc::vec::Vec;
use core::fmt;
use crate::hmac::{hmac_sha256, HmacSha256};
#[derive(Debug, PartialEq)]
pub enum HkdfError {
InvalidLength,
EmptyInput,
}
impl fmt::Display for HkdfError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HkdfError::InvalidLength => write!(f, "Invalid output key length"),
HkdfError::EmptyInput => write!(f, "Cannot derive key from empty input material"),
}
}
}
pub fn hkdf(
length: usize,
derive_from: &[u8],
salt: Option<&[u8]>,
context: Option<&[u8]>,
) -> Result<Vec<u8>, HkdfError> {
let hash_len: usize = 32;
if length < 1 {
return Err(HkdfError::InvalidLength);
}
if derive_from.is_empty() {
return Err(HkdfError::EmptyInput);
}
let salt = match salt {
Some(s) if !s.is_empty() => s.to_vec(),
_ => alloc::vec![0u8; hash_len],
};
let context = context.unwrap_or(b"");
let prk = hmac_sha256(&salt, derive_from);
let expansion_hmac = HmacSha256::new(&prk);
let mut block = [0u8; 32];
let mut has_block = false;
let mut derived = Vec::with_capacity(length);
let iterations = length.div_ceil(hash_len);
for i in 0..iterations {
let mut hmac = expansion_hmac.clone();
if has_block {
hmac.update(&block);
}
hmac.update(context);
hmac.update(&[((i + 1) % 256) as u8]);
block = hmac.finalize();
has_block = true;
derived.extend_from_slice(&block);
}
derived.truncate(length);
Ok(derived)
}
#[cfg(test)]
mod tests {
use super::*;
fn decode_hex(value: &str) -> Vec<u8> {
value
.as_bytes()
.chunks_exact(2)
.map(|pair| {
let digit = |byte: u8| match byte {
b'0'..=b'9' => byte - b'0',
b'a'..=b'f' => byte - b'a' + 10,
_ => panic!("invalid test vector"),
};
(digit(pair[0]) << 4) | digit(pair[1])
})
.collect()
}
fn reference_hkdf(length: usize, ikm: &[u8], salt: &[u8], context: &[u8]) -> Vec<u8> {
let prk = hmac_sha256(salt, ikm);
let mut block = Vec::new();
let mut derived = Vec::with_capacity(length);
for index in 0..length.div_ceil(32) {
let mut input = block;
input.extend_from_slice(context);
input.push(((index + 1) % 256) as u8);
block = hmac_sha256(&prk, &input).to_vec();
derived.extend_from_slice(&block);
}
derived.truncate(length);
derived
}
#[test]
fn optimized_expansion_matches_reference_across_counter_wrap() {
let ikm = b"deterministic input material";
let salt = b"deterministic salt";
let context = b"counter-wrap";
for length in [8191, 8192, 8193] {
assert_eq!(
hkdf(length, ikm, Some(salt), Some(context)).unwrap(),
reference_hkdf(length, ikm, salt, context)
);
}
}
#[test]
fn rfc5869_sha256_vectors() {
let cases = [
(
vec![0x0b; 22],
(0x00..0x0d).collect::<Vec<_>>(),
(0xf0..0xfa).collect::<Vec<_>>(),
42,
"3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865",
),
(
(0x00..0x50).collect::<Vec<_>>(),
(0x60..0xb0).collect::<Vec<_>>(),
(0xb0..=0xff).collect::<Vec<_>>(),
82,
"b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87",
),
(
vec![0x0b; 22],
Vec::new(),
Vec::new(),
42,
"8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8",
),
];
for (ikm, salt, context, length, expected) in cases {
assert_eq!(
hkdf(length, &ikm, Some(&salt), Some(&context)).unwrap(),
decode_hex(expected)
);
}
}
#[test]
fn test_hkdf_32bytes() {
let ikm = b"input key material";
let salt = b"salt value";
let result = hkdf(32, ikm, Some(salt), None).unwrap();
assert_eq!(result.len(), 32);
}
#[test]
fn test_hkdf_64bytes() {
let ikm = b"input key material";
let salt = b"salt value";
let result = hkdf(64, ikm, Some(salt), None).unwrap();
assert_eq!(result.len(), 64);
}
#[test]
fn test_hkdf_with_context() {
let ikm = b"input key material";
let salt = b"salt";
let ctx = b"context info";
let result = hkdf(32, ikm, Some(salt), Some(ctx)).unwrap();
assert_eq!(result.len(), 32);
let result2 = hkdf(32, ikm, Some(salt), None).unwrap();
assert_ne!(result, result2);
}
#[test]
fn test_hkdf_none_salt() {
let ikm = b"input key material";
let result = hkdf(32, ikm, None, None).unwrap();
assert_eq!(result.len(), 32);
}
#[test]
fn test_hkdf_empty_salt() {
let ikm = b"input key material";
let result1 = hkdf(32, ikm, Some(b""), None).unwrap();
let result2 = hkdf(32, ikm, None, None).unwrap();
assert_eq!(result1, result2);
}
#[test]
fn test_hkdf_invalid_length() {
assert_eq!(hkdf(0, b"ikm", None, None), Err(HkdfError::InvalidLength));
}
#[test]
fn test_hkdf_empty_ikm() {
assert_eq!(hkdf(32, b"", None, None), Err(HkdfError::EmptyInput));
}
}