use ic_core::traits::Mac;
use ic_core::{ensure, Result, Zeroize};
const MAX_TAG_LEN: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IterationVerdict {
Recommended,
Weak,
Unacceptable,
}
pub const fn check_iterations(iterations: u32) -> IterationVerdict {
if iterations < 1_000 {
IterationVerdict::Unacceptable
} else if iterations < crate::PBKDF2_MIN_RECOMMENDED_ITERATIONS {
IterationVerdict::Weak
} else {
IterationVerdict::Recommended
}
}
pub fn pbkdf2<M: Mac>(password: &[u8], salt: &[u8], iterations: u32, out: &mut [u8]) -> Result<()> {
ensure!(
!matches!(check_iterations(iterations), IterationVerdict::Unacceptable),
InvalidParameter,
"pbkdf2 iterations below the SP 800-132 minimum of 1000"
);
ensure!(
salt.len() >= 16,
InvalidParameter,
"pbkdf2 salt must be >= 128 bits"
);
ensure!(!out.is_empty(), InvalidLength, "pbkdf2 output");
ensure!(
M::TAG_LEN <= MAX_TAG_LEN,
InvalidParameter,
"mac tag too wide"
);
let mut u = [0u8; MAX_TAG_LEN];
let mut acc = [0u8; MAX_TAG_LEN];
for (block_index, chunk) in out.chunks_mut(M::TAG_LEN).enumerate() {
let counter = (block_index as u32)
.checked_add(1)
.ok_or(ic_core::err!(CounterExhausted, "pbkdf2 block counter"))?;
let mut m = M::new(password)?;
m.update(salt);
m.update(&counter.to_be_bytes());
let t = m.finalize();
u[..M::TAG_LEN].copy_from_slice(t.as_ref());
acc[..M::TAG_LEN].copy_from_slice(t.as_ref());
for _ in 1..iterations {
let mut m = M::new(password)?;
m.update(&u[..M::TAG_LEN]);
let t = m.finalize();
u[..M::TAG_LEN].copy_from_slice(t.as_ref());
for j in 0..M::TAG_LEN {
acc[j] ^= u[j];
}
}
chunk.copy_from_slice(&acc[..chunk.len()]);
}
u.zeroize();
acc.zeroize();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use ic_core::codec::hex;
use ic_mac::{HmacSha256, HmacSha512};
#[test]
fn matches_the_prf_xor_chain() {
let salt = b"0123456789abcdef";
let mut out = [0u8; 32];
pbkdf2::<HmacSha256>(b"pw", salt, 1_000, &mut out).unwrap();
let mut first = HmacSha256::new(b"pw").unwrap();
first.update(salt);
first.update(&1u32.to_be_bytes());
let mut u = first.finalize();
let mut acc = u;
for _ in 1..1_000 {
u = HmacSha256::mac(b"pw", u.as_ref()).unwrap();
for j in 0..32 {
acc[j] ^= u[j];
}
}
assert_eq!(hex(&out), hex(acc.as_ref()));
}
#[test]
fn is_deterministic() {
let mut a = [0u8; 32];
let mut b = [0u8; 32];
pbkdf2::<HmacSha256>(b"passwd", b"salt-at-least-16", 1_000, &mut a).unwrap();
pbkdf2::<HmacSha256>(b"passwd", b"salt-at-least-16", 1_000, &mut b).unwrap();
assert_eq!(a, b);
}
#[test]
fn iteration_count_changes_the_key() {
let mut a = [0u8; 32];
let mut b = [0u8; 32];
pbkdf2::<HmacSha256>(b"pw", b"0123456789abcdef", 1_000, &mut a).unwrap();
pbkdf2::<HmacSha256>(b"pw", b"0123456789abcdef", 2_000, &mut b).unwrap();
assert_ne!(a, b);
}
#[test]
fn output_longer_than_one_block() {
let mut out = [0u8; 100];
pbkdf2::<HmacSha256>(b"pw", b"0123456789abcdef", 1_000, &mut out).unwrap();
assert_ne!(&out[..32], &out[32..64]);
}
#[test]
fn sha512_instantiation_differs() {
let mut a = [0u8; 32];
let mut b = [0u8; 32];
pbkdf2::<HmacSha256>(b"pw", b"0123456789abcdef", 1_000, &mut a).unwrap();
pbkdf2::<HmacSha512>(b"pw", b"0123456789abcdef", 1_000, &mut b).unwrap();
assert_ne!(a, b);
}
#[test]
fn rejects_weak_parameters() {
let mut out = [0u8; 32];
assert!(pbkdf2::<HmacSha256>(b"pw", b"0123456789abcdef", 999, &mut out).is_err());
assert!(pbkdf2::<HmacSha256>(b"pw", b"short", 100_000, &mut out).is_err());
assert!(pbkdf2::<HmacSha256>(b"pw", b"0123456789abcdef", 1_000, &mut []).is_err());
}
#[test]
fn iteration_verdicts() {
assert_eq!(check_iterations(999), IterationVerdict::Unacceptable);
assert_eq!(check_iterations(1_000), IterationVerdict::Weak);
assert_eq!(check_iterations(600_000), IterationVerdict::Recommended);
}
}