Skip to main content

ic_kdf/
pbkdf2.rs

1//! SP 800-132 / RFC 8018 PBKDF2.
2
3use ic_core::traits::Mac;
4use ic_core::{ensure, Result, Zeroize};
5
6/// The largest MAC output any supported instantiation produces.
7const MAX_TAG_LEN: usize = 64;
8
9/// Advice on an iteration count, for agents choosing parameters.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum IterationVerdict {
12    /// At or above the modern recommendation.
13    Recommended,
14    /// Above the SP 800-132 floor but below current practice.
15    Weak,
16    /// Below the SP 800-132 minimum of 1 000; rejected outright.
17    Unacceptable,
18}
19
20/// Classify an iteration count without performing any derivation.
21///
22/// Exposed as an ontology precondition so an agent can validate parameters
23/// before spending the work, and so a reviewer can see the threshold the
24/// library actually enforces.
25pub const fn check_iterations(iterations: u32) -> IterationVerdict {
26    if iterations < 1_000 {
27        IterationVerdict::Unacceptable
28    } else if iterations < crate::PBKDF2_MIN_RECOMMENDED_ITERATIONS {
29        IterationVerdict::Weak
30    } else {
31        IterationVerdict::Recommended
32    }
33}
34
35/// Derive `out.len()` bytes from `password` and `salt`.
36///
37/// Rejects iteration counts below the SP 800-132 minimum of 1 000 and salts
38/// shorter than the 128-bit minimum, so a misconfigured caller fails loudly
39/// rather than producing a weak key.
40pub fn pbkdf2<M: Mac>(password: &[u8], salt: &[u8], iterations: u32, out: &mut [u8]) -> Result<()> {
41    ensure!(
42        !matches!(check_iterations(iterations), IterationVerdict::Unacceptable),
43        InvalidParameter,
44        "pbkdf2 iterations below the SP 800-132 minimum of 1000"
45    );
46    ensure!(
47        salt.len() >= 16,
48        InvalidParameter,
49        "pbkdf2 salt must be >= 128 bits"
50    );
51    ensure!(!out.is_empty(), InvalidLength, "pbkdf2 output");
52    ensure!(
53        M::TAG_LEN <= MAX_TAG_LEN,
54        InvalidParameter,
55        "mac tag too wide"
56    );
57
58    let mut u = [0u8; MAX_TAG_LEN];
59    let mut acc = [0u8; MAX_TAG_LEN];
60
61    for (block_index, chunk) in out.chunks_mut(M::TAG_LEN).enumerate() {
62        let counter = (block_index as u32)
63            .checked_add(1)
64            .ok_or(ic_core::err!(CounterExhausted, "pbkdf2 block counter"))?;
65
66        // U_1 = PRF(password, salt || INT_BE(i))
67        let mut m = M::new(password)?;
68        m.update(salt);
69        m.update(&counter.to_be_bytes());
70        let t = m.finalize();
71        u[..M::TAG_LEN].copy_from_slice(t.as_ref());
72        acc[..M::TAG_LEN].copy_from_slice(t.as_ref());
73
74        // U_j = PRF(password, U_{j-1}); accumulate the XOR of every U_j.
75        for _ in 1..iterations {
76            let mut m = M::new(password)?;
77            m.update(&u[..M::TAG_LEN]);
78            let t = m.finalize();
79            u[..M::TAG_LEN].copy_from_slice(t.as_ref());
80            for j in 0..M::TAG_LEN {
81                acc[j] ^= u[j];
82            }
83        }
84        chunk.copy_from_slice(&acc[..chunk.len()]);
85    }
86
87    u.zeroize();
88    acc.zeroize();
89    Ok(())
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use ic_core::codec::hex;
96    use ic_mac::{HmacSha256, HmacSha512};
97
98    /// The defining property of PBKDF2: each output block is the XOR chain of
99    /// the PRF iterations. Reconstructing it from HMAC directly checks the
100    /// construction rather than pinning an opaque constant.
101    ///
102    /// RFC 6070's published vectors are HMAC-SHA1 only, which this library
103    /// deliberately does not implement.
104    #[test]
105    fn matches_the_prf_xor_chain() {
106        let salt = b"0123456789abcdef";
107        let mut out = [0u8; 32];
108        pbkdf2::<HmacSha256>(b"pw", salt, 1_000, &mut out).unwrap();
109
110        let mut first = HmacSha256::new(b"pw").unwrap();
111        first.update(salt);
112        first.update(&1u32.to_be_bytes());
113        let mut u = first.finalize();
114        let mut acc = u;
115        for _ in 1..1_000 {
116            u = HmacSha256::mac(b"pw", u.as_ref()).unwrap();
117            for j in 0..32 {
118                acc[j] ^= u[j];
119            }
120        }
121        assert_eq!(hex(&out), hex(acc.as_ref()));
122    }
123
124    #[test]
125    fn is_deterministic() {
126        let mut a = [0u8; 32];
127        let mut b = [0u8; 32];
128        pbkdf2::<HmacSha256>(b"passwd", b"salt-at-least-16", 1_000, &mut a).unwrap();
129        pbkdf2::<HmacSha256>(b"passwd", b"salt-at-least-16", 1_000, &mut b).unwrap();
130        assert_eq!(a, b);
131    }
132
133    #[test]
134    fn iteration_count_changes_the_key() {
135        let mut a = [0u8; 32];
136        let mut b = [0u8; 32];
137        pbkdf2::<HmacSha256>(b"pw", b"0123456789abcdef", 1_000, &mut a).unwrap();
138        pbkdf2::<HmacSha256>(b"pw", b"0123456789abcdef", 2_000, &mut b).unwrap();
139        assert_ne!(a, b);
140    }
141
142    #[test]
143    fn output_longer_than_one_block() {
144        let mut out = [0u8; 100];
145        pbkdf2::<HmacSha256>(b"pw", b"0123456789abcdef", 1_000, &mut out).unwrap();
146        // A repeated 32-byte pattern would mean the block counter never reaches
147        // the PRF.
148        assert_ne!(&out[..32], &out[32..64]);
149    }
150
151    #[test]
152    fn sha512_instantiation_differs() {
153        let mut a = [0u8; 32];
154        let mut b = [0u8; 32];
155        pbkdf2::<HmacSha256>(b"pw", b"0123456789abcdef", 1_000, &mut a).unwrap();
156        pbkdf2::<HmacSha512>(b"pw", b"0123456789abcdef", 1_000, &mut b).unwrap();
157        assert_ne!(a, b);
158    }
159
160    #[test]
161    fn rejects_weak_parameters() {
162        let mut out = [0u8; 32];
163        assert!(pbkdf2::<HmacSha256>(b"pw", b"0123456789abcdef", 999, &mut out).is_err());
164        assert!(pbkdf2::<HmacSha256>(b"pw", b"short", 100_000, &mut out).is_err());
165        assert!(pbkdf2::<HmacSha256>(b"pw", b"0123456789abcdef", 1_000, &mut []).is_err());
166    }
167
168    #[test]
169    fn iteration_verdicts() {
170        assert_eq!(check_iterations(999), IterationVerdict::Unacceptable);
171        assert_eq!(check_iterations(1_000), IterationVerdict::Weak);
172        assert_eq!(check_iterations(600_000), IterationVerdict::Recommended);
173    }
174}