Skip to main content

dcrypt_algorithms/kdf/pbkdf2/
mod.rs

1//! Password-Based Key Derivation Function 2 (PBKDF2)
2//!
3//! This module implements PBKDF2 as specified in RFC 8018.
4//! PBKDF2 applies a pseudorandom function (such as HMAC) to the input password
5//! along with a salt value and repeats the process many times to produce a
6//! derived key, which can then be used as a cryptographic key in subsequent operations.
7//!
8//! # Timing boundary
9//!
10//! Password length, salt length, iteration count, and output length are public
11//! metadata to this slice-based API. They affect allocation, copying, hash
12//! absorption, and loop counts. Callers that need to conceal password length
13//! must encode or pad passwords to a fixed public length before calling PBKDF2.
14//! Timing regressions for secret password bytes therefore hold all of this
15//! metadata fixed; they are not a whole-operation constant-time guarantee
16//! across different public parameter values.
17
18use crate::error::{validate, Error, Result};
19use crate::hash::HashFunction;
20#[cfg(feature = "std")]
21use crate::kdf::common::constant_time_eq;
22use crate::kdf::{KdfAlgorithm, KdfOperation, KeyDerivationFunction, ParamProvider, SecurityLevel};
23#[cfg(feature = "std")]
24use crate::kdf::{PasswordHash, PasswordHashFunction};
25use crate::mac::hmac::Hmac;
26use crate::types::salt::Pbkdf2Compatible;
27use crate::types::Salt;
28#[cfg(feature = "std")]
29use crate::types::{ByteSerializable, SecretBytes};
30
31// Import security types
32use dcrypt_common::security::SecretVec;
33
34// Conditional imports based on features
35#[cfg(feature = "std")]
36use std::collections::BTreeMap;
37#[cfg(feature = "std")]
38use std::string::String;
39#[cfg(feature = "std")]
40use std::time::{Duration, Instant};
41
42#[cfg(all(feature = "alloc", not(feature = "std")))]
43use alloc::string::String;
44
45use core::marker::PhantomData;
46use dcrypt_internal::random::{CryptoRng, RngCore};
47use dcrypt_internal::zeroing::{
48    boxed_bytes_zeroed, Zeroize, ZeroizeOnDrop, Zeroizing, ZeroizingBytes,
49};
50
51/// Type-level constants for PBKDF2 algorithm
52pub enum Pbkdf2Algorithm<H: HashFunction> {
53    /// Phantom field for the hash function
54    _Hash(PhantomData<H>),
55}
56
57impl<H: HashFunction> KdfAlgorithm for Pbkdf2Algorithm<H> {
58    const MIN_SALT_SIZE: usize = 16;
59    const DEFAULT_OUTPUT_SIZE: usize = 32;
60    const ALGORITHM_ID: &'static str = "PBKDF2";
61
62    fn name() -> String {
63        format!("{}-{}", Self::ALGORITHM_ID, H::name())
64    }
65
66    fn security_level() -> SecurityLevel {
67        // PBKDF2 security depends on the underlying hash
68        match H::output_size() * 8 {
69            bits if bits >= 512 => SecurityLevel::L128, // Conservative estimate
70            bits if bits >= 384 => SecurityLevel::L128,
71            bits if bits >= 256 => SecurityLevel::L128,
72            bits => SecurityLevel::Custom(bits as u32 / 2),
73        }
74    }
75}
76
77/// Parameters for PBKDF2
78#[derive(Clone, Debug)]
79pub struct Pbkdf2Params<const S: usize = 16> {
80    /// Salt value
81    pub salt: Salt<S>,
82
83    /// Number of iterations
84    pub iterations: u32,
85
86    /// Length of derived key in bytes
87    pub key_length: usize,
88}
89
90impl<const S: usize> Zeroize for Pbkdf2Params<S> {
91    fn zeroize(&mut self) {
92        self.salt.zeroize();
93        self.iterations.zeroize();
94        self.key_length.zeroize();
95    }
96}
97
98impl<const S: usize> Default for Pbkdf2Params<S>
99where
100    Salt<S>: Pbkdf2Compatible,
101{
102    fn default() -> Self {
103        Self {
104            salt: Salt::<S>::zeroed(), // Will be filled with random data during initialization
105            iterations: 600_000,       // OWASP recommended minimum as of 2023
106            key_length: 32,            // 256 bits
107        }
108    }
109}
110
111/// PBKDF2 implementation using any HMAC-based PRF
112///
113/// PBKDF2 can be used with any pseudorandom function, but this implementation
114/// uses HMAC with a configurable hash function.
115#[derive(Clone)]
116pub struct Pbkdf2<H: HashFunction + Clone, const S: usize = 16> {
117    /// The hash function type
118    _hash_type: PhantomData<H>,
119
120    /// PBKDF2 parameters
121    params: Pbkdf2Params<S>,
122}
123
124impl<H: HashFunction + Clone, const S: usize> Zeroize for Pbkdf2<H, S> {
125    fn zeroize(&mut self) {
126        self.params.zeroize();
127    }
128}
129
130impl<H: HashFunction + Clone, const S: usize> Drop for Pbkdf2<H, S> {
131    fn drop(&mut self) {
132        self.zeroize();
133    }
134}
135
136impl<H: HashFunction + Clone, const S: usize> ZeroizeOnDrop for Pbkdf2<H, S> {}
137
138/// PBKDF2 builder implementation
139pub struct Pbkdf2Builder<'a, H: HashFunction + Clone, const S: usize = 16> {
140    kdf: &'a Pbkdf2<H, S>,
141    ikm: Option<&'a [u8]>,
142    salt: Option<&'a [u8]>,
143    iterations: u32,
144    length: usize,
145}
146
147// FIXED: Elided lifetime in impl block
148impl<H: HashFunction + Clone, const S: usize> Pbkdf2Builder<'_, H, S> {
149    /// Set the number of iterations
150    pub fn with_iterations(mut self, iterations: u32) -> Self {
151        self.iterations = iterations;
152        self
153    }
154}
155
156impl<'a, H: HashFunction + Clone, const S: usize> KdfOperation<'a, Pbkdf2Algorithm<H>>
157    for Pbkdf2Builder<'a, H, S>
158where
159    Salt<S>: Pbkdf2Compatible,
160{
161    fn with_ikm(mut self, ikm: &'a [u8]) -> Self {
162        self.ikm = Some(ikm);
163        self
164    }
165
166    fn with_salt(mut self, salt: &'a [u8]) -> Self {
167        self.salt = Some(salt);
168        self
169    }
170
171    fn with_info(self, _info: &'a [u8]) -> Self {
172        // PBKDF2 doesn't use info, but we implement for API compatibility
173        self
174    }
175
176    fn with_output_length(mut self, length: usize) -> Self {
177        self.length = length;
178        self
179    }
180
181    fn derive(self) -> Result<ZeroizingBytes> {
182        let ikm = self.ikm.ok_or_else(|| {
183            Error::param("input_keying_material", "Input keying material is required")
184        })?;
185
186        let salt = match self.salt {
187            Some(s) => s,
188            None => self.kdf.params.salt.as_ref(),
189        };
190
191        // Use PBKDF2 with secure key handling
192        Pbkdf2::<H, S>::pbkdf2_secure(ikm, salt, self.iterations, self.length)
193    }
194
195    fn derive_array<const N: usize>(self) -> Result<Zeroizing<[u8; N]>> {
196        // Ensure the requested size matches
197        validate::length("PBKDF2 output", self.length, N)?;
198
199        let vec = self.derive()?;
200
201        // Convert to fixed-size array
202        let mut array = Zeroizing::new([0u8; N]);
203        array.copy_from_slice(&vec);
204        Ok(array)
205    }
206}
207
208impl<H: HashFunction + Clone, const S: usize> Pbkdf2<H, S> {
209    /// Internal PBKDF2 implementation with secure key handling
210    ///
211    /// This implements the core PBKDF2 algorithm as defined in RFC 8018 Section 5.2
212    /// with enhanced security for key material handling.
213    ///
214    /// # Arguments
215    /// * `password` - The password to derive the key from
216    /// * `salt` - The salt value
217    /// * `iterations` - The number of iterations
218    /// * `key_length` - The length of the derived key in bytes
219    ///
220    /// # Returns
221    /// The derived key of length key_length bytes
222    pub fn pbkdf2(
223        password: &[u8],
224        salt: &[u8],
225        iterations: u32,
226        key_length: usize,
227    ) -> Result<ZeroizingBytes> {
228        // Wrap password in secure buffer for internal operations
229        let secure_password = SecretVec::from_slice(password);
230        Self::pbkdf2_internal(&secure_password, salt, iterations, key_length)
231    }
232
233    /// Secure PBKDF2 implementation returning exact-size zeroizing storage.
234    pub fn pbkdf2_secure(
235        password: &[u8],
236        salt: &[u8],
237        iterations: u32,
238        key_length: usize,
239    ) -> Result<ZeroizingBytes> {
240        Self::pbkdf2(password, salt, iterations, key_length)
241    }
242
243    /// Internal PBKDF2 implementation using secure types
244    fn pbkdf2_internal(
245        password: &SecretVec,
246        salt: &[u8],
247        iterations: u32,
248        key_length: usize,
249    ) -> Result<ZeroizingBytes> {
250        // Strict parameter validation
251        validate::parameter(
252            iterations > 0,
253            "iterations",
254            "PBKDF2 iteration count must be > 0",
255        )?;
256
257        validate::parameter(
258            key_length > 0,
259            "key_length",
260            "PBKDF2 output length must be > 0",
261        )?;
262
263        let hash_len = H::output_size();
264
265        // Calculate how many blocks we need to generate - FIXED: Using div_ceil
266        let block_count = key_length.div_ceil(hash_len);
267
268        // Check that the output length is not too large
269        // RFC 8018 section 5.2 states that the maximum output length is (2^32 - 1) * hash_len
270        if block_count > 0xFFFFFFFF {
271            return Err(Error::Length {
272                context: "PBKDF2 output length",
273                expected: 0xFFFFFFFF * hash_len,
274                actual: key_length,
275            });
276        }
277
278        let mut result = Zeroizing::new(boxed_bytes_zeroed(key_length));
279        let mut result_offset = 0usize;
280
281        // Derive each block of the output
282        // Each block is calculated independently using the F function
283        for block_index in 1..=block_count {
284            let block =
285                Self::pbkdf2_f::<H>(password.as_ref(), salt, iterations, block_index as u32)?;
286
287            // Determine how much of this block to use
288            // Most blocks are used completely, but the last one might be partial
289            let to_copy = if block_index == block_count {
290                let remainder = key_length % hash_len;
291                if remainder == 0 {
292                    hash_len
293                } else {
294                    remainder
295                }
296            } else {
297                hash_len
298            };
299
300            result[result_offset..result_offset + to_copy].copy_from_slice(&block[..to_copy]);
301            result_offset += to_copy;
302        }
303
304        Ok(result)
305    }
306
307    /// F function for PBKDF2 as defined in RFC 8018
308    ///
309    /// This function applies the pseudorandom function (PRF) iteratively and
310    /// combines the results by XOR.
311    ///
312    /// Computes F(P, S, c, i) = U_1 XOR U_2 XOR ... XOR U_c
313    /// where U_1 = PRF(P, S || INT_32_BE(i))
314    ///       U_j = PRF(P, U_{j-1})
315    fn pbkdf2_f<T: HashFunction + Clone>(
316        password: &[u8],
317        salt: &[u8],
318        iterations: u32,
319        block_index: u32,
320    ) -> Result<ZeroizingBytes> {
321        // First iteration: HMAC(password, salt || block_index)
322        // U_1 = PRF(P, S || INT_32_BE(i))
323        let mut hmac = Hmac::<T>::new(password)?;
324        hmac.update(salt)?;
325        hmac.update(&block_index.to_be_bytes())?;
326        let result = hmac.finalize()?;
327
328        let mut prev = result.clone();
329
330        // Subsequent iterations: HMAC(password, prev_result)
331        // U_j = PRF(P, U_{j-1})
332        // Combine results by XOR: U_1 XOR U_2 XOR ... XOR U_c
333        let mut output = result;
334
335        for _ in 1..iterations {
336            let mut hmac = Hmac::<T>::new(password)?;
337            hmac.update(&prev)?;
338            prev = hmac.finalize()?;
339
340            // XOR the result with prev
341            for i in 0..output.len() {
342                output[i] ^= prev[i];
343            }
344        }
345
346        Ok(output)
347    }
348}
349
350impl<H: HashFunction + Clone, const S: usize> ParamProvider for Pbkdf2<H, S> {
351    type Params = Pbkdf2Params<S>;
352
353    fn with_params(params: Self::Params) -> Self {
354        Self {
355            _hash_type: PhantomData,
356            params,
357        }
358    }
359
360    fn params(&self) -> &Self::Params {
361        &self.params
362    }
363
364    fn set_params(&mut self, params: Self::Params) {
365        self.params = params;
366    }
367}
368
369impl<H: HashFunction + Clone, const S: usize> KeyDerivationFunction for Pbkdf2<H, S>
370where
371    Salt<S>: Pbkdf2Compatible,
372{
373    type Algorithm = Pbkdf2Algorithm<H>;
374    type Salt = Salt<S>;
375
376    fn new() -> Self {
377        Self {
378            _hash_type: PhantomData,
379            params: Pbkdf2Params::default(),
380        }
381    }
382
383    #[cfg(feature = "alloc")]
384    fn derive_key(
385        &self,
386        input: &[u8],
387        salt: Option<&[u8]>,
388        _info: Option<&[u8]>,
389        length: usize,
390    ) -> Result<ZeroizingBytes> {
391        // Use provided salt or fallback to default from params - FIXED: Removed needless borrow
392        let effective_salt = match salt {
393            Some(s) => s,
394            None => self.params.salt.as_ref(),
395        };
396
397        // Use provided length or fallback to default from params
398        let effective_length = if length > 0 {
399            length
400        } else {
401            self.params.key_length
402        };
403
404        // Use the secure version
405        Self::pbkdf2_secure(
406            input,
407            effective_salt,
408            self.params.iterations,
409            effective_length,
410        )
411    }
412
413    // FIXED: Elided lifetime
414    fn builder(&self) -> impl KdfOperation<'_, Self::Algorithm> {
415        Pbkdf2Builder::<H, S> {
416            kdf: self,
417            ikm: None,
418            salt: None,
419            iterations: self.params.iterations,
420            length: self.params.key_length,
421        }
422    }
423
424    fn generate_salt<R: RngCore + CryptoRng>(rng: &mut R) -> Result<Self::Salt> {
425        Salt::random_with_size(rng, Self::Algorithm::MIN_SALT_SIZE)
426    }
427
428    fn security_level() -> SecurityLevel {
429        Self::Algorithm::security_level()
430    }
431}
432
433#[cfg(feature = "std")]
434impl<H: HashFunction + Clone, const S: usize> PasswordHashFunction for Pbkdf2<H, S>
435where
436    Salt<S>: Pbkdf2Compatible,
437{
438    type Password = SecretBytes<32>; // Using a 32-byte buffer for passwords
439
440    fn hash_password(&self, password: &Self::Password) -> Result<PasswordHash> {
441        // Derive the key using secure implementation
442        let hash = Self::pbkdf2(
443            password.as_ref(),
444            self.params.salt.as_ref(),
445            self.params.iterations,
446            self.params.key_length,
447        )?;
448
449        // Create parameters map
450        let mut params = BTreeMap::new();
451        params.insert("i".to_string(), self.params.iterations.to_string());
452
453        Ok(PasswordHash {
454            algorithm: format!("pbkdf2-{}", H::name().to_lowercase()),
455            params,
456            salt: self.params.salt.to_bytes(),
457            hash: hash.into_inner().into_vec(),
458        })
459    }
460
461    fn verify(&self, password: &Self::Password, hash: &PasswordHash) -> Result<bool> {
462        // Verify the algorithm
463        let expected_alg = format!("pbkdf2-{}", H::name().to_lowercase());
464        validate::parameter(
465            hash.algorithm == expected_alg,
466            "algorithm",
467            "Algorithm mismatch",
468        )?;
469
470        // Get iterations from the hash parameters
471        let iterations = match hash.param("i") {
472            Some(i) => i
473                .parse::<u32>()
474                .map_err(|_| Error::param("iterations", "Invalid iterations parameter"))?,
475            None => return Err(Error::param("iterations", "Missing iterations parameter")),
476        };
477
478        // Derive key with the same parameters, using secure implementation
479        let derived = Self::pbkdf2(password.as_ref(), &hash.salt, iterations, hash.hash.len())?;
480
481        // Compare in constant time
482        Ok(constant_time_eq(&derived, &hash.hash))
483    }
484
485    fn benchmark(&self) -> Duration {
486        let start = Instant::now();
487        let password = SecretBytes::new([0u8; 32]); // Use a dummy password for benchmarking
488
489        // If hash_password fails, we still return a valid Duration
490        // This is acceptable since benchmark is not critical for security
491        match self.hash_password(&password) {
492            Ok(_) => {}
493            Err(_) => {
494                // We could log the error here if we had a logging system
495                // For now, we'll just continue and return the elapsed time
496                // This gives a reasonable approximation even on error
497            }
498        }
499
500        start.elapsed()
501    }
502
503    fn recommended_params(target_duration: Duration) -> Self::Params {
504        // Start with the default parameters
505        let mut params = Pbkdf2Params::default();
506
507        // Create a temporary instance
508        let instance = Self::with_params(params.clone());
509
510        // Measure the current execution time
511        let current_duration = instance.benchmark();
512
513        // Calculate the ratio and adjust iterations
514        let ratio = target_duration.as_secs_f64() / current_duration.as_secs_f64();
515        params.iterations = (params.iterations as f64 * ratio) as u32;
516
517        // Ensure iterations is at least 10,000
518        params.iterations = core::cmp::max(params.iterations, 10_000);
519
520        params
521    }
522}
523
524#[cfg(test)]
525mod tests;