Skip to main content

crypto/
pbkdf2.rs

1//! PBKDF2 (Password-Based Key Derivation Function 2) as defined in RFC 2898.
2use crate::{Hasher, MAX_HASH_BLOCK_SIZE, hmac::Hmac};
3
4/// Derives a key using PBKDF2-HMAC with the given hash function.
5///
6/// PBKDF2 applies the HMAC-based PRF repeatedly (`iterations` times) to
7/// produce a derived key of `N` bytes.
8///
9/// ⚠️ **PBKDF2 is not memory-hard**, making it vulnerable to GPU/ASIC-based
10/// brute-force attacks. For password hashing, prefer [`crate::argon2::Argon2id`]
11/// unless PBKDF2 is required for legacy compatibility or specific protocol
12/// standards.
13///
14/// # Example
15///
16/// ```ignore
17/// use crypto::pbkdf2;
18/// use crypto::sha2::Sha256;
19///
20/// let key: [u8; 32] = pbkdf2::derive::<Sha256, 32>(
21///     b"password",
22///     b"salt",
23///     4096,
24/// );
25/// ```
26///
27/// # Panics
28///
29/// `derive` panics if `iterations == 0` (iterations must be >= 1)
30/// or `N > (2^32 - 1) * H::OUTPUT_SIZE` (output length exceeds RFC 2898 limit).
31pub fn derive<H: Hasher, const N: usize>(password: &[u8], salt: &[u8], iterations: u32) -> [u8; N] {
32    assert!(iterations != 0, "PBKDF2 iterations must be >= 1");
33    const {
34        assert!(
35            N <= (u32::MAX as usize) * H::OUTPUT_SIZE,
36            "PBKDF2 output length exceeds RFC 2898 limit",
37        );
38    }
39
40    let hlen = H::OUTPUT_SIZE;
41    let block_count = (N + hlen - 1) / hlen;
42
43    let mut okm = [0u8; N];
44    let mut t = [0u8; MAX_HASH_BLOCK_SIZE];
45    let mut u = [0u8; MAX_HASH_BLOCK_SIZE];
46
47    for block in 1..=block_count {
48        let mut mac = Hmac::<H>::new(password);
49        mac.update(salt);
50        mac.update(&(block as u32).to_be_bytes());
51        let hash = mac.finalize();
52        let hash_bytes = hash.as_ref();
53        t[..hlen].copy_from_slice(hash_bytes);
54        u[..hlen].copy_from_slice(hash_bytes);
55
56        for _ in 2..=iterations {
57            let mut mac = Hmac::<H>::new(password);
58            mac.update(&u[..hlen]);
59            let hash = mac.finalize();
60            let hash_bytes = hash.as_ref();
61            u[..hlen].copy_from_slice(hash_bytes);
62            for i in 0..hlen {
63                t[i] ^= u[i];
64            }
65        }
66
67        let start = (block - 1) * hlen;
68        let end = usize::min(start + hlen, N);
69        okm[start..end].copy_from_slice(&t[..end - start]);
70    }
71
72    return okm;
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use crate::sha2::{Sha256, Sha512};
79
80    #[test]
81    #[should_panic(expected = "PBKDF2 iterations must be >= 1")]
82    fn pbkdf2_zero_iterations() {
83        derive::<Sha256, 32>(b"password", b"salt", 0);
84    }
85
86    #[test]
87    fn pbkdf2_zero_length_output() {
88        assert_eq!(derive::<Sha256, 0>(b"password", b"salt", 1), [] as [u8; 0]);
89    }
90
91    #[test]
92    fn pbkdf2_sha256_wycheproof() {
93        const MAX_OUTPUT: usize = 128;
94
95        let data: serde_json::Value = serde_json::from_str(include_str!(
96            "../testdata/wycheproof/testvectors_v1/pbkdf2_hmacsha256_test.json"
97        ))
98        .unwrap();
99        let mut tested = 0u64;
100        for group in data["testGroups"].as_array().unwrap() {
101            for test in group["tests"].as_array().unwrap() {
102                let password_hex = test["password"].as_str().unwrap();
103                let salt_hex = test["salt"].as_str().unwrap();
104                let iteration_count = test["iterationCount"].as_u64().unwrap() as u32;
105                let dk_len = test["dkLen"].as_u64().unwrap() as usize;
106                let expected_dk_hex = test["dk"].as_str().unwrap();
107
108                let password = hex::decode(password_hex).unwrap();
109                let salt = hex::decode(salt_hex).unwrap();
110
111                let dk = derive::<Sha256, MAX_OUTPUT>(&password, &salt, iteration_count);
112                let dk_hex = hex::encode(&dk[..dk_len]);
113                assert_eq!(
114                    dk_hex, expected_dk_hex,
115                    "wycheproof PBKDF2-SHA-256 tcId={} dkLen={}",
116                    test["tcId"], dk_len
117                );
118                tested += 1;
119            }
120        }
121        assert!(tested > 0, "no PBKDF2-SHA-256 wycheproof tests were run");
122    }
123
124    #[test]
125    fn pbkdf2_sha512_wycheproof() {
126        const MAX_OUTPUT: usize = 128;
127
128        let data: serde_json::Value = serde_json::from_str(include_str!(
129            "../testdata/wycheproof/testvectors_v1/pbkdf2_hmacsha512_test.json"
130        ))
131        .unwrap();
132        let mut tested = 0u64;
133        for group in data["testGroups"].as_array().unwrap() {
134            for test in group["tests"].as_array().unwrap() {
135                let password_hex = test["password"].as_str().unwrap();
136                let salt_hex = test["salt"].as_str().unwrap();
137                let iteration_count = test["iterationCount"].as_u64().unwrap() as u32;
138                let dk_len = test["dkLen"].as_u64().unwrap() as usize;
139                let expected_dk_hex = test["dk"].as_str().unwrap();
140
141                let password = hex::decode(password_hex).unwrap();
142                let salt = hex::decode(salt_hex).unwrap();
143
144                let dk = derive::<Sha512, MAX_OUTPUT>(&password, &salt, iteration_count);
145                let dk_hex = hex::encode(&dk[..dk_len]);
146                assert_eq!(
147                    dk_hex, expected_dk_hex,
148                    "wycheproof PBKDF2-SHA-512 tcId={} dkLen={}",
149                    test["tcId"], dk_len
150                );
151                tested += 1;
152            }
153        }
154        assert!(tested > 0, "no PBKDF2-SHA-512 wycheproof tests were run");
155    }
156}