Skip to main content

rscrypto/auth/
pbkdf2.rs

1//! PBKDF2-HMAC-SHA2 key derivation (RFC 2898, NIST SP 800-132).
2//!
3//! Derives cryptographic keys from passwords using iterated HMAC-SHA256 or
4//! HMAC-SHA512. Each iteration runs directly against cached SHA compress
5//! function pointers with pre-computed HMAC prefix states — no per-iteration
6//! struct creation, dispatch, or `Drop` overhead.
7//!
8//! # Examples
9//!
10//! ```rust
11//! use rscrypto::Pbkdf2Sha256;
12//!
13//! let password = b"correct horse battery staple";
14//! let salt = b"random-salt-value";
15//!
16//! let key = Pbkdf2Sha256::derive_key_array::<32>(password, salt, 600_000)?;
17//! assert_ne!(key, [0u8; 32]);
18//!
19//! assert!(Pbkdf2Sha256::verify_password(password, salt, 600_000, &key).is_ok());
20//! assert!(Pbkdf2Sha256::verify_password(b"wrong", salt, 600_000, &key).is_err());
21//! # Ok::<(), rscrypto::auth::Pbkdf2Error>(())
22//! ```
23//!
24//! # Security
25//!
26//! For NIST SP 800-132 / OWASP 2023 compliance baselines, pair type-specific
27//! minimums are:
28//!
29//! - `Pbkdf2Sha256`: at least `600_000` iterations
30//! - `Pbkdf2Sha512`: at least `210_000` iterations
31//! - salt length: at least `16` bytes (128 bits) for both types
32//!
33//! These are policy minima for production password-hashing deployments.
34//! This module is algorithmic PBKDF2; enforcement depends on caller-side policy.
35
36use core::fmt;
37
38use super::hmac::hmac_prefix_state;
39use crate::{
40  hashes::crypto::{
41    Sha256, Sha512,
42    sha256::{H0 as SHA256_H0, dispatch as sha256_dispatch, kernels::CompressBlocksFn as Sha256CompressBlocksFn},
43    sha512::{H0 as SHA512_H0, dispatch as sha512_dispatch, kernels::CompressBlocksFn as Sha512CompressBlocksFn},
44  },
45  traits::{VerificationError, ct},
46};
47
48const SHA256_OUTPUT_SIZE: usize = 32;
49const SHA256_BLOCK_SIZE: usize = 64;
50/// Maximum salt length whose `salt || INT_32_BE(block_index) || 0x80 || len`
51/// payload fits in a single SHA-256 block: 64 - 4 (block index) - 1 (`0x80`)
52/// - 8 (length) = 51.
53const SHA256_INLINE_SALT_MAX: usize = SHA256_BLOCK_SIZE - 4 - 1 - 8;
54
55const SHA512_OUTPUT_SIZE: usize = 64;
56const SHA512_BLOCK_SIZE: usize = 128;
57/// Maximum salt length whose `salt || INT_32_BE(block_index) || 0x80 || len`
58/// payload fits in a single SHA-512 block: 128 - 4 - 1 - 16 = 107.
59const SHA512_INLINE_SALT_MAX: usize = SHA512_BLOCK_SIZE - 4 - 1 - 16;
60
61#[inline(always)]
62#[allow(clippy::indexing_slicing)]
63fn write_u32x8_be(dst: &mut [u8], words: &[u32; 8]) {
64  dst[0..4].copy_from_slice(&words[0].to_be_bytes());
65  dst[4..8].copy_from_slice(&words[1].to_be_bytes());
66  dst[8..12].copy_from_slice(&words[2].to_be_bytes());
67  dst[12..16].copy_from_slice(&words[3].to_be_bytes());
68  dst[16..20].copy_from_slice(&words[4].to_be_bytes());
69  dst[20..24].copy_from_slice(&words[5].to_be_bytes());
70  dst[24..28].copy_from_slice(&words[6].to_be_bytes());
71  dst[28..32].copy_from_slice(&words[7].to_be_bytes());
72}
73
74#[inline(always)]
75#[allow(clippy::indexing_slicing)]
76fn write_u64x8_be(dst: &mut [u8], words: &[u64; 8]) {
77  dst[0..8].copy_from_slice(&words[0].to_be_bytes());
78  dst[8..16].copy_from_slice(&words[1].to_be_bytes());
79  dst[16..24].copy_from_slice(&words[2].to_be_bytes());
80  dst[24..32].copy_from_slice(&words[3].to_be_bytes());
81  dst[32..40].copy_from_slice(&words[4].to_be_bytes());
82  dst[40..48].copy_from_slice(&words[5].to_be_bytes());
83  dst[48..56].copy_from_slice(&words[6].to_be_bytes());
84  dst[56..64].copy_from_slice(&words[7].to_be_bytes());
85}
86
87#[inline(always)]
88fn zeroize_u32x8_no_fence(words: &mut [u32; 8]) {
89  ct::zeroize_words_no_fence(words);
90}
91
92#[inline(always)]
93fn zeroize_u64x8_no_fence(words: &mut [u64; 8]) {
94  ct::zeroize_words_no_fence(words);
95}
96
97// ─── Error ──────────────────────────────────────────────────────────────────
98
99/// Invalid PBKDF2 parameters.
100///
101/// Returned when the iteration count is zero or the derived key length exceeds
102/// the RFC 2898 maximum of `(2^32 − 1) × hLen`.
103///
104/// # Examples
105///
106/// ```rust
107/// use rscrypto::{Pbkdf2Sha256, auth::Pbkdf2Error};
108///
109/// let err = Pbkdf2Sha256::derive_key(b"pw", b"salt", 0, &mut [0u8; 32]);
110/// assert_eq!(err, Err(Pbkdf2Error::InvalidIterations));
111/// ```
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
113#[non_exhaustive]
114pub enum Pbkdf2Error {
115  /// The iteration count must be at least 1. NIST/OWASP policy minima are documented on
116  /// `Pbkdf2Sha256::MIN_RECOMMENDED_ITERATIONS` and
117  /// `Pbkdf2Sha512::MIN_RECOMMENDED_ITERATIONS`.
118  InvalidIterations,
119  /// The requested output length exceeds `(2^32 − 1) × hLen`.
120  OutputTooLong,
121}
122
123impl fmt::Display for Pbkdf2Error {
124  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125    match self {
126      Self::InvalidIterations => f.write_str("PBKDF2 iteration count must be at least 1"),
127      Self::OutputTooLong => f.write_str("PBKDF2 output length exceeds algorithm maximum"),
128    }
129  }
130}
131
132impl core::error::Error for Pbkdf2Error {}
133
134macro_rules! define_pbkdf2_sha2 {
135  (
136    $(#[$struct_meta:meta])*
137    $name:ident {
138      output_size_const: $output_size_const:ident,
139      block_size_const: $block_size_const:ident,
140      compress_ty: $compress_ty:ty,
141      digest_ty: $digest_ty:ty,
142      h0: $h0:path,
143      dispatch: $dispatch:ident,
144      f_fn: $f_fn:path,
145      iter1_fn: $iter1_fn:path,
146      test_oneshot: $test_oneshot:path,
147      word_ty: $word_ty:ty,
148      recommended_iterations: $recommended_iterations:expr,
149    }
150  ) => {
151    $(#[$struct_meta])*
152    #[derive(Clone)]
153    pub struct $name {
154      inner_init: [$word_ty; 8],
155      outer_init: [$word_ty; 8],
156      compress: $compress_ty,
157    }
158
159    impl fmt::Debug for $name {
160      fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161        f.debug_struct(stringify!($name)).finish_non_exhaustive()
162      }
163    }
164
165    impl $name {
166      /// Digest output size in bytes.
167      pub const OUTPUT_SIZE: usize = $output_size_const;
168      /// Minimum iteration count recommended for compliance-sensitive deployments.
169      pub const MIN_RECOMMENDED_ITERATIONS: u32 = $recommended_iterations;
170      /// Minimum salt length (bytes) recommended for compliance-sensitive deployments.
171      pub const MIN_SALT_LEN: usize = 16;
172
173      /// Pre-compute HMAC prefix states from `password`.
174      #[must_use]
175      #[allow(clippy::indexing_slicing)] // password.len() <= block size in the else branch.
176      pub fn new(password: &[u8]) -> Self {
177        let compress = $dispatch::compress_dispatch().select(0);
178
179        let mut key_block = [0u8; $block_size_const];
180        if password.len() > $block_size_const {
181          let digest = <$digest_ty>::digest(password);
182          key_block[..$output_size_const].copy_from_slice(&digest);
183        } else {
184          key_block[..password.len()].copy_from_slice(password);
185        }
186
187        let (inner_init, outer_init) = hmac_prefix_state(&mut key_block, |ipad, opad| {
188          let mut inner_init = $h0;
189          compress(&mut inner_init, ipad);
190
191          let mut outer_init = $h0;
192          compress(&mut outer_init, opad);
193
194          (inner_init, outer_init)
195        });
196
197        Self {
198          inner_init,
199          outer_init,
200          compress,
201        }
202      }
203
204      /// Derive a key into `okm`.
205      #[inline]
206      #[allow(clippy::indexing_slicing)]
207      pub fn derive(&self, salt: &[u8], iterations: u32, okm: &mut [u8]) -> Result<(), Pbkdf2Error> {
208        Self::derive_with_prefixes(self.compress, &self.inner_init, &self.outer_init, salt, iterations, okm)
209      }
210
211      #[inline]
212      #[allow(clippy::indexing_slicing)]
213      fn derive_with_prefixes(
214        compress: $compress_ty,
215        inner_init: &[$word_ty; 8],
216        outer_init: &[$word_ty; 8],
217        salt: &[u8],
218        iterations: u32,
219        okm: &mut [u8],
220      ) -> Result<(), Pbkdf2Error> {
221        if iterations == 0 {
222          return Err(Pbkdf2Error::InvalidIterations);
223        }
224        if okm.is_empty() {
225          return Ok(());
226        }
227        let num_blocks = okm.len().div_ceil($output_size_const);
228        if num_blocks as u64 > u32::MAX as u64 {
229          return Err(Pbkdf2Error::OutputTooLong);
230        }
231
232        if iterations == 1 {
233          $iter1_fn(compress, inner_init, outer_init, salt, okm);
234          return Ok(());
235        }
236
237        let mut block_index = 1u32;
238        let mut chunks = okm.chunks_exact_mut($output_size_const);
239
240        for chunk in chunks.by_ref() {
241          // SAFETY: chunks_exact_mut yields slices whose length is exactly
242          // $output_size_const, so this cast is to the same initialized bytes.
243          let full_chunk = unsafe { &mut *(chunk.as_mut_ptr().cast::<[u8; $output_size_const]>()) };
244          $f_fn(
245            compress,
246            inner_init,
247            outer_init,
248            salt,
249            iterations,
250            block_index,
251            full_chunk,
252          );
253          block_index = block_index.strict_add(1);
254        }
255
256        let tail = chunks.into_remainder();
257        if !tail.is_empty() {
258          let mut block_out = [0u8; $output_size_const];
259          $f_fn(
260            compress,
261            inner_init,
262            outer_init,
263            salt,
264            iterations,
265            block_index,
266            &mut block_out,
267          );
268          tail.copy_from_slice(&block_out[..tail.len()]);
269          ct::zeroize(&mut block_out);
270        }
271        Ok(())
272      }
273
274      /// Derive a key into a fixed-size array.
275      pub fn derive_array<const N: usize>(&self, salt: &[u8], iterations: u32) -> Result<[u8; N], Pbkdf2Error> {
276        let mut out = [0u8; N];
277        self.derive(salt, iterations, &mut out)?;
278        Ok(out)
279      }
280
281      /// Verify `expected` against the derived key in constant time.
282      #[allow(clippy::indexing_slicing)]
283      #[must_use = "password verification must be checked; a dropped Result silently accepts the wrong password"]
284      pub fn verify(&self, salt: &[u8], iterations: u32, expected: &[u8]) -> Result<(), VerificationError> {
285        if iterations == 0 || expected.is_empty() {
286          return Err(VerificationError::new());
287        }
288        let num_blocks = expected.len().div_ceil($output_size_const);
289        if num_blocks as u64 > u32::MAX as u64 {
290          return Err(VerificationError::new());
291        }
292
293        let compress = self.compress;
294
295        let mut block_out = [0u8; $output_size_const];
296        let mut acc = 0u8;
297
298        for (i, chunk) in expected.chunks($output_size_const).enumerate() {
299          let block_index = (i as u32).strict_add(1);
300          $f_fn(
301            compress,
302            &self.inner_init,
303            &self.outer_init,
304            salt,
305            iterations,
306            block_index,
307            &mut block_out,
308          );
309          for (&a, &b) in block_out[..chunk.len()].iter().zip(chunk.iter()) {
310            acc |= a ^ b;
311          }
312        }
313
314        ct::zeroize(&mut block_out);
315
316        if core::hint::black_box(acc) == 0 {
317          Ok(())
318        } else {
319          Err(VerificationError::new())
320        }
321      }
322
323      /// Derive a key in one shot.
324      #[inline]
325      #[allow(clippy::indexing_slicing)]
326      pub fn derive_key(password: &[u8], salt: &[u8], iterations: u32, okm: &mut [u8]) -> Result<(), Pbkdf2Error> {
327        let compress = $dispatch::compress_dispatch().select(0);
328
329        let mut key_block = [0u8; $block_size_const];
330        if password.len() > $block_size_const {
331          let digest = <$digest_ty>::digest(password);
332          key_block[..$output_size_const].copy_from_slice(&digest);
333        } else {
334          key_block[..password.len()].copy_from_slice(password);
335        }
336
337        let mut ipad = [0x36u8; $block_size_const];
338        let mut opad = [0x5Cu8; $block_size_const];
339        for ((ipad_byte, opad_byte), key_byte) in ipad.iter_mut().zip(opad.iter_mut()).zip(key_block.iter().copied()) {
340          *ipad_byte ^= key_byte;
341          *opad_byte ^= key_byte;
342        }
343
344        let mut inner_init = $h0;
345        compress(&mut inner_init, &ipad);
346
347        let mut outer_init = $h0;
348        compress(&mut outer_init, &opad);
349
350        let result = Self::derive_with_prefixes(compress, &inner_init, &outer_init, salt, iterations, okm);
351
352        ct::zeroize_no_fence(&mut key_block);
353        ct::zeroize_no_fence(&mut ipad);
354        ct::zeroize_no_fence(&mut opad);
355        for word in inner_init.iter_mut().chain(outer_init.iter_mut()) {
356          // SAFETY: word is a valid, aligned, dereferenceable pointer to initialized memory.
357          unsafe { core::ptr::write_volatile(word, 0) };
358        }
359        core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
360
361        result
362      }
363
364      /// Derive a key into a fixed-size array in one shot.
365      #[inline]
366      pub fn derive_key_array<const N: usize>(
367        password: &[u8],
368        salt: &[u8],
369        iterations: u32,
370      ) -> Result<[u8; N], Pbkdf2Error> {
371        Self::new(password).derive_array(salt, iterations)
372      }
373
374      /// Verify a password in one shot.
375      #[inline]
376      #[must_use = "password verification must be checked; a dropped Result silently accepts the wrong password"]
377      pub fn verify_password(
378        password: &[u8],
379        salt: &[u8],
380        iterations: u32,
381        expected: &[u8],
382      ) -> Result<(), VerificationError> {
383        Self::new(password).verify(salt, iterations, expected)
384      }
385
386      /// Test-only: build with a specific digest compress function.
387      #[cfg(test)]
388      #[allow(clippy::indexing_slicing)]
389      pub(crate) fn new_with_compress_for_test(password: &[u8], compress: $compress_ty) -> Self {
390        let mut key_block = [0u8; $block_size_const];
391        if password.len() > $block_size_const {
392          key_block[..$output_size_const].copy_from_slice(&$test_oneshot(password, compress));
393        } else {
394          key_block[..password.len()].copy_from_slice(password);
395        }
396
397        let (inner_init, outer_init) = hmac_prefix_state(&mut key_block, |ipad, opad| {
398          let mut inner_init = $h0;
399          compress(&mut inner_init, ipad);
400
401          let mut outer_init = $h0;
402          compress(&mut outer_init, opad);
403
404          (inner_init, outer_init)
405        });
406
407        Self {
408          inner_init,
409          outer_init,
410          compress,
411        }
412      }
413    }
414
415    impl Drop for $name {
416      fn drop(&mut self) {
417        for word in self.inner_init.iter_mut().chain(self.outer_init.iter_mut()) {
418          // SAFETY: word is a valid, aligned, dereferenceable pointer to initialized memory.
419          unsafe { core::ptr::write_volatile(word, 0) };
420        }
421        core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
422      }
423    }
424  };
425}
426
427// ─── PBKDF2-HMAC-SHA256 ────────────────────────────────────────────────────
428
429define_pbkdf2_sha2! {
430  /// PBKDF2-HMAC-SHA256 key derivation (RFC 2898).
431  ///
432  /// Pre-computes the HMAC-SHA256 prefix states from the password so that
433  /// subsequent `derive` and `verify` calls run the iteration loop directly
434  /// against the SHA-256 compress function with no per-iteration overhead.
435  ///
436  /// # Examples
437  ///
438  /// ```rust
439  /// use rscrypto::Pbkdf2Sha256;
440  ///
441  /// // Derive a 32-byte key
442  /// let dk = Pbkdf2Sha256::derive_key_array::<32>(b"password", b"salt", 600_000)?;
443  ///
444  /// // Re-use pre-computed state for multiple operations
445  /// let state = Pbkdf2Sha256::new(b"password");
446  /// let dk2 = state.derive_array::<32>(b"salt", 600_000)?;
447  /// assert_eq!(dk, dk2);
448  ///
449  /// // Verify
450  /// assert!(state.verify(b"salt", 600_000, &dk).is_ok());
451  /// # Ok::<(), rscrypto::auth::Pbkdf2Error>(())
452  /// ```
453  Pbkdf2Sha256 {
454    output_size_const: SHA256_OUTPUT_SIZE,
455    block_size_const: SHA256_BLOCK_SIZE,
456    compress_ty: Sha256CompressBlocksFn,
457    digest_ty: Sha256,
458    h0: SHA256_H0,
459    dispatch: sha256_dispatch,
460    f_fn: pbkdf2_sha256_f,
461    iter1_fn: pbkdf2_sha256_iter1,
462    test_oneshot: sha256_oneshot_with_compress,
463    word_ty: u32,
464    recommended_iterations: 600_000,
465  }
466}
467
468/// Test-only: one-shot SHA-256 digest using a specific compress function.
469#[cfg(test)]
470#[allow(clippy::indexing_slicing)]
471fn sha256_oneshot_with_compress(data: &[u8], compress: Sha256CompressBlocksFn) -> [u8; SHA256_OUTPUT_SIZE] {
472  let mut state = SHA256_H0;
473  let mut pos = 0usize;
474  while pos.strict_add(SHA256_BLOCK_SIZE) <= data.len() {
475    compress(&mut state, &data[pos..pos.strict_add(SHA256_BLOCK_SIZE)]);
476    pos = pos.strict_add(SHA256_BLOCK_SIZE);
477  }
478  let mut block = [0u8; SHA256_BLOCK_SIZE];
479  let tail = data.len().strict_sub(pos);
480  block[..tail].copy_from_slice(&data[pos..]);
481  block[tail] = 0x80;
482  if tail >= 56 {
483    compress(&mut state, &block);
484    block = [0u8; SHA256_BLOCK_SIZE];
485  }
486  block[56..64].copy_from_slice(&(data.len() as u64).strict_mul(8).to_be_bytes());
487  compress(&mut state, &block);
488  let mut out = [0u8; SHA256_OUTPUT_SIZE];
489  for (chunk, &word) in out.chunks_exact_mut(4).zip(state.iter()) {
490    chunk.copy_from_slice(&word.to_be_bytes());
491  }
492  out
493}
494
495/// Compute one PBKDF2-SHA256 block: `F(Password, Salt, c, i)`.
496///
497/// Each HMAC iteration in the hot loop runs exactly 2 SHA-256 compress calls
498/// using pre-padded block templates — no hash struct creation, no dispatch
499/// overhead, no padding recomputation.
500#[allow(clippy::indexing_slicing)]
501#[inline(always)]
502fn pbkdf2_sha256_f(
503  compress: Sha256CompressBlocksFn,
504  inner_init: &[u32; 8],
505  outer_init: &[u32; 8],
506  salt: &[u8],
507  iterations: u32,
508  block_index: u32,
509  output: &mut [u8; SHA256_OUTPUT_SIZE],
510) {
511  let mut state: [u32; 8];
512  let mut u_words: [u32; 8];
513  let mut result_words: [u32; 8];
514
515  // ── U1 = HMAC(Password, Salt || INT_32_BE(block_index)) ──────────────
516  state = *inner_init;
517  let msg_len = salt.len().strict_add(4);
518  let total_inner = (SHA256_BLOCK_SIZE as u64).strict_add(msg_len as u64);
519
520  let mut block = [0u8; SHA256_BLOCK_SIZE];
521  if salt.len() <= SHA256_INLINE_SALT_MAX {
522    block[..salt.len()].copy_from_slice(salt);
523    let pos = salt.len().strict_add(4);
524    block[salt.len()..pos].copy_from_slice(&block_index.to_be_bytes());
525    block[pos] = 0x80;
526    block[56..SHA256_BLOCK_SIZE].copy_from_slice(&total_inner.strict_mul(8).to_be_bytes());
527    compress(&mut state, &block);
528  } else {
529    let mut pos = 0usize;
530
531    // Feed salt
532    let mut salt_off = 0usize;
533    while salt_off < salt.len() {
534      let space = SHA256_BLOCK_SIZE.strict_sub(pos);
535      let remaining = salt.len().strict_sub(salt_off);
536      let take = if space < remaining { space } else { remaining };
537      block[pos..pos.strict_add(take)].copy_from_slice(&salt[salt_off..salt_off.strict_add(take)]);
538      pos = pos.strict_add(take);
539      salt_off = salt_off.strict_add(take);
540      if pos == SHA256_BLOCK_SIZE {
541        compress(&mut state, &block);
542        block = [0u8; SHA256_BLOCK_SIZE];
543        pos = 0;
544      }
545    }
546
547    // Feed block_index (4 bytes big-endian)
548    for &b in &block_index.to_be_bytes() {
549      block[pos] = b;
550      pos = pos.strict_add(1);
551      if pos == SHA256_BLOCK_SIZE {
552        compress(&mut state, &block);
553        block = [0u8; SHA256_BLOCK_SIZE];
554        pos = 0;
555      }
556    }
557
558    // SHA-256 padding
559    block[pos] = 0x80;
560    if pos.strict_add(1) > 56 {
561      compress(&mut state, &block);
562      block = [0u8; SHA256_BLOCK_SIZE];
563    }
564    block[56..SHA256_BLOCK_SIZE].copy_from_slice(&total_inner.strict_mul(8).to_be_bytes());
565    compress(&mut state, &block);
566  }
567
568  // Outer hash of U1: single block
569  let mut outer_block = [0u8; SHA256_BLOCK_SIZE];
570  write_u32x8_be(&mut outer_block[..SHA256_OUTPUT_SIZE], &state);
571  outer_block[SHA256_OUTPUT_SIZE] = 0x80;
572  // total outer bytes = 64 (opad, pre-compressed) + 32 (inner hash) = 96 → 768 bits
573  outer_block[56..SHA256_BLOCK_SIZE].copy_from_slice(&768u64.to_be_bytes());
574
575  state = *outer_init;
576  compress(&mut state, &outer_block);
577  u_words = state;
578  result_words = u_words;
579
580  if iterations == 1 {
581    write_u32x8_be(output, &result_words);
582    ct::zeroize_no_fence(&mut outer_block);
583    ct::zeroize_no_fence(&mut block);
584    zeroize_u32x8_no_fence(&mut state);
585    zeroize_u32x8_no_fence(&mut u_words);
586    zeroize_u32x8_no_fence(&mut result_words);
587    core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
588    return;
589  }
590
591  // ── Iterations 2..=c (fixed-size HMAC, 2 compress calls each) ────────
592  // Inner template: [32-byte U] [0x80] [zeros] [768-bit length]
593  let mut inner_block = [0u8; SHA256_BLOCK_SIZE];
594  inner_block[SHA256_OUTPUT_SIZE] = 0x80;
595  inner_block[56..SHA256_BLOCK_SIZE].copy_from_slice(&768u64.to_be_bytes());
596
597  for _ in 1..iterations {
598    // Inner: compress(inner_init, U_{j-1} || padding)
599    write_u32x8_be(&mut inner_block[..SHA256_OUTPUT_SIZE], &u_words);
600    state = *inner_init;
601    compress(&mut state, &inner_block);
602
603    // Serialize inner hash directly into outer block
604    write_u32x8_be(&mut outer_block[..SHA256_OUTPUT_SIZE], &state);
605
606    // Outer: compress(outer_init, inner_hash || padding)
607    state = *outer_init;
608    compress(&mut state, &outer_block);
609
610    u_words = state;
611
612    // XOR into output
613    for (dst, &word) in result_words.iter_mut().zip(u_words.iter()) {
614      *dst ^= word;
615    }
616  }
617
618  write_u32x8_be(output, &result_words);
619
620  // Zeroize sensitive state
621  ct::zeroize_no_fence(&mut inner_block);
622  ct::zeroize_no_fence(&mut outer_block);
623  ct::zeroize_no_fence(&mut block);
624  zeroize_u32x8_no_fence(&mut state);
625  zeroize_u32x8_no_fence(&mut u_words);
626  zeroize_u32x8_no_fence(&mut result_words);
627  core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
628}
629
630#[allow(clippy::indexing_slicing)]
631#[inline(always)]
632fn pbkdf2_sha256_iter1(
633  compress: Sha256CompressBlocksFn,
634  inner_init: &[u32; 8],
635  outer_init: &[u32; 8],
636  salt: &[u8],
637  okm: &mut [u8],
638) {
639  if salt.len() > SHA256_INLINE_SALT_MAX {
640    let mut block_index = 1u32;
641    let mut chunks = okm.chunks_exact_mut(SHA256_OUTPUT_SIZE);
642    for chunk in chunks.by_ref() {
643      // SAFETY: chunks_exact_mut yields slices whose length is exactly SHA256_OUTPUT_SIZE.
644      let full_chunk = unsafe { &mut *(chunk.as_mut_ptr().cast::<[u8; SHA256_OUTPUT_SIZE]>()) };
645      pbkdf2_sha256_f(compress, inner_init, outer_init, salt, 1, block_index, full_chunk);
646      block_index = block_index.strict_add(1);
647    }
648    let tail = chunks.into_remainder();
649    if !tail.is_empty() {
650      let mut block_out = [0u8; SHA256_OUTPUT_SIZE];
651      pbkdf2_sha256_f(compress, inner_init, outer_init, salt, 1, block_index, &mut block_out);
652      tail.copy_from_slice(&block_out[..tail.len()]);
653      ct::zeroize(&mut block_out);
654    }
655    return;
656  }
657
658  let msg_len = salt.len().strict_add(4);
659  let total_inner = (SHA256_BLOCK_SIZE as u64).strict_add(msg_len as u64);
660  let index_pos = salt.len();
661  let pad_pos = index_pos.strict_add(4);
662
663  let mut block = [0u8; SHA256_BLOCK_SIZE];
664  block[..salt.len()].copy_from_slice(salt);
665  block[pad_pos] = 0x80;
666  block[56..SHA256_BLOCK_SIZE].copy_from_slice(&total_inner.strict_mul(8).to_be_bytes());
667
668  let mut outer_block = [0u8; SHA256_BLOCK_SIZE];
669  outer_block[SHA256_OUTPUT_SIZE] = 0x80;
670  outer_block[56..SHA256_BLOCK_SIZE].copy_from_slice(&768u64.to_be_bytes());
671
672  let mut state = [0u32; 8];
673  let mut block_index = 1u32;
674
675  let mut chunks = okm.chunks_exact_mut(SHA256_OUTPUT_SIZE);
676  for chunk in chunks.by_ref() {
677    block[index_pos..pad_pos].copy_from_slice(&block_index.to_be_bytes());
678
679    state = *inner_init;
680    compress(&mut state, &block);
681
682    write_u32x8_be(&mut outer_block[..SHA256_OUTPUT_SIZE], &state);
683    state = *outer_init;
684    compress(&mut state, &outer_block);
685
686    write_u32x8_be(chunk, &state);
687    block_index = block_index.strict_add(1);
688  }
689
690  let tail = chunks.into_remainder();
691  if !tail.is_empty() {
692    block[index_pos..pad_pos].copy_from_slice(&block_index.to_be_bytes());
693
694    state = *inner_init;
695    compress(&mut state, &block);
696
697    write_u32x8_be(&mut outer_block[..SHA256_OUTPUT_SIZE], &state);
698    state = *outer_init;
699    compress(&mut state, &outer_block);
700
701    let mut block_out = [0u8; SHA256_OUTPUT_SIZE];
702    write_u32x8_be(&mut block_out, &state);
703    tail.copy_from_slice(&block_out[..tail.len()]);
704    ct::zeroize_no_fence(&mut block_out);
705  }
706
707  ct::zeroize_no_fence(&mut block);
708  ct::zeroize_no_fence(&mut outer_block);
709  zeroize_u32x8_no_fence(&mut state);
710  core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
711}
712
713// ─── PBKDF2-HMAC-SHA512 ────────────────────────────────────────────────────
714
715define_pbkdf2_sha2! {
716  /// PBKDF2-HMAC-SHA512 key derivation (RFC 2898).
717  ///
718  /// Pre-computes the HMAC-SHA512 prefix states from the password so that
719  /// subsequent `derive` and `verify` calls run the iteration loop directly
720  /// against the SHA-512 compress function with no per-iteration overhead.
721  ///
722  /// # Examples
723  ///
724  /// ```rust
725  /// use rscrypto::Pbkdf2Sha512;
726  ///
727  /// let dk = Pbkdf2Sha512::derive_key_array::<64>(b"password", b"salt", 210_000)?;
728  /// assert!(Pbkdf2Sha512::verify_password(b"password", b"salt", 210_000, &dk).is_ok());
729  /// # Ok::<(), rscrypto::auth::Pbkdf2Error>(())
730  /// ```
731  Pbkdf2Sha512 {
732    output_size_const: SHA512_OUTPUT_SIZE,
733    block_size_const: SHA512_BLOCK_SIZE,
734    compress_ty: Sha512CompressBlocksFn,
735    digest_ty: Sha512,
736    h0: SHA512_H0,
737    dispatch: sha512_dispatch,
738    f_fn: pbkdf2_sha512_f,
739    iter1_fn: pbkdf2_sha512_iter1,
740    test_oneshot: sha512_oneshot_with_compress,
741    word_ty: u64,
742    recommended_iterations: 210_000,
743  }
744}
745
746/// Test-only: one-shot SHA-512 digest using a specific compress function.
747#[cfg(test)]
748#[allow(clippy::indexing_slicing)]
749fn sha512_oneshot_with_compress(data: &[u8], compress: Sha512CompressBlocksFn) -> [u8; SHA512_OUTPUT_SIZE] {
750  let mut state = SHA512_H0;
751  let mut pos = 0usize;
752  while pos.strict_add(SHA512_BLOCK_SIZE) <= data.len() {
753    compress(&mut state, &data[pos..pos.strict_add(SHA512_BLOCK_SIZE)]);
754    pos = pos.strict_add(SHA512_BLOCK_SIZE);
755  }
756  let mut block = [0u8; SHA512_BLOCK_SIZE];
757  let tail = data.len().strict_sub(pos);
758  block[..tail].copy_from_slice(&data[pos..]);
759  block[tail] = 0x80;
760  if tail >= 112 {
761    compress(&mut state, &block);
762    block = [0u8; SHA512_BLOCK_SIZE];
763  }
764  block[112..128].copy_from_slice(&(data.len() as u128).strict_mul(8).to_be_bytes());
765  compress(&mut state, &block);
766  let mut out = [0u8; SHA512_OUTPUT_SIZE];
767  for (chunk, &word) in out.chunks_exact_mut(8).zip(state.iter()) {
768    chunk.copy_from_slice(&word.to_be_bytes());
769  }
770  out
771}
772
773/// Compute one PBKDF2-SHA512 block: `F(Password, Salt, c, i)`.
774#[allow(clippy::indexing_slicing)]
775#[inline(always)]
776fn pbkdf2_sha512_f(
777  compress: Sha512CompressBlocksFn,
778  inner_init: &[u64; 8],
779  outer_init: &[u64; 8],
780  salt: &[u8],
781  iterations: u32,
782  block_index: u32,
783  output: &mut [u8; SHA512_OUTPUT_SIZE],
784) {
785  let mut state: [u64; 8];
786  let mut u_words: [u64; 8];
787  let mut result_words: [u64; 8];
788
789  // ── U1 = HMAC(Password, Salt || INT_32_BE(block_index)) ──────────────
790  state = *inner_init;
791  let msg_len = salt.len().strict_add(4);
792  let total_inner = (SHA512_BLOCK_SIZE as u128).strict_add(msg_len as u128);
793
794  let mut block = [0u8; SHA512_BLOCK_SIZE];
795  if salt.len() <= SHA512_INLINE_SALT_MAX {
796    block[..salt.len()].copy_from_slice(salt);
797    let pos = salt.len().strict_add(4);
798    block[salt.len()..pos].copy_from_slice(&block_index.to_be_bytes());
799    block[pos] = 0x80;
800    block[112..SHA512_BLOCK_SIZE].copy_from_slice(&total_inner.strict_mul(8).to_be_bytes());
801    compress(&mut state, &block);
802  } else {
803    let mut pos = 0usize;
804
805    // Feed salt
806    let mut salt_off = 0usize;
807    while salt_off < salt.len() {
808      let space = SHA512_BLOCK_SIZE.strict_sub(pos);
809      let remaining = salt.len().strict_sub(salt_off);
810      let take = if space < remaining { space } else { remaining };
811      block[pos..pos.strict_add(take)].copy_from_slice(&salt[salt_off..salt_off.strict_add(take)]);
812      pos = pos.strict_add(take);
813      salt_off = salt_off.strict_add(take);
814      if pos == SHA512_BLOCK_SIZE {
815        compress(&mut state, &block);
816        block = [0u8; SHA512_BLOCK_SIZE];
817        pos = 0;
818      }
819    }
820
821    // Feed block_index (4 bytes big-endian)
822    for &b in &block_index.to_be_bytes() {
823      block[pos] = b;
824      pos = pos.strict_add(1);
825      if pos == SHA512_BLOCK_SIZE {
826        compress(&mut state, &block);
827        block = [0u8; SHA512_BLOCK_SIZE];
828        pos = 0;
829      }
830    }
831
832    // SHA-512 padding (16-byte length field)
833    block[pos] = 0x80;
834    if pos.strict_add(1) > 112 {
835      compress(&mut state, &block);
836      block = [0u8; SHA512_BLOCK_SIZE];
837    }
838    block[112..SHA512_BLOCK_SIZE].copy_from_slice(&total_inner.strict_mul(8).to_be_bytes());
839    compress(&mut state, &block);
840  }
841
842  // Outer hash of U1: single block
843  let mut outer_block = [0u8; SHA512_BLOCK_SIZE];
844  write_u64x8_be(&mut outer_block[..SHA512_OUTPUT_SIZE], &state);
845  outer_block[SHA512_OUTPUT_SIZE] = 0x80;
846  // total outer bytes = 128 (opad, pre-compressed) + 64 (inner hash) = 192 → 1536 bits
847  outer_block[112..SHA512_BLOCK_SIZE].copy_from_slice(&1536u128.to_be_bytes());
848
849  state = *outer_init;
850  compress(&mut state, &outer_block);
851  u_words = state;
852  result_words = u_words;
853
854  // ── Iterations 2..=c (fixed-size HMAC, 2 compress calls each) ────────
855  let mut inner_block = [0u8; SHA512_BLOCK_SIZE];
856  inner_block[SHA512_OUTPUT_SIZE] = 0x80;
857  inner_block[112..SHA512_BLOCK_SIZE].copy_from_slice(&1536u128.to_be_bytes());
858
859  for _ in 1..iterations {
860    write_u64x8_be(&mut inner_block[..SHA512_OUTPUT_SIZE], &u_words);
861    state = *inner_init;
862    compress(&mut state, &inner_block);
863
864    write_u64x8_be(&mut outer_block[..SHA512_OUTPUT_SIZE], &state);
865
866    state = *outer_init;
867    compress(&mut state, &outer_block);
868
869    u_words = state;
870
871    for (dst, &word) in result_words.iter_mut().zip(u_words.iter()) {
872      *dst ^= word;
873    }
874  }
875
876  write_u64x8_be(output, &result_words);
877
878  ct::zeroize_no_fence(&mut inner_block);
879  ct::zeroize_no_fence(&mut outer_block);
880  ct::zeroize_no_fence(&mut block);
881  zeroize_u64x8_no_fence(&mut state);
882  zeroize_u64x8_no_fence(&mut u_words);
883  zeroize_u64x8_no_fence(&mut result_words);
884  core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
885}
886
887#[allow(clippy::indexing_slicing)]
888#[inline(always)]
889fn pbkdf2_sha512_iter1(
890  compress: Sha512CompressBlocksFn,
891  inner_init: &[u64; 8],
892  outer_init: &[u64; 8],
893  salt: &[u8],
894  okm: &mut [u8],
895) {
896  if salt.len() > SHA512_INLINE_SALT_MAX {
897    let mut block_index = 1u32;
898    let mut chunks = okm.chunks_exact_mut(SHA512_OUTPUT_SIZE);
899    for chunk in chunks.by_ref() {
900      // SAFETY: chunks_exact_mut yields slices whose length is exactly SHA512_OUTPUT_SIZE.
901      let full_chunk = unsafe { &mut *(chunk.as_mut_ptr().cast::<[u8; SHA512_OUTPUT_SIZE]>()) };
902      pbkdf2_sha512_f(compress, inner_init, outer_init, salt, 1, block_index, full_chunk);
903      block_index = block_index.strict_add(1);
904    }
905    let tail = chunks.into_remainder();
906    if !tail.is_empty() {
907      let mut block_out = [0u8; SHA512_OUTPUT_SIZE];
908      pbkdf2_sha512_f(compress, inner_init, outer_init, salt, 1, block_index, &mut block_out);
909      tail.copy_from_slice(&block_out[..tail.len()]);
910      ct::zeroize(&mut block_out);
911    }
912    return;
913  }
914
915  let msg_len = salt.len().strict_add(4);
916  let total_inner = (SHA512_BLOCK_SIZE as u128).strict_add(msg_len as u128);
917  let index_pos = salt.len();
918  let pad_pos = index_pos.strict_add(4);
919
920  let mut block = [0u8; SHA512_BLOCK_SIZE];
921  block[..salt.len()].copy_from_slice(salt);
922  block[pad_pos] = 0x80;
923  block[112..SHA512_BLOCK_SIZE].copy_from_slice(&total_inner.strict_mul(8).to_be_bytes());
924
925  let mut outer_block = [0u8; SHA512_BLOCK_SIZE];
926  outer_block[SHA512_OUTPUT_SIZE] = 0x80;
927  outer_block[112..SHA512_BLOCK_SIZE].copy_from_slice(&1536u128.to_be_bytes());
928
929  let mut state = [0u64; 8];
930  let mut block_index = 1u32;
931
932  let mut chunks = okm.chunks_exact_mut(SHA512_OUTPUT_SIZE);
933  for chunk in chunks.by_ref() {
934    block[index_pos..pad_pos].copy_from_slice(&block_index.to_be_bytes());
935
936    state = *inner_init;
937    compress(&mut state, &block);
938
939    write_u64x8_be(&mut outer_block[..SHA512_OUTPUT_SIZE], &state);
940    state = *outer_init;
941    compress(&mut state, &outer_block);
942
943    write_u64x8_be(chunk, &state);
944    block_index = block_index.strict_add(1);
945  }
946
947  let tail = chunks.into_remainder();
948  if !tail.is_empty() {
949    block[index_pos..pad_pos].copy_from_slice(&block_index.to_be_bytes());
950
951    state = *inner_init;
952    compress(&mut state, &block);
953
954    write_u64x8_be(&mut outer_block[..SHA512_OUTPUT_SIZE], &state);
955    state = *outer_init;
956    compress(&mut state, &outer_block);
957
958    let mut block_out = [0u8; SHA512_OUTPUT_SIZE];
959    write_u64x8_be(&mut block_out, &state);
960    tail.copy_from_slice(&block_out[..tail.len()]);
961    ct::zeroize_no_fence(&mut block_out);
962  }
963
964  ct::zeroize_no_fence(&mut block);
965  ct::zeroize_no_fence(&mut outer_block);
966  zeroize_u64x8_no_fence(&mut state);
967  core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
968}
969
970#[cfg(test)]
971mod tests {
972  use alloc::vec;
973  use core::sync::atomic::{AtomicUsize, Ordering};
974
975  use super::*;
976
977  // ── RFC 7914 §11 PBKDF2-HMAC-SHA256 test vectors ──────────────────────
978
979  #[test]
980  fn rfc7914_sha256_vector_1() {
981    let mut dk = [0u8; 64];
982    Pbkdf2Sha256::derive_key(b"passwd", b"salt", 1, &mut dk).unwrap();
983    assert_eq!(
984      dk,
985      [
986        0x55, 0xac, 0x04, 0x6e, 0x56, 0xe3, 0x08, 0x9f, 0xec, 0x16, 0x91, 0xc2, 0x25, 0x44, 0xb6, 0x05, 0xf9, 0x41,
987        0x85, 0x21, 0x6d, 0xde, 0x04, 0x65, 0xe6, 0x8b, 0x9d, 0x57, 0xc2, 0x0d, 0xac, 0xbc, 0x49, 0xca, 0x9c, 0xcc,
988        0xf1, 0x79, 0xb6, 0x45, 0x99, 0x16, 0x64, 0xb3, 0x9d, 0x77, 0xef, 0x31, 0x7c, 0x71, 0xb8, 0x45, 0xb1, 0xe3,
989        0x0b, 0xd5, 0x09, 0x11, 0x20, 0x41, 0xd3, 0xa1, 0x97, 0x83,
990      ]
991    );
992  }
993
994  #[cfg(not(miri))]
995  #[test]
996  fn rfc7914_sha256_vector_2() {
997    let mut dk = [0u8; 64];
998    Pbkdf2Sha256::derive_key(b"Password", b"NaCl", 80000, &mut dk).unwrap();
999    assert_eq!(
1000      dk,
1001      [
1002        0x4d, 0xdc, 0xd8, 0xf6, 0x0b, 0x98, 0xbe, 0x21, 0x83, 0x0c, 0xee, 0x5e, 0xf2, 0x27, 0x01, 0xf9, 0x64, 0x1a,
1003        0x44, 0x18, 0xd0, 0x4c, 0x04, 0x14, 0xae, 0xff, 0x08, 0x87, 0x6b, 0x34, 0xab, 0x56, 0xa1, 0xd4, 0x25, 0xa1,
1004        0x22, 0x58, 0x33, 0x54, 0x9a, 0xdb, 0x84, 0x1b, 0x51, 0xc9, 0xb3, 0x17, 0x6a, 0x27, 0x2b, 0xde, 0xbb, 0xa1,
1005        0xd0, 0x78, 0x47, 0x8f, 0x62, 0xb3, 0x97, 0xf3, 0x3c, 0x8d,
1006      ]
1007    );
1008  }
1009
1010  // ── Oracle: RustCrypto PBKDF2 primitive ────────────────────────────────
1011
1012  fn oracle_sha256(password: &[u8], salt: &[u8], iterations: u32, out: &mut [u8]) {
1013    pbkdf2::pbkdf2_hmac::<sha2::Sha256>(password, salt, iterations, out);
1014  }
1015
1016  fn oracle_sha512(password: &[u8], salt: &[u8], iterations: u32, out: &mut [u8]) {
1017    pbkdf2::pbkdf2_hmac::<sha2::Sha512>(password, salt, iterations, out);
1018  }
1019
1020  #[test]
1021  fn sha256_matches_oracle() {
1022    #[cfg(not(miri))]
1023    let cases: &[(&[u8], &[u8], u32, usize)] = &[
1024      (b"password", b"salt", 1, 32),
1025      (b"password", b"salt", 2, 32),
1026      (b"password", b"salt", 4096, 32),
1027      (b"password", b"salt", 1, 64),
1028      (b"password", b"salt", 100, 64),
1029      (b"", b"salt", 1, 32),
1030      (b"password", b"", 1, 32),
1031      (b"", b"", 1, 32),
1032      (b"p", b"s", 1, 1),
1033      (b"password", b"salt", 1, 20),
1034      (b"password", b"salt", 1, 48),
1035      (b"password", b"salt", 1, 96),
1036      (b"password", b"salt", 1, 128),
1037      // Long salt (multi-block inner hash for U1)
1038      (&[0xAA; 100], b"salt", 1, 32),
1039      // Long salt spanning SHA-256 blocks
1040      (b"password", &[0xBB; 200], 1, 32),
1041      // Key longer than block size (hashed first)
1042      (&[0xCC; 128], b"salt", 1, 32),
1043    ];
1044    #[cfg(miri)]
1045    let cases: &[(&[u8], &[u8], u32, usize)] = &[
1046      (b"password", b"salt", 1, 32),
1047      (b"password", b"salt", 2, 32),
1048      (b"password", b"salt", 16, 64),
1049      (b"", b"", 1, 32),
1050      (b"p", b"s", 1, 1),
1051      (b"password", b"salt", 1, 96),
1052      (&[0xAA; 100], b"salt", 1, 32),
1053      (b"password", &[0xBB; 200], 1, 32),
1054      (&[0xCC; 128], b"salt", 1, 32),
1055    ];
1056
1057    for &(password, salt, iterations, dk_len) in cases {
1058      let mut expected = vec![0u8; dk_len];
1059      oracle_sha256(password, salt, iterations, &mut expected);
1060
1061      let mut actual = vec![0u8; dk_len];
1062      Pbkdf2Sha256::derive_key(password, salt, iterations, &mut actual).unwrap();
1063
1064      assert_eq!(
1065        actual,
1066        expected,
1067        "SHA-256 mismatch: pw_len={} salt_len={} c={} dk_len={}",
1068        password.len(),
1069        salt.len(),
1070        iterations,
1071        dk_len,
1072      );
1073    }
1074  }
1075
1076  #[test]
1077  fn sha512_matches_oracle() {
1078    #[cfg(not(miri))]
1079    let cases: &[(&[u8], &[u8], u32, usize)] = &[
1080      (b"password", b"salt", 1, 64),
1081      (b"password", b"salt", 2, 64),
1082      (b"password", b"salt", 4096, 64),
1083      (b"password", b"salt", 1, 128),
1084      (b"password", b"salt", 100, 128),
1085      (b"", b"salt", 1, 64),
1086      (b"password", b"", 1, 64),
1087      (b"", b"", 1, 64),
1088      (b"p", b"s", 1, 1),
1089      (b"password", b"salt", 1, 20),
1090      (b"password", b"salt", 1, 48),
1091      (b"password", b"salt", 1, 96),
1092      (b"password", b"salt", 1, 192),
1093      // Long salt
1094      (b"password", &[0xBB; 200], 1, 64),
1095      // Key longer than block size
1096      (&[0xCC; 200], b"salt", 1, 64),
1097    ];
1098    #[cfg(miri)]
1099    let cases: &[(&[u8], &[u8], u32, usize)] = &[
1100      (b"password", b"salt", 1, 64),
1101      (b"password", b"salt", 2, 64),
1102      (b"password", b"salt", 16, 128),
1103      (b"", b"", 1, 64),
1104      (b"p", b"s", 1, 1),
1105      (b"password", b"salt", 1, 192),
1106      (b"password", &[0xBB; 200], 1, 64),
1107      (&[0xCC; 200], b"salt", 1, 64),
1108    ];
1109
1110    for &(password, salt, iterations, dk_len) in cases {
1111      let mut expected = vec![0u8; dk_len];
1112      oracle_sha512(password, salt, iterations, &mut expected);
1113
1114      let mut actual = vec![0u8; dk_len];
1115      Pbkdf2Sha512::derive_key(password, salt, iterations, &mut actual).unwrap();
1116
1117      assert_eq!(
1118        actual,
1119        expected,
1120        "SHA-512 mismatch: pw_len={} salt_len={} c={} dk_len={}",
1121        password.len(),
1122        salt.len(),
1123        iterations,
1124        dk_len,
1125      );
1126    }
1127  }
1128
1129  // ── Verify ────────────────────────────────────────────────────────────
1130
1131  #[test]
1132  fn sha256_verify_correct_password() {
1133    let dk = Pbkdf2Sha256::derive_key_array::<32>(b"password", b"salt", 100).unwrap();
1134    assert!(Pbkdf2Sha256::verify_password(b"password", b"salt", 100, &dk).is_ok());
1135  }
1136
1137  #[test]
1138  fn sha256_verify_wrong_password() {
1139    let dk = Pbkdf2Sha256::derive_key_array::<32>(b"password", b"salt", 100).unwrap();
1140    assert!(Pbkdf2Sha256::verify_password(b"wrong", b"salt", 100, &dk).is_err());
1141  }
1142
1143  #[test]
1144  fn sha256_verify_wrong_salt() {
1145    let dk = Pbkdf2Sha256::derive_key_array::<32>(b"password", b"salt", 100).unwrap();
1146    assert!(Pbkdf2Sha256::verify_password(b"password", b"wrong", 100, &dk).is_err());
1147  }
1148
1149  #[test]
1150  fn sha256_verify_wrong_iterations() {
1151    let dk = Pbkdf2Sha256::derive_key_array::<32>(b"password", b"salt", 100).unwrap();
1152    assert!(Pbkdf2Sha256::verify_password(b"password", b"salt", 101, &dk).is_err());
1153  }
1154
1155  #[test]
1156  fn sha512_verify_correct_password() {
1157    let dk = Pbkdf2Sha512::derive_key_array::<64>(b"password", b"salt", 100).unwrap();
1158    assert!(Pbkdf2Sha512::verify_password(b"password", b"salt", 100, &dk).is_ok());
1159  }
1160
1161  #[test]
1162  fn sha512_verify_wrong_password() {
1163    let dk = Pbkdf2Sha512::derive_key_array::<64>(b"password", b"salt", 100).unwrap();
1164    assert!(Pbkdf2Sha512::verify_password(b"wrong", b"salt", 100, &dk).is_err());
1165  }
1166
1167  // ── Error paths ───────────────────────────────────────────────────────
1168
1169  #[test]
1170  fn sha256_zero_iterations_error() {
1171    let mut dk = [0u8; 32];
1172    assert_eq!(
1173      Pbkdf2Sha256::derive_key(b"pw", b"salt", 0, &mut dk),
1174      Err(Pbkdf2Error::InvalidIterations)
1175    );
1176  }
1177
1178  #[test]
1179  fn sha512_zero_iterations_error() {
1180    let mut dk = [0u8; 64];
1181    assert_eq!(
1182      Pbkdf2Sha512::derive_key(b"pw", b"salt", 0, &mut dk),
1183      Err(Pbkdf2Error::InvalidIterations)
1184    );
1185  }
1186
1187  #[test]
1188  fn sha256_empty_output_ok() {
1189    assert!(Pbkdf2Sha256::derive_key(b"pw", b"salt", 1, &mut []).is_ok());
1190  }
1191
1192  #[test]
1193  fn sha512_empty_output_ok() {
1194    assert!(Pbkdf2Sha512::derive_key(b"pw", b"salt", 1, &mut []).is_ok());
1195  }
1196
1197  #[test]
1198  fn sha256_verify_zero_iterations() {
1199    assert!(Pbkdf2Sha256::verify_password(b"pw", b"salt", 0, &[0u8; 32]).is_err());
1200  }
1201
1202  #[test]
1203  fn sha256_verify_empty_expected() {
1204    assert!(Pbkdf2Sha256::verify_password(b"pw", b"salt", 1, &[]).is_err());
1205  }
1206
1207  #[test]
1208  fn sha256_verify_password_covers_output_lengths_and_mismatch_positions() {
1209    let password = [0xA5; 97];
1210    let salt = [0x5A; 200];
1211    #[cfg(not(miri))]
1212    let output_lengths = 1..=96;
1213    #[cfg(miri)]
1214    let output_lengths = [1usize, 2, 31, 32, 33, 63, 64, 65, 95, 96].into_iter();
1215
1216    for out_len in output_lengths {
1217      let mut expected = vec![0u8; out_len];
1218      Pbkdf2Sha256::derive_key(&password, &salt, 2, &mut expected).unwrap();
1219      assert!(Pbkdf2Sha256::verify_password(&password, &salt, 2, &expected).is_ok());
1220
1221      let mut wrong_first = expected.clone();
1222      wrong_first[0] ^= 1;
1223      assert!(Pbkdf2Sha256::verify_password(&password, &salt, 2, &wrong_first).is_err());
1224
1225      let mut wrong_last = expected.clone();
1226      let last = wrong_last.len().strict_sub(1);
1227      wrong_last[last] ^= 1;
1228      assert!(Pbkdf2Sha256::verify_password(&password, &salt, 2, &wrong_last).is_err());
1229    }
1230  }
1231
1232  #[test]
1233  fn sha512_verify_password_covers_output_lengths_and_mismatch_positions() {
1234    let password = [0x3C; 193];
1235    let salt = [0xC3; 260];
1236    #[cfg(not(miri))]
1237    let output_lengths = 1..=192;
1238    #[cfg(miri)]
1239    let output_lengths = [1usize, 2, 63, 64, 65, 127, 128, 129, 191, 192].into_iter();
1240
1241    for out_len in output_lengths {
1242      let mut expected = vec![0u8; out_len];
1243      Pbkdf2Sha512::derive_key(&password, &salt, 2, &mut expected).unwrap();
1244      assert!(Pbkdf2Sha512::verify_password(&password, &salt, 2, &expected).is_ok());
1245
1246      let mut wrong_first = expected.clone();
1247      wrong_first[0] ^= 1;
1248      assert!(Pbkdf2Sha512::verify_password(&password, &salt, 2, &wrong_first).is_err());
1249
1250      let mut wrong_last = expected.clone();
1251      let last = wrong_last.len().strict_sub(1);
1252      wrong_last[last] ^= 1;
1253      assert!(Pbkdf2Sha512::verify_password(&password, &salt, 2, &wrong_last).is_err());
1254    }
1255  }
1256
1257  // ── Streaming state reuse ─────────────────────────────────────────────
1258
1259  #[test]
1260  fn sha256_state_reuse_matches_oneshot() {
1261    let state = Pbkdf2Sha256::new(b"password");
1262    let dk1 = state.derive_array::<32>(b"salt1", 100).unwrap();
1263    let dk2 = state.derive_array::<32>(b"salt2", 100).unwrap();
1264
1265    let oneshot1 = Pbkdf2Sha256::derive_key_array::<32>(b"password", b"salt1", 100).unwrap();
1266    let oneshot2 = Pbkdf2Sha256::derive_key_array::<32>(b"password", b"salt2", 100).unwrap();
1267
1268    assert_eq!(dk1, oneshot1);
1269    assert_eq!(dk2, oneshot2);
1270    assert_ne!(dk1, dk2);
1271  }
1272
1273  // ── c=1 edge case ─────────────────────────────────────────────────────
1274
1275  #[test]
1276  fn sha256_single_iteration() {
1277    let mut expected = [0u8; 32];
1278    oracle_sha256(b"pw", b"salt", 1, &mut expected);
1279    let actual = Pbkdf2Sha256::derive_key_array::<32>(b"pw", b"salt", 1).unwrap();
1280    assert_eq!(actual, expected);
1281  }
1282
1283  #[test]
1284  fn sha512_single_iteration() {
1285    let mut expected = [0u8; 64];
1286    oracle_sha512(b"pw", b"salt", 1, &mut expected);
1287    let actual = Pbkdf2Sha512::derive_key_array::<64>(b"pw", b"salt", 1).unwrap();
1288    assert_eq!(actual, expected);
1289  }
1290
1291  // ── Error traits ──────────────────────────────────────────────────────
1292
1293  #[test]
1294  fn error_is_copy() {
1295    let e = Pbkdf2Error::InvalidIterations;
1296    let e2 = e;
1297    assert_eq!(e, e2);
1298  }
1299
1300  #[test]
1301  fn error_display() {
1302    fn assert_display<T: core::fmt::Display>() {}
1303    assert_display::<Pbkdf2Error>();
1304  }
1305
1306  #[test]
1307  fn error_debug() {
1308    fn assert_debug<T: core::fmt::Debug>() {}
1309    assert_debug::<Pbkdf2Error>();
1310  }
1311
1312  #[test]
1313  fn error_implements_error_trait() {
1314    fn assert_error<T: core::error::Error>() {}
1315    assert_error::<Pbkdf2Error>();
1316  }
1317
1318  // ── Forced-kernel oracle tests ──────────────────────────────────────
1319
1320  use crate::hashes::crypto::{
1321    sha256::kernels::{
1322      Sha256KernelId, compress_blocks_fn as sha256_compress_blocks_fn, required_caps as sha256_required_caps,
1323    },
1324    sha512::kernels::{
1325      ALL as SHA512_KERNELS, Sha512KernelId, compress_blocks_fn as sha512_compress_blocks_fn,
1326      required_caps as sha512_required_caps,
1327    },
1328  };
1329
1330  /// SHA-256 kernel list (sha256 module doesn't define ALL).
1331  const SHA256_KERNELS: &[Sha256KernelId] = &[
1332    Sha256KernelId::Portable,
1333    #[cfg(target_arch = "x86_64")]
1334    Sha256KernelId::X86Sha,
1335    #[cfg(target_arch = "aarch64")]
1336    Sha256KernelId::Aarch64Sha2,
1337    #[cfg(any(target_arch = "riscv64", target_arch = "riscv32"))]
1338    Sha256KernelId::RiscvZknh,
1339    #[cfg(target_arch = "wasm32")]
1340    Sha256KernelId::WasmSimd128,
1341    #[cfg(target_arch = "s390x")]
1342    Sha256KernelId::S390xKimd,
1343  ];
1344
1345  static SHA256_VERIFY_BLOCKS: AtomicUsize = AtomicUsize::new(0);
1346  static SHA512_VERIFY_BLOCKS: AtomicUsize = AtomicUsize::new(0);
1347
1348  fn counting_sha256_compress(state: &mut [u32; 8], blocks: &[u8]) {
1349    SHA256_VERIFY_BLOCKS.fetch_add(blocks.len() / SHA256_BLOCK_SIZE, Ordering::Relaxed);
1350    sha256_compress_blocks_fn(Sha256KernelId::Portable)(state, blocks);
1351  }
1352
1353  fn counting_sha512_compress(state: &mut [u64; 8], blocks: &[u8]) {
1354    SHA512_VERIFY_BLOCKS.fetch_add(blocks.len() / SHA512_BLOCK_SIZE, Ordering::Relaxed);
1355    sha512_compress_blocks_fn(Sha512KernelId::Portable)(state, blocks);
1356  }
1357
1358  fn counted_sha256_verify(
1359    state: &Pbkdf2Sha256,
1360    salt: &[u8],
1361    iterations: u32,
1362    expected: &[u8],
1363  ) -> (Result<(), VerificationError>, usize) {
1364    SHA256_VERIFY_BLOCKS.store(0, Ordering::Relaxed);
1365    let result = state.verify(salt, iterations, expected);
1366    let blocks = SHA256_VERIFY_BLOCKS.swap(0, Ordering::Relaxed);
1367    (result, blocks)
1368  }
1369
1370  fn counted_sha512_verify(
1371    state: &Pbkdf2Sha512,
1372    salt: &[u8],
1373    iterations: u32,
1374    expected: &[u8],
1375  ) -> (Result<(), VerificationError>, usize) {
1376    SHA512_VERIFY_BLOCKS.store(0, Ordering::Relaxed);
1377    let result = state.verify(salt, iterations, expected);
1378    let blocks = SHA512_VERIFY_BLOCKS.swap(0, Ordering::Relaxed);
1379    (result, blocks)
1380  }
1381
1382  fn assert_pbkdf2_sha256_kernel(id: Sha256KernelId) {
1383    let compress = sha256_compress_blocks_fn(id);
1384    let cases: &[(&[u8], &[u8], u32, usize)] = &[
1385      (b"password", b"salt", 1, 32),
1386      (b"password", b"salt", 4, 32),
1387      (b"password", b"salt", 100, 32),
1388      (b"password", b"salt", 1, 64),      // multi-block
1389      (b"", b"salt", 1, 32),              // empty password
1390      (b"password", b"", 1, 32),          // empty salt
1391      (b"p", b"s", 1, 1),                 // minimal output
1392      (b"password", &[0xBB; 200], 1, 32), // long salt
1393      (&[0xCC; 128], b"salt", 1, 32),     // password > block_size (triggers key hashing)
1394    ];
1395
1396    for &(password, salt, iterations, dk_len) in cases {
1397      let mut expected = vec![0u8; dk_len];
1398      oracle_sha256(password, salt, iterations, &mut expected);
1399
1400      let state = Pbkdf2Sha256::new_with_compress_for_test(password, compress);
1401      let mut actual = vec![0u8; dk_len];
1402      state.derive(salt, iterations, &mut actual).unwrap();
1403
1404      assert_eq!(
1405        actual,
1406        expected,
1407        "pbkdf2-sha256 forced mismatch kernel={} pw_len={} salt_len={} c={} dk_len={}",
1408        id.as_str(),
1409        password.len(),
1410        salt.len(),
1411        iterations,
1412        dk_len,
1413      );
1414    }
1415  }
1416
1417  fn assert_pbkdf2_sha512_kernel(id: Sha512KernelId) {
1418    let compress = sha512_compress_blocks_fn(id);
1419    let cases: &[(&[u8], &[u8], u32, usize)] = &[
1420      (b"password", b"salt", 1, 64),
1421      (b"password", b"salt", 4, 64),
1422      (b"password", b"salt", 100, 64),
1423      (b"password", b"salt", 1, 128),     // multi-block
1424      (b"", b"salt", 1, 64),              // empty password
1425      (b"password", b"", 1, 64),          // empty salt
1426      (b"p", b"s", 1, 1),                 // minimal output
1427      (b"password", &[0xBB; 200], 1, 64), // long salt
1428      (&[0xCC; 200], b"salt", 1, 64),     // password > block_size
1429    ];
1430
1431    for &(password, salt, iterations, dk_len) in cases {
1432      let mut expected = vec![0u8; dk_len];
1433      oracle_sha512(password, salt, iterations, &mut expected);
1434
1435      let state = Pbkdf2Sha512::new_with_compress_for_test(password, compress);
1436      let mut actual = vec![0u8; dk_len];
1437      state.derive(salt, iterations, &mut actual).unwrap();
1438
1439      assert_eq!(
1440        actual,
1441        expected,
1442        "pbkdf2-sha512 forced mismatch kernel={} pw_len={} salt_len={} c={} dk_len={}",
1443        id.as_str(),
1444        password.len(),
1445        salt.len(),
1446        iterations,
1447        dk_len,
1448      );
1449    }
1450  }
1451
1452  #[test]
1453  fn pbkdf2_sha256_forced_kernels_match_oracle() {
1454    let caps = crate::platform::caps();
1455    for &id in SHA256_KERNELS {
1456      if caps.has(sha256_required_caps(id)) {
1457        assert_pbkdf2_sha256_kernel(id);
1458      }
1459    }
1460  }
1461
1462  #[test]
1463  fn pbkdf2_sha512_forced_kernels_match_oracle() {
1464    let caps = crate::platform::caps();
1465    for &id in SHA512_KERNELS {
1466      if caps.has(sha512_required_caps(id)) {
1467        assert_pbkdf2_sha512_kernel(id);
1468      }
1469    }
1470  }
1471
1472  #[test]
1473  fn sha256_verify_keeps_same_compress_work_for_match_and_mismatch_positions() {
1474    let password = [0x11; 89];
1475    let salt = [0x22; 200];
1476    let state = Pbkdf2Sha256::new_with_compress_for_test(&password, counting_sha256_compress);
1477    #[cfg(not(miri))]
1478    let output_lengths = 1..=96;
1479    #[cfg(miri)]
1480    let output_lengths = [1usize, 2, 31, 32, 33, 63, 64, 65, 95, 96].into_iter();
1481
1482    for out_len in output_lengths {
1483      let mut expected = vec![0u8; out_len];
1484      state.derive(&salt, 3, &mut expected).unwrap();
1485
1486      let (ok, ok_blocks) = counted_sha256_verify(&state, &salt, 3, &expected);
1487      assert!(ok.is_ok(), "sha256 verify must accept correct output_len={out_len}");
1488
1489      let mut wrong_first = expected.clone();
1490      wrong_first[0] ^= 1;
1491      let (wrong_first_result, wrong_first_blocks) = counted_sha256_verify(&state, &salt, 3, &wrong_first);
1492      assert!(
1493        wrong_first_result.is_err(),
1494        "sha256 verify must reject first-byte mismatch output_len={out_len}"
1495      );
1496
1497      let mut wrong_last = expected.clone();
1498      let last = wrong_last.len().strict_sub(1);
1499      wrong_last[last] ^= 1;
1500      let (wrong_last_result, wrong_last_blocks) = counted_sha256_verify(&state, &salt, 3, &wrong_last);
1501      assert!(
1502        wrong_last_result.is_err(),
1503        "sha256 verify must reject last-byte mismatch output_len={out_len}"
1504      );
1505
1506      assert!(ok_blocks > 0, "sha256 verify must do real work output_len={out_len}");
1507      assert_eq!(
1508        ok_blocks, wrong_first_blocks,
1509        "sha256 verify must not short-circuit on first-byte mismatch output_len={out_len}"
1510      );
1511      assert_eq!(
1512        ok_blocks, wrong_last_blocks,
1513        "sha256 verify must not short-circuit on last-byte mismatch output_len={out_len}"
1514      );
1515    }
1516  }
1517
1518  #[test]
1519  fn sha512_verify_keeps_same_compress_work_for_match_and_mismatch_positions() {
1520    let password = [0x44; 161];
1521    let salt = [0x55; 260];
1522    let state = Pbkdf2Sha512::new_with_compress_for_test(&password, counting_sha512_compress);
1523    #[cfg(not(miri))]
1524    let output_lengths = 1..=192;
1525    #[cfg(miri)]
1526    let output_lengths = [1usize, 2, 63, 64, 65, 127, 128, 129, 191, 192].into_iter();
1527
1528    for out_len in output_lengths {
1529      let mut expected = vec![0u8; out_len];
1530      state.derive(&salt, 3, &mut expected).unwrap();
1531
1532      let (ok, ok_blocks) = counted_sha512_verify(&state, &salt, 3, &expected);
1533      assert!(ok.is_ok(), "sha512 verify must accept correct output_len={out_len}");
1534
1535      let mut wrong_first = expected.clone();
1536      wrong_first[0] ^= 1;
1537      let (wrong_first_result, wrong_first_blocks) = counted_sha512_verify(&state, &salt, 3, &wrong_first);
1538      assert!(
1539        wrong_first_result.is_err(),
1540        "sha512 verify must reject first-byte mismatch output_len={out_len}"
1541      );
1542
1543      let mut wrong_last = expected.clone();
1544      let last = wrong_last.len().strict_sub(1);
1545      wrong_last[last] ^= 1;
1546      let (wrong_last_result, wrong_last_blocks) = counted_sha512_verify(&state, &salt, 3, &wrong_last);
1547      assert!(
1548        wrong_last_result.is_err(),
1549        "sha512 verify must reject last-byte mismatch output_len={out_len}"
1550      );
1551
1552      assert!(ok_blocks > 0, "sha512 verify must do real work output_len={out_len}");
1553      assert_eq!(
1554        ok_blocks, wrong_first_blocks,
1555        "sha512 verify must not short-circuit on first-byte mismatch output_len={out_len}"
1556      );
1557      assert_eq!(
1558        ok_blocks, wrong_last_blocks,
1559        "sha512 verify must not short-circuit on last-byte mismatch output_len={out_len}"
1560      );
1561    }
1562  }
1563}