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        let max_len: usize = if cfg!(target_pointer_width = "64") {
35            (u32::MAX as usize) * H::OUTPUT_SIZE
36        } else {
37            usize::MAX
38        };
39        assert!(N <= max_len, "PBKDF2 output length exceeds RFC 2898 limit",);
40    }
41
42    let hlen = H::OUTPUT_SIZE;
43    let block_count = (N + hlen - 1) / hlen;
44
45    let mut okm = [0u8; N];
46    let mut t = [0u8; MAX_HASH_BLOCK_SIZE];
47    let mut u = [0u8; MAX_HASH_BLOCK_SIZE];
48
49    for block in 1..=block_count {
50        let mut mac = Hmac::<H>::new(password);
51        mac.update(salt);
52        mac.update(&(block as u32).to_be_bytes());
53        let hash = mac.finalize();
54        let hash_bytes = hash.as_ref();
55        t[..hlen].copy_from_slice(hash_bytes);
56        u[..hlen].copy_from_slice(hash_bytes);
57
58        for _ in 2..=iterations {
59            let mut mac = Hmac::<H>::new(password);
60            mac.update(&u[..hlen]);
61            let hash = mac.finalize();
62            let hash_bytes = hash.as_ref();
63            u[..hlen].copy_from_slice(hash_bytes);
64            for i in 0..hlen {
65                t[i] ^= u[i];
66            }
67        }
68
69        let start = (block - 1) * hlen;
70        let end = usize::min(start + hlen, N);
71        okm[start..end].copy_from_slice(&t[..end - start]);
72    }
73
74    return okm;
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use crate::sha2::{Sha256, Sha512};
81
82    #[test]
83    #[should_panic(expected = "PBKDF2 iterations must be >= 1")]
84    fn pbkdf2_zero_iterations() {
85        derive::<Sha256, 32>(b"password", b"salt", 0);
86    }
87
88    #[test]
89    fn pbkdf2_zero_length_output() {
90        assert_eq!(derive::<Sha256, 0>(b"password", b"salt", 1), [] as [u8; 0]);
91    }
92
93    #[test]
94    fn pbkdf2_sha256_wycheproof() {
95        const MAX_OUTPUT: usize = 128;
96
97        let data: serde_json::Value = serde_json::from_str(include_str!(
98            "../testdata/wycheproof/testvectors_v1/pbkdf2_hmacsha256_test.json"
99        ))
100        .unwrap();
101        let mut tested = 0u64;
102        for group in data["testGroups"].as_array().unwrap() {
103            for test in group["tests"].as_array().unwrap() {
104                let password_hex = test["password"].as_str().unwrap();
105                let salt_hex = test["salt"].as_str().unwrap();
106                let iteration_count = test["iterationCount"].as_u64().unwrap() as u32;
107                let dk_len = test["dkLen"].as_u64().unwrap() as usize;
108                let expected_dk_hex = test["dk"].as_str().unwrap();
109
110                let password = hex::decode(password_hex).unwrap();
111                let salt = hex::decode(salt_hex).unwrap();
112
113                let dk = derive::<Sha256, MAX_OUTPUT>(&password, &salt, iteration_count);
114                let dk_hex = hex::encode(&dk[..dk_len]);
115                assert_eq!(
116                    dk_hex, expected_dk_hex,
117                    "wycheproof PBKDF2-SHA-256 tcId={} dkLen={}",
118                    test["tcId"], dk_len
119                );
120                tested += 1;
121            }
122        }
123        assert!(tested > 0, "no PBKDF2-SHA-256 wycheproof tests were run");
124    }
125
126    #[test]
127    fn pbkdf2_sha512_wycheproof() {
128        const MAX_OUTPUT: usize = 128;
129
130        let data: serde_json::Value = serde_json::from_str(include_str!(
131            "../testdata/wycheproof/testvectors_v1/pbkdf2_hmacsha512_test.json"
132        ))
133        .unwrap();
134        let mut tested = 0u64;
135        for group in data["testGroups"].as_array().unwrap() {
136            for test in group["tests"].as_array().unwrap() {
137                let password_hex = test["password"].as_str().unwrap();
138                let salt_hex = test["salt"].as_str().unwrap();
139                let iteration_count = test["iterationCount"].as_u64().unwrap() as u32;
140                let dk_len = test["dkLen"].as_u64().unwrap() as usize;
141                let expected_dk_hex = test["dk"].as_str().unwrap();
142
143                let password = hex::decode(password_hex).unwrap();
144                let salt = hex::decode(salt_hex).unwrap();
145
146                let dk = derive::<Sha512, MAX_OUTPUT>(&password, &salt, iteration_count);
147                let dk_hex = hex::encode(&dk[..dk_len]);
148                assert_eq!(
149                    dk_hex, expected_dk_hex,
150                    "wycheproof PBKDF2-SHA-512 tcId={} dkLen={}",
151                    test["tcId"], dk_len
152                );
153                tested += 1;
154            }
155        }
156        assert!(tested > 0, "no PBKDF2-SHA-512 wycheproof tests were run");
157    }
158}