use crate::algorithm::HashAlgorithm;
use crate::backend::{require_supported, Operation};
use crate::error::{Error, Result};
use digest::Digest;
#[derive(Debug, Clone)]
pub struct ConcatKdfParams {
pub hash: HashAlgorithm,
pub algorithm_id: Option<Vec<u8>>,
pub party_u_info: Option<Vec<u8>>,
pub party_v_info: Option<Vec<u8>>,
}
impl Default for ConcatKdfParams {
fn default() -> Self {
Self {
hash: HashAlgorithm::Sha256,
algorithm_id: None,
party_u_info: None,
party_v_info: None,
}
}
}
pub const PBKDF2_MIN_SALT_LEN: usize = 8;
pub const PBKDF2_MIN_ITERATIONS: u32 = 1;
pub const PBKDF2_MAX_ITERATIONS: u32 = 100_000_000;
pub const PBKDF2_MAX_KEY_LEN: usize = 1 << 20;
pub const CONCAT_KDF_MAX_KEY_LEN: usize = 1 << 20;
#[derive(Debug, Clone)]
pub struct Pbkdf2Params {
pub hash: HashAlgorithm,
pub salt: Vec<u8>,
pub iteration_count: u32,
pub key_length: usize,
}
impl Pbkdf2Params {
pub fn recommended(hash: HashAlgorithm, salt: Vec<u8>, key_length: usize) -> Self {
let iteration_count = match hash {
HashAlgorithm::Sha1 => 1_300_000,
HashAlgorithm::Sha384 => 310_000,
HashAlgorithm::Sha512 => 210_000,
_ => 600_000,
};
Self {
hash,
salt,
iteration_count,
key_length,
}
}
}
#[derive(Debug, Clone)]
pub struct HkdfParams {
pub hash: HashAlgorithm,
pub salt: Option<Vec<u8>>,
pub info: Option<Vec<u8>>,
pub key_length_bits: u32,
}
impl Default for HkdfParams {
fn default() -> Self {
Self {
hash: HashAlgorithm::Sha256,
salt: None,
info: None,
key_length_bits: 0,
}
}
}
pub fn concat_kdf(
shared_secret: &[u8],
key_len: usize,
params: &ConcatKdfParams,
) -> Result<Vec<u8>> {
require_supported(Operation::ConcatKdf(params.hash))?;
let mut other_info = Vec::new();
if let Some(ref alg_id) = params.algorithm_id {
other_info.extend_from_slice(alg_id);
}
if let Some(ref party_u) = params.party_u_info {
other_info.extend_from_slice(party_u);
}
if let Some(ref party_v) = params.party_v_info {
other_info.extend_from_slice(party_v);
}
match params.hash {
HashAlgorithm::Sha1 => concat_kdf_inner::<sha1::Sha1>(shared_secret, &other_info, key_len),
HashAlgorithm::Sha224 => {
concat_kdf_inner::<sha2::Sha224>(shared_secret, &other_info, key_len)
}
HashAlgorithm::Sha256 => {
concat_kdf_inner::<sha2::Sha256>(shared_secret, &other_info, key_len)
}
HashAlgorithm::Sha384 => {
concat_kdf_inner::<sha2::Sha384>(shared_secret, &other_info, key_len)
}
HashAlgorithm::Sha512 => {
concat_kdf_inner::<sha2::Sha512>(shared_secret, &other_info, key_len)
}
HashAlgorithm::Sha3_224 => {
concat_kdf_inner::<sha3::Sha3_224>(shared_secret, &other_info, key_len)
}
HashAlgorithm::Sha3_256 => {
concat_kdf_inner::<sha3::Sha3_256>(shared_secret, &other_info, key_len)
}
HashAlgorithm::Sha3_384 => {
concat_kdf_inner::<sha3::Sha3_384>(shared_secret, &other_info, key_len)
}
HashAlgorithm::Sha3_512 => {
concat_kdf_inner::<sha3::Sha3_512>(shared_secret, &other_info, key_len)
}
#[cfg(feature = "legacy")]
HashAlgorithm::Md5 => concat_kdf_inner::<md5::Md5>(shared_secret, &other_info, key_len),
#[cfg(feature = "legacy")]
HashAlgorithm::Ripemd160 => {
concat_kdf_inner::<ripemd::Ripemd160>(shared_secret, &other_info, key_len)
}
}
}
fn concat_kdf_inner<H: Digest + Clone>(
shared_secret: &[u8],
other_info: &[u8],
key_len: usize,
) -> Result<Vec<u8>> {
if key_len == 0 {
return Err(Error::Crypto("ConcatKDF key_len must be > 0".into()));
}
if key_len > CONCAT_KDF_MAX_KEY_LEN {
return Err(Error::Crypto(format!(
"ConcatKDF key_len {key_len} exceeds cap of {CONCAT_KDF_MAX_KEY_LEN} bytes"
)));
}
let hash_len = <H as Digest>::output_size();
let reps = key_len.div_ceil(hash_len);
let mut derived = Vec::with_capacity(reps * hash_len);
for counter in 1..=(reps as u32) {
let mut hasher = H::new();
hasher.update(counter.to_be_bytes());
hasher.update(shared_secret);
hasher.update(other_info);
derived.extend_from_slice(&hasher.finalize());
}
derived.truncate(key_len);
Ok(derived)
}
pub fn pbkdf2_derive(password: &[u8], params: &Pbkdf2Params) -> Result<Vec<u8>> {
require_supported(Operation::Pbkdf2(params.hash))?;
if params.salt.len() < PBKDF2_MIN_SALT_LEN {
return Err(Error::Crypto(format!(
"PBKDF2 salt must be at least {PBKDF2_MIN_SALT_LEN} bytes (RFC 8018 §4.1), got {}",
params.salt.len()
)));
}
if params.iteration_count < PBKDF2_MIN_ITERATIONS {
return Err(Error::Crypto(format!(
"PBKDF2 iteration_count must be at least {PBKDF2_MIN_ITERATIONS}, got {}",
params.iteration_count
)));
}
if params.iteration_count > PBKDF2_MAX_ITERATIONS {
return Err(Error::Crypto(format!(
"PBKDF2 iteration_count {} exceeds cap of {PBKDF2_MAX_ITERATIONS}",
params.iteration_count
)));
}
if params.key_length == 0 {
return Err(Error::Crypto("PBKDF2 key_length must be > 0".into()));
}
if params.key_length > PBKDF2_MAX_KEY_LEN {
return Err(Error::Crypto(format!(
"PBKDF2 key_length {} exceeds cap of {PBKDF2_MAX_KEY_LEN} bytes",
params.key_length
)));
}
let mut derived = vec![0u8; params.key_length];
match params.hash {
HashAlgorithm::Sha1 => {
pbkdf2::pbkdf2_hmac::<sha1::Sha1>(
password,
¶ms.salt,
params.iteration_count,
&mut derived,
);
}
HashAlgorithm::Sha224 => {
pbkdf2::pbkdf2_hmac::<sha2::Sha224>(
password,
¶ms.salt,
params.iteration_count,
&mut derived,
);
}
HashAlgorithm::Sha256 => {
pbkdf2::pbkdf2_hmac::<sha2::Sha256>(
password,
¶ms.salt,
params.iteration_count,
&mut derived,
);
}
HashAlgorithm::Sha384 => {
pbkdf2::pbkdf2_hmac::<sha2::Sha384>(
password,
¶ms.salt,
params.iteration_count,
&mut derived,
);
}
HashAlgorithm::Sha512 => {
pbkdf2::pbkdf2_hmac::<sha2::Sha512>(
password,
¶ms.salt,
params.iteration_count,
&mut derived,
);
}
HashAlgorithm::Sha3_224
| HashAlgorithm::Sha3_256
| HashAlgorithm::Sha3_384
| HashAlgorithm::Sha3_512 => {
return Err(Error::unsupported(
Operation::Pbkdf2(params.hash),
format!(
"PBKDF2 with {:?}: SHA-3 not supported by PBKDF2",
params.hash
),
));
}
#[cfg(feature = "legacy")]
HashAlgorithm::Md5 | HashAlgorithm::Ripemd160 => {
return Err(Error::unsupported(
Operation::Pbkdf2(params.hash),
format!("PBKDF2 with {:?}: legacy hash not supported", params.hash),
));
}
}
Ok(derived)
}
pub fn hkdf_derive(shared_secret: &[u8], key_len: usize, params: &HkdfParams) -> Result<Vec<u8>> {
require_supported(Operation::Hkdf(params.hash))?;
let out_len = if params.key_length_bits > 0 {
if !params.key_length_bits.is_multiple_of(8) {
return Err(Error::Crypto(format!(
"HKDF key_length_bits must be byte-aligned, got {}",
params.key_length_bits
)));
}
(params.key_length_bits as usize) / 8
} else if key_len > 0 {
key_len
} else {
return Err(Error::Crypto(
"HKDF output length is required (set params.key_length_bits or pass key_len)".into(),
));
};
let salt = params.salt.as_deref();
let info = params.info.as_deref().unwrap_or(&[]);
macro_rules! hkdf_expand {
($hasher:ty) => {{
let max_len = 255 * <$hasher as Digest>::output_size();
if out_len == 0 {
return Err(Error::Crypto("HKDF output length must be > 0".into()));
}
if out_len > max_len {
return Err(Error::Crypto(format!(
"HKDF output length {out_len} exceeds RFC 5869 cap of {max_len} bytes for {:?}",
params.hash
)));
}
let hk = hkdf::Hkdf::<$hasher>::new(salt, shared_secret);
let mut okm = vec![0u8; out_len];
hk.expand(info, &mut okm)
.map_err(|e| Error::Crypto(format!("HKDF expand failed: {e}")))?;
Ok(okm)
}};
}
match params.hash {
HashAlgorithm::Sha1 => hkdf_expand!(sha1::Sha1),
HashAlgorithm::Sha224 => hkdf_expand!(sha2::Sha224),
HashAlgorithm::Sha256 => hkdf_expand!(sha2::Sha256),
HashAlgorithm::Sha384 => hkdf_expand!(sha2::Sha384),
HashAlgorithm::Sha512 => hkdf_expand!(sha2::Sha512),
HashAlgorithm::Sha3_224 => hkdf_expand!(sha3::Sha3_224),
HashAlgorithm::Sha3_256 => hkdf_expand!(sha3::Sha3_256),
HashAlgorithm::Sha3_384 => hkdf_expand!(sha3::Sha3_384),
HashAlgorithm::Sha3_512 => hkdf_expand!(sha3::Sha3_512),
#[cfg(feature = "legacy")]
HashAlgorithm::Md5 => hkdf_expand!(md5::Md5),
#[cfg(feature = "legacy")]
HashAlgorithm::Ripemd160 => hkdf_expand!(ripemd::Ripemd160),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hkdf_sha256_test_case_1() {
let ikm = [0x0b; 22];
let salt = hex_decode("000102030405060708090a0b0c");
let info = hex_decode("f0f1f2f3f4f5f6f7f8f9");
let params = HkdfParams {
hash: HashAlgorithm::Sha256,
salt: Some(salt),
info: Some(info),
key_length_bits: 336, };
let okm = hkdf_derive(&ikm, 0, ¶ms).unwrap();
assert_eq!(okm.len(), 42);
assert_eq!(
hex_encode(&okm),
"3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865"
);
}
#[test]
fn hkdf_sha256_test_case_2() {
let ikm = hex_decode(
"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f\
202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f\
404142434445464748494a4b4c4d4e4f",
);
let salt = hex_decode(
"606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f\
808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f\
a0a1a2a3a4a5a6a7a8a9aaabacadaeaf",
);
let info = hex_decode(
"b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf\
d0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeef\
f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
);
let params = HkdfParams {
hash: HashAlgorithm::Sha256,
salt: Some(salt),
info: Some(info),
key_length_bits: 656, };
let okm = hkdf_derive(&ikm, 0, ¶ms).unwrap();
assert_eq!(okm.len(), 82);
assert_eq!(
hex_encode(&okm),
"b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c\
59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71\
cc30c58179ec3e87c14c01d5c1f3434f1d87"
);
}
#[test]
fn hkdf_sha256_test_case_3() {
let ikm = [0x0b; 22];
let params = HkdfParams {
hash: HashAlgorithm::Sha256,
salt: None,
info: None,
key_length_bits: 336, };
let okm = hkdf_derive(&ikm, 0, ¶ms).unwrap();
assert_eq!(okm.len(), 42);
assert_eq!(
hex_encode(&okm),
"8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8"
);
}
#[test]
fn hkdf_sha1_test_case_4() {
let ikm = [0x0b; 11];
let salt = hex_decode("000102030405060708090a0b0c");
let info = hex_decode("f0f1f2f3f4f5f6f7f8f9");
let params = HkdfParams {
hash: HashAlgorithm::Sha1,
salt: Some(salt),
info: Some(info),
key_length_bits: 336, };
let okm = hkdf_derive(&ikm, 0, ¶ms).unwrap();
assert_eq!(okm.len(), 42);
assert_eq!(
hex_encode(&okm),
"085a01ea1b10f36933068b56efa5ad81a4f14b822f5b091568a9cdd4f155fda2c22e422478d305f3f896"
);
}
#[test]
fn hkdf_sha1_test_case_5() {
let ikm = hex_decode(
"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f\
202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f\
404142434445464748494a4b4c4d4e4f",
);
let salt = hex_decode(
"606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f\
808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f\
a0a1a2a3a4a5a6a7a8a9aaabacadaeaf",
);
let info = hex_decode(
"b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf\
d0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeef\
f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
);
let params = HkdfParams {
hash: HashAlgorithm::Sha1,
salt: Some(salt),
info: Some(info),
key_length_bits: 656, };
let okm = hkdf_derive(&ikm, 0, ¶ms).unwrap();
assert_eq!(okm.len(), 82);
assert_eq!(
hex_encode(&okm),
"0bd770a74d1160f7c9f12cd5912a06ebff6adcae899d92191fe4305673ba2ffe\
8fa3f1a4e5ad79f3f334b3b202b2173c486ea37ce3d397ed034c7f9dfeb15c5e\
927336d0441f4c4300e2cff0d0900b52d3b4"
);
}
#[test]
fn hkdf_sha1_test_case_7() {
let ikm = [0x0b; 22];
let params = HkdfParams {
hash: HashAlgorithm::Sha1,
salt: None,
info: None,
key_length_bits: 336, };
let okm = hkdf_derive(&ikm, 0, ¶ms).unwrap();
assert_eq!(okm.len(), 42);
assert_eq!(
hex_encode(&okm),
"0ac1af7002b3d761d1e55298da9d0506b9ae52057220a306e07b6b87e8df21d0ea00033de03984d34918"
);
}
#[test]
fn hkdf_default_hash_is_sha256() {
let ikm = [0x0b; 22];
let params_explicit = HkdfParams {
hash: HashAlgorithm::Sha256,
salt: None,
info: None,
key_length_bits: 128,
};
let params_default = HkdfParams {
key_length_bits: 128,
..Default::default()
};
let okm1 = hkdf_derive(&ikm, 0, ¶ms_explicit).unwrap();
let okm2 = hkdf_derive(&ikm, 0, ¶ms_default).unwrap();
assert_eq!(okm1, okm2);
}
#[test]
fn hkdf_key_len_fallback() {
let ikm = [0x0b; 22];
let params = HkdfParams {
hash: HashAlgorithm::Sha256,
salt: None,
info: None,
key_length_bits: 0,
};
let okm = hkdf_derive(&ikm, 32, ¶ms).unwrap();
assert_eq!(okm.len(), 32);
}
#[test]
fn hkdf_rejects_non_byte_aligned_key_length_bits() {
let params = HkdfParams {
hash: HashAlgorithm::Sha256,
salt: None,
info: None,
key_length_bits: 9,
};
let err = hkdf_derive(&[0x0b; 22], 0, ¶ms).unwrap_err();
assert!(err.to_string().contains("byte-aligned"), "got: {err}");
}
#[test]
fn hkdf_rejects_output_longer_than_rfc5869_cap() {
let params = HkdfParams {
hash: HashAlgorithm::Sha256,
salt: None,
info: None,
key_length_bits: 0,
};
let err = hkdf_derive(&[0x0b; 22], 255 * 32 + 1, ¶ms).unwrap_err();
assert!(err.to_string().contains("RFC 5869 cap"), "got: {err}");
}
#[test]
fn concat_kdf_sha256_produces_correct_length() {
let shared = [0xab; 32];
let params = ConcatKdfParams {
hash: HashAlgorithm::Sha256,
algorithm_id: Some(b"A128CBC-HS256".to_vec()),
..Default::default()
};
let derived = concat_kdf(&shared, 16, ¶ms).unwrap();
assert_eq!(derived.len(), 16);
}
#[test]
fn concat_kdf_default_hash_is_sha256() {
let shared = [0xab; 32];
let params_default = ConcatKdfParams::default();
let params_explicit = ConcatKdfParams {
hash: HashAlgorithm::Sha256,
..Default::default()
};
let d1 = concat_kdf(&shared, 32, ¶ms_default).unwrap();
let d2 = concat_kdf(&shared, 32, ¶ms_explicit).unwrap();
assert_eq!(d1, d2);
}
#[test]
fn concat_kdf_multi_round() {
let shared = [0xcd; 32];
let params = ConcatKdfParams::default();
let derived = concat_kdf(&shared, 64, ¶ms).unwrap();
assert_eq!(derived.len(), 64);
}
#[test]
fn concat_kdf_rejects_zero_key_length() {
let params = ConcatKdfParams::default();
let err = concat_kdf(&[0xab; 32], 0, ¶ms).unwrap_err();
assert!(err.to_string().contains("key_len"), "got: {err}");
}
#[test]
fn concat_kdf_rejects_huge_key_length() {
let params = ConcatKdfParams::default();
let err = concat_kdf(&[0xab; 32], CONCAT_KDF_MAX_KEY_LEN + 1, ¶ms).unwrap_err();
assert!(err.to_string().contains("key_len"), "got: {err}");
}
const PBKDF2_TEST_SALT: &[u8; 16] = b"pbkdf2-salt-16by";
#[test]
fn pbkdf2_sha256_basic() {
let params = Pbkdf2Params {
hash: HashAlgorithm::Sha256,
salt: PBKDF2_TEST_SALT.to_vec(),
iteration_count: 4096,
key_length: 32,
};
let derived = pbkdf2_derive(b"password", ¶ms).unwrap();
assert_eq!(derived.len(), 32);
}
#[test]
fn pbkdf2_sha512_basic() {
let params = Pbkdf2Params {
hash: HashAlgorithm::Sha512,
salt: PBKDF2_TEST_SALT.to_vec(),
iteration_count: 1000,
key_length: 64,
};
let derived = pbkdf2_derive(b"secret", ¶ms).unwrap();
assert_eq!(derived.len(), 64);
}
#[test]
fn pbkdf2_sha3_unsupported() {
let params = Pbkdf2Params {
hash: HashAlgorithm::Sha3_256,
salt: PBKDF2_TEST_SALT.to_vec(),
iteration_count: 1000,
key_length: 32,
};
let err = pbkdf2_derive(b"password", ¶ms).unwrap_err();
assert!(
matches!(
err,
Error::UnsupportedAlgorithm {
operation: Operation::Pbkdf2(HashAlgorithm::Sha3_256),
..
}
),
"unexpected error: {err}"
);
}
#[test]
fn pbkdf2_rejects_zero_iterations() {
let params = Pbkdf2Params {
hash: HashAlgorithm::Sha256,
salt: PBKDF2_TEST_SALT.to_vec(),
iteration_count: 0,
key_length: 32,
};
let err = pbkdf2_derive(b"password", ¶ms).unwrap_err();
assert!(err.to_string().contains("iteration_count"), "got: {err}");
}
#[test]
fn pbkdf2_accepts_low_but_positive_iterations() {
let params = Pbkdf2Params {
hash: HashAlgorithm::Sha256,
salt: PBKDF2_TEST_SALT.to_vec(),
iteration_count: 512,
key_length: 32,
};
let derived = pbkdf2_derive(b"password", ¶ms).unwrap();
assert_eq!(derived.len(), 32);
}
#[test]
fn pbkdf2_rejects_empty_salt() {
let params = Pbkdf2Params {
hash: HashAlgorithm::Sha256,
salt: Vec::new(),
iteration_count: 4096,
key_length: 32,
};
let err = pbkdf2_derive(b"password", ¶ms).unwrap_err();
assert!(err.to_string().contains("salt"), "got: {err}");
}
#[test]
fn pbkdf2_rejects_short_salt() {
let params = Pbkdf2Params {
hash: HashAlgorithm::Sha256,
salt: vec![0xAB; PBKDF2_MIN_SALT_LEN - 1],
iteration_count: 4096,
key_length: 32,
};
let err = pbkdf2_derive(b"password", ¶ms).unwrap_err();
assert!(err.to_string().contains("salt"), "got: {err}");
}
#[test]
fn pbkdf2_rejects_zero_key_length() {
let params = Pbkdf2Params {
hash: HashAlgorithm::Sha256,
salt: PBKDF2_TEST_SALT.to_vec(),
iteration_count: 4096,
key_length: 0,
};
let err = pbkdf2_derive(b"password", ¶ms).unwrap_err();
assert!(err.to_string().contains("key_length"), "got: {err}");
}
#[test]
fn pbkdf2_rejects_huge_key_length() {
let params = Pbkdf2Params {
hash: HashAlgorithm::Sha256,
salt: PBKDF2_TEST_SALT.to_vec(),
iteration_count: 4096,
key_length: PBKDF2_MAX_KEY_LEN + 1,
};
let err = pbkdf2_derive(b"password", ¶ms).unwrap_err();
assert!(err.to_string().contains("key_length"), "got: {err}");
}
#[test]
fn pbkdf2_recommended_encodes_owasp_values() {
let salt = PBKDF2_TEST_SALT.to_vec();
let sha1 = Pbkdf2Params::recommended(HashAlgorithm::Sha1, salt.clone(), 32);
assert_eq!(sha1.iteration_count, 1_300_000);
let sha256 = Pbkdf2Params::recommended(HashAlgorithm::Sha256, salt.clone(), 32);
assert_eq!(sha256.iteration_count, 600_000);
let sha384 = Pbkdf2Params::recommended(HashAlgorithm::Sha384, salt.clone(), 32);
assert_eq!(sha384.iteration_count, 310_000);
let sha512 = Pbkdf2Params::recommended(HashAlgorithm::Sha512, salt, 32);
assert_eq!(sha512.iteration_count, 210_000);
}
fn hex_decode(s: &str) -> Vec<u8> {
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
.collect()
}
fn hex_encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
}