Skip to main content

oxicrypto_kdf/
pbkdf2_kdf.rs

1#![forbid(unsafe_code)]
2
3//! PBKDF2 password-based key derivation for the OxiCrypto stack.
4//!
5//! Provides PBKDF2-HMAC-SHA-256 and PBKDF2-HMAC-SHA-512 via the
6//! `pbkdf2` crate (RustCrypto, digest 0.11 chain).
7
8use oxicrypto_core::{CryptoError, Kdf, PasswordHash as PasswordHashTrait, PasswordHashParams};
9use sha2::{Sha256, Sha512};
10
11// ---------------------------------------------------------------------------
12// Low-level standalone functions (existing API — preserved for compatibility)
13// ---------------------------------------------------------------------------
14
15/// PBKDF2-HMAC-SHA-256 key derivation.
16///
17/// # Arguments
18/// - `password`   — secret password bytes
19/// - `salt`       — random salt (recommended ≥ 16 bytes)
20/// - `iterations` — NIST SP 800-132 recommends ≥ 310_000 for interactive logins
21/// - `out`        — output buffer (any length)
22#[must_use = "PBKDF2 derive result must be checked"]
23pub fn pbkdf2_sha256(
24    password: &[u8],
25    salt: &[u8],
26    iterations: u32,
27    out: &mut [u8],
28) -> Result<(), CryptoError> {
29    if out.is_empty() {
30        return Err(CryptoError::BadInput);
31    }
32    if iterations == 0 {
33        return Err(CryptoError::BadInput);
34    }
35    pbkdf2::pbkdf2_hmac::<Sha256>(password, salt, iterations, out);
36    Ok(())
37}
38
39/// PBKDF2-HMAC-SHA-512 key derivation.
40#[must_use = "PBKDF2 derive result must be checked"]
41pub fn pbkdf2_sha512(
42    password: &[u8],
43    salt: &[u8],
44    iterations: u32,
45    out: &mut [u8],
46) -> Result<(), CryptoError> {
47    if out.is_empty() {
48        return Err(CryptoError::BadInput);
49    }
50    if iterations == 0 {
51        return Err(CryptoError::BadInput);
52    }
53    pbkdf2::pbkdf2_hmac::<Sha512>(password, salt, iterations, out);
54    Ok(())
55}
56
57// ---------------------------------------------------------------------------
58// Pbkdf2Params — implements `PasswordHashParams` for the core trait surface
59// ---------------------------------------------------------------------------
60
61/// Cost parameters for PBKDF2.
62///
63/// PBKDF2 has only a time cost (iteration count); there is no memory cost or
64/// parallelism parameter.
65#[derive(Debug, Clone, Copy)]
66pub struct Pbkdf2Params {
67    /// Number of PBKDF2 iterations.
68    pub iterations: u32,
69}
70
71impl PasswordHashParams for Pbkdf2Params {
72    fn memory_cost(&self) -> Option<u32> {
73        None
74    }
75
76    fn time_cost(&self) -> Option<u32> {
77        Some(self.iterations)
78    }
79
80    fn parallelism(&self) -> Option<u32> {
81        None
82    }
83}
84
85// ---------------------------------------------------------------------------
86// Pbkdf2Sha256Hasher — PasswordHash + Kdf + presets
87// ---------------------------------------------------------------------------
88
89/// PBKDF2-HMAC-SHA-256 password hasher.
90///
91/// Implements both [`PasswordHash`](oxicrypto_core::PasswordHash) (for use
92/// with [`crate::verify_password`]) and [`Kdf`] (for use as a standard key
93/// derivation function).
94///
95/// # Design note — `params` argument is ignored
96/// The [`PasswordHash::hash_password`](oxicrypto_core::PasswordHash::hash_password)
97/// trait method accepts a `params: &dyn PasswordHashParams` argument, but this
98/// implementation ignores it and uses `self.iterations` instead. Callers that
99/// need different iteration counts should construct a new `Pbkdf2Sha256Hasher`
100/// with the desired count rather than passing a different `PasswordHashParams` object.
101#[derive(Debug, Clone, Copy)]
102pub struct Pbkdf2Sha256Hasher {
103    /// Number of PBKDF2 iterations.
104    pub iterations: u32,
105}
106
107impl Pbkdf2Sha256Hasher {
108    /// Create a new hasher with an explicit iteration count.
109    #[must_use]
110    pub fn new(iterations: u32) -> Self {
111        Self { iterations }
112    }
113
114    /// Interactive login preset — 310,000 iterations (NIST SP 800-132 minimum).
115    #[must_use]
116    pub fn interactive() -> Self {
117        Self {
118            iterations: 310_000,
119        }
120    }
121
122    /// Moderate preset — 600,000 iterations (OWASP 2024 recommendation).
123    #[must_use]
124    pub fn moderate() -> Self {
125        Self {
126            iterations: 600_000,
127        }
128    }
129
130    /// Sensitive (high-security) preset — 1,000,000 iterations.
131    #[must_use]
132    pub fn sensitive() -> Self {
133        Self {
134            iterations: 1_000_000,
135        }
136    }
137
138    /// Return the cost parameters as a [`Pbkdf2Params`].
139    #[must_use]
140    pub fn params(&self) -> Pbkdf2Params {
141        Pbkdf2Params {
142            iterations: self.iterations,
143        }
144    }
145}
146
147impl PasswordHashTrait for Pbkdf2Sha256Hasher {
148    fn name(&self) -> &'static str {
149        "pbkdf2-sha256"
150    }
151
152    fn hash_password(
153        &self,
154        password: &[u8],
155        salt: &[u8],
156        _params: &dyn PasswordHashParams,
157        out: &mut [u8],
158    ) -> Result<(), CryptoError> {
159        pbkdf2_sha256(password, salt, self.iterations, out)
160    }
161}
162
163impl Kdf for Pbkdf2Sha256Hasher {
164    fn name(&self) -> &'static str {
165        "PBKDF2-SHA-256"
166    }
167
168    /// Derive key material from a password (IKM) and salt.
169    ///
170    /// The `info` argument is not used by PBKDF2 (it has no native concept of
171    /// application-specific context); pass an empty slice.
172    fn derive(
173        &self,
174        ikm: &[u8],
175        salt: &[u8],
176        _info: &[u8],
177        okm_out: &mut [u8],
178    ) -> Result<(), CryptoError> {
179        pbkdf2_sha256(ikm, salt, self.iterations, okm_out)
180    }
181}
182
183// ---------------------------------------------------------------------------
184// Pbkdf2Sha512Hasher — PasswordHash + Kdf + presets
185// ---------------------------------------------------------------------------
186
187/// PBKDF2-HMAC-SHA-512 password hasher.
188///
189/// Implements both [`PasswordHash`](oxicrypto_core::PasswordHash) and [`Kdf`].
190///
191/// # Design note — `params` argument is ignored
192/// The [`PasswordHash::hash_password`](oxicrypto_core::PasswordHash::hash_password)
193/// trait method accepts a `params: &dyn PasswordHashParams` argument, but this
194/// implementation ignores it and uses `self.iterations` instead. Callers that
195/// need different iteration counts should construct a new `Pbkdf2Sha512Hasher`
196/// with the desired count rather than passing a different `PasswordHashParams` object.
197#[derive(Debug, Clone, Copy)]
198pub struct Pbkdf2Sha512Hasher {
199    /// Number of PBKDF2 iterations.
200    pub iterations: u32,
201}
202
203impl Pbkdf2Sha512Hasher {
204    /// Create a new hasher with an explicit iteration count.
205    #[must_use]
206    pub fn new(iterations: u32) -> Self {
207        Self { iterations }
208    }
209
210    /// Interactive login preset — 210,000 iterations (approx. equivalent CPU
211    /// cost to 310,000 SHA-256 rounds, per OWASP guidance).
212    #[must_use]
213    pub fn interactive() -> Self {
214        Self {
215            iterations: 210_000,
216        }
217    }
218
219    /// Moderate preset — 400,000 iterations.
220    #[must_use]
221    pub fn moderate() -> Self {
222        Self {
223            iterations: 400_000,
224        }
225    }
226
227    /// Sensitive (high-security) preset — 700,000 iterations.
228    #[must_use]
229    pub fn sensitive() -> Self {
230        Self {
231            iterations: 700_000,
232        }
233    }
234
235    /// Return the cost parameters as a [`Pbkdf2Params`].
236    #[must_use]
237    pub fn params(&self) -> Pbkdf2Params {
238        Pbkdf2Params {
239            iterations: self.iterations,
240        }
241    }
242}
243
244impl PasswordHashTrait for Pbkdf2Sha512Hasher {
245    fn name(&self) -> &'static str {
246        "pbkdf2-sha512"
247    }
248
249    fn hash_password(
250        &self,
251        password: &[u8],
252        salt: &[u8],
253        _params: &dyn PasswordHashParams,
254        out: &mut [u8],
255    ) -> Result<(), CryptoError> {
256        pbkdf2_sha512(password, salt, self.iterations, out)
257    }
258}
259
260impl Kdf for Pbkdf2Sha512Hasher {
261    fn name(&self) -> &'static str {
262        "PBKDF2-SHA-512"
263    }
264
265    /// Derive key material from a password (IKM) and salt.
266    ///
267    /// The `info` argument is not used by PBKDF2; pass an empty slice.
268    fn derive(
269        &self,
270        ikm: &[u8],
271        salt: &[u8],
272        _info: &[u8],
273        okm_out: &mut [u8],
274    ) -> Result<(), CryptoError> {
275        pbkdf2_sha512(ikm, salt, self.iterations, okm_out)
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    const SALT: &[u8] = b"test-salt-16byte";
284    const ITERS: u32 = 1_000; // Fast for tests
285
286    #[test]
287    fn pbkdf2_sha256_deterministic() {
288        let mut out1 = [0u8; 32];
289        let mut out2 = [0u8; 32];
290        pbkdf2_sha256(b"password", SALT, ITERS, &mut out1).expect("derive 1");
291        pbkdf2_sha256(b"password", SALT, ITERS, &mut out2).expect("derive 2");
292        assert_eq!(out1, out2);
293        assert_ne!(out1, [0u8; 32]);
294    }
295
296    #[test]
297    fn pbkdf2_sha512_deterministic() {
298        let mut out1 = [0u8; 64];
299        let mut out2 = [0u8; 64];
300        pbkdf2_sha512(b"password", SALT, ITERS, &mut out1).expect("derive 1");
301        pbkdf2_sha512(b"password", SALT, ITERS, &mut out2).expect("derive 2");
302        assert_eq!(out1, out2);
303        assert_ne!(out1, [0u8; 64]);
304    }
305
306    #[test]
307    fn pbkdf2_sha256_zero_iterations_errors() {
308        let mut out = [0u8; 32];
309        assert_eq!(
310            pbkdf2_sha256(b"pw", SALT, 0, &mut out),
311            Err(CryptoError::BadInput)
312        );
313    }
314
315    #[test]
316    fn pbkdf2_sha256_empty_output_errors() {
317        assert_eq!(
318            pbkdf2_sha256(b"pw", SALT, ITERS, &mut []),
319            Err(CryptoError::BadInput)
320        );
321    }
322
323    // ── PasswordHash trait ───────────────────────────────────────────────────
324
325    #[test]
326    fn pbkdf2_sha256_hasher_hash_password_deterministic() {
327        let hasher = Pbkdf2Sha256Hasher::new(ITERS);
328        let params = hasher.params();
329        let mut out1 = [0u8; 32];
330        let mut out2 = [0u8; 32];
331        hasher
332            .hash_password(b"password", SALT, &params, &mut out1)
333            .expect("hash 1");
334        hasher
335            .hash_password(b"password", SALT, &params, &mut out2)
336            .expect("hash 2");
337        assert_eq!(out1, out2);
338        assert_ne!(out1, [0u8; 32]);
339    }
340
341    #[test]
342    fn pbkdf2_sha512_hasher_hash_password_deterministic() {
343        let hasher = Pbkdf2Sha512Hasher::new(ITERS);
344        let params = hasher.params();
345        let mut out1 = [0u8; 64];
346        let mut out2 = [0u8; 64];
347        hasher
348            .hash_password(b"password", SALT, &params, &mut out1)
349            .expect("hash 1");
350        hasher
351            .hash_password(b"password", SALT, &params, &mut out2)
352            .expect("hash 2");
353        assert_eq!(out1, out2);
354        assert_ne!(out1, [0u8; 64]);
355    }
356
357    // ── Kdf trait ───────────────────────────────────────────────────────────
358
359    #[test]
360    fn pbkdf2_sha256_kdf_matches_standalone() {
361        let hasher = Pbkdf2Sha256Hasher::new(ITERS);
362        let mut from_kdf = [0u8; 32];
363        let mut from_fn = [0u8; 32];
364        hasher
365            .derive(b"key", SALT, b"", &mut from_kdf)
366            .expect("kdf derive");
367        pbkdf2_sha256(b"key", SALT, ITERS, &mut from_fn).expect("fn derive");
368        assert_eq!(from_kdf, from_fn, "Kdf::derive must match standalone fn");
369    }
370
371    #[test]
372    fn pbkdf2_sha512_kdf_matches_standalone() {
373        let hasher = Pbkdf2Sha512Hasher::new(ITERS);
374        let mut from_kdf = [0u8; 64];
375        let mut from_fn = [0u8; 64];
376        hasher
377            .derive(b"key", SALT, b"", &mut from_kdf)
378            .expect("kdf derive");
379        pbkdf2_sha512(b"key", SALT, ITERS, &mut from_fn).expect("fn derive");
380        assert_eq!(from_kdf, from_fn, "Kdf::derive must match standalone fn");
381    }
382
383    // ── Presets ─────────────────────────────────────────────────────────────
384
385    #[test]
386    fn pbkdf2_sha256_preset_cost_ordering() {
387        let interactive = Pbkdf2Sha256Hasher::interactive();
388        let moderate = Pbkdf2Sha256Hasher::moderate();
389        let sensitive = Pbkdf2Sha256Hasher::sensitive();
390        assert!(sensitive.iterations > moderate.iterations);
391        assert!(moderate.iterations > interactive.iterations);
392    }
393
394    #[test]
395    fn pbkdf2_sha512_preset_cost_ordering() {
396        let interactive = Pbkdf2Sha512Hasher::interactive();
397        let moderate = Pbkdf2Sha512Hasher::moderate();
398        let sensitive = Pbkdf2Sha512Hasher::sensitive();
399        assert!(sensitive.iterations > moderate.iterations);
400        assert!(moderate.iterations > interactive.iterations);
401    }
402
403    #[test]
404    fn pbkdf2_params_trait_impl() {
405        let params = Pbkdf2Params { iterations: 42 };
406        assert_eq!(params.memory_cost(), None);
407        assert_eq!(params.time_cost(), Some(42));
408        assert_eq!(params.parallelism(), None);
409    }
410
411    #[test]
412    fn hasher_names() {
413        assert_eq!(
414            <Pbkdf2Sha256Hasher as oxicrypto_core::PasswordHash>::name(&Pbkdf2Sha256Hasher::new(1)),
415            "pbkdf2-sha256"
416        );
417        assert_eq!(
418            <Pbkdf2Sha512Hasher as oxicrypto_core::PasswordHash>::name(&Pbkdf2Sha512Hasher::new(1)),
419            "pbkdf2-sha512"
420        );
421    }
422}