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    ic_core::module::operational()?;
42    ensure!(
43        !matches!(check_iterations(iterations), IterationVerdict::Unacceptable),
44        InvalidParameter,
45        "pbkdf2 iterations below the SP 800-132 minimum of 1000"
46    );
47    ensure!(
48        salt.len() >= 16,
49        InvalidParameter,
50        "pbkdf2 salt must be >= 128 bits"
51    );
52    ensure!(!out.is_empty(), InvalidLength, "pbkdf2 output");
53    ensure!(
54        M::TAG_LEN <= MAX_TAG_LEN,
55        InvalidParameter,
56        "mac tag too wide"
57    );
58
59    let mut u = [0u8; MAX_TAG_LEN];
60    let mut acc = [0u8; MAX_TAG_LEN];
61
62    for (block_index, chunk) in out.chunks_mut(M::TAG_LEN).enumerate() {
63        let counter = (block_index as u32)
64            .checked_add(1)
65            .ok_or(ic_core::err!(CounterExhausted, "pbkdf2 block counter"))?;
66
67        // U_1 = PRF(password, salt || INT_BE(i))
68        let mut m = M::new(password)?;
69        m.update(salt);
70        m.update(&counter.to_be_bytes());
71        let t = m.finalize();
72        u[..M::TAG_LEN].copy_from_slice(t.as_ref());
73        acc[..M::TAG_LEN].copy_from_slice(t.as_ref());
74
75        // U_j = PRF(password, U_{j-1}); accumulate the XOR of every U_j.
76        for _ in 1..iterations {
77            let mut m = M::new(password)?;
78            m.update(&u[..M::TAG_LEN]);
79            let t = m.finalize();
80            u[..M::TAG_LEN].copy_from_slice(t.as_ref());
81            for j in 0..M::TAG_LEN {
82                acc[j] ^= u[j];
83            }
84        }
85        chunk.copy_from_slice(&acc[..chunk.len()]);
86    }
87
88    u.zeroize();
89    acc.zeroize();
90    Ok(())
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use ic_core::codec::hex;
97    use ic_mac::{HmacSha256, HmacSha512};
98
99    /// The defining property of PBKDF2: each output block is the XOR chain of
100    /// the PRF iterations. Reconstructing it from HMAC directly checks the
101    /// construction rather than pinning an opaque constant.
102    ///
103    /// RFC 6070's published vectors are HMAC-SHA1 only, which this library
104    /// deliberately does not implement.
105    #[test]
106    fn matches_the_prf_xor_chain() {
107        let salt = b"0123456789abcdef";
108        let mut out = [0u8; 32];
109        pbkdf2::<HmacSha256>(b"pw", salt, 1_000, &mut out).unwrap();
110
111        let mut first = HmacSha256::new(b"pw").unwrap();
112        first.update(salt);
113        first.update(&1u32.to_be_bytes());
114        let mut u = first.finalize();
115        let mut acc = u;
116        for _ in 1..1_000 {
117            u = HmacSha256::mac(b"pw", u.as_ref()).unwrap();
118            for j in 0..32 {
119                acc[j] ^= u[j];
120            }
121        }
122        assert_eq!(hex(&out), hex(acc.as_ref()));
123    }
124
125    #[test]
126    fn is_deterministic() {
127        let mut a = [0u8; 32];
128        let mut b = [0u8; 32];
129        pbkdf2::<HmacSha256>(b"passwd", b"salt-at-least-16", 1_000, &mut a).unwrap();
130        pbkdf2::<HmacSha256>(b"passwd", b"salt-at-least-16", 1_000, &mut b).unwrap();
131        assert_eq!(a, b);
132    }
133
134    #[test]
135    fn iteration_count_changes_the_key() {
136        let mut a = [0u8; 32];
137        let mut b = [0u8; 32];
138        pbkdf2::<HmacSha256>(b"pw", b"0123456789abcdef", 1_000, &mut a).unwrap();
139        pbkdf2::<HmacSha256>(b"pw", b"0123456789abcdef", 2_000, &mut b).unwrap();
140        assert_ne!(a, b);
141    }
142
143    #[test]
144    fn output_longer_than_one_block() {
145        let mut out = [0u8; 100];
146        pbkdf2::<HmacSha256>(b"pw", b"0123456789abcdef", 1_000, &mut out).unwrap();
147        // A repeated 32-byte pattern would mean the block counter never reaches
148        // the PRF.
149        assert_ne!(&out[..32], &out[32..64]);
150    }
151
152    #[test]
153    fn sha512_instantiation_differs() {
154        let mut a = [0u8; 32];
155        let mut b = [0u8; 32];
156        pbkdf2::<HmacSha256>(b"pw", b"0123456789abcdef", 1_000, &mut a).unwrap();
157        pbkdf2::<HmacSha512>(b"pw", b"0123456789abcdef", 1_000, &mut b).unwrap();
158        assert_ne!(a, b);
159    }
160
161    #[test]
162    fn rejects_weak_parameters() {
163        let mut out = [0u8; 32];
164        assert!(pbkdf2::<HmacSha256>(b"pw", b"0123456789abcdef", 999, &mut out).is_err());
165        assert!(pbkdf2::<HmacSha256>(b"pw", b"short", 100_000, &mut out).is_err());
166        assert!(pbkdf2::<HmacSha256>(b"pw", b"0123456789abcdef", 1_000, &mut []).is_err());
167    }
168
169    #[test]
170    fn iteration_verdicts() {
171        assert_eq!(check_iterations(999), IterationVerdict::Unacceptable);
172        assert_eq!(check_iterations(1_000), IterationVerdict::Weak);
173        assert_eq!(check_iterations(600_000), IterationVerdict::Recommended);
174    }
175}