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      pub fn derive_key(password: &[u8], salt: &[u8], iterations: u32, okm: &mut [u8]) -> Result<(), Pbkdf2Error> {
326        Self::new(password).derive(salt, iterations, okm)
327      }
328
329      /// Derive a key into a fixed-size array in one shot.
330      #[inline]
331      pub fn derive_key_array<const N: usize>(
332        password: &[u8],
333        salt: &[u8],
334        iterations: u32,
335      ) -> Result<[u8; N], Pbkdf2Error> {
336        Self::new(password).derive_array(salt, iterations)
337      }
338
339      /// Verify a password in one shot.
340      #[inline]
341      #[must_use = "password verification must be checked; a dropped Result silently accepts the wrong password"]
342      pub fn verify_password(
343        password: &[u8],
344        salt: &[u8],
345        iterations: u32,
346        expected: &[u8],
347      ) -> Result<(), VerificationError> {
348        Self::new(password).verify(salt, iterations, expected)
349      }
350
351      /// Test-only: build with a specific digest compress function.
352      #[cfg(test)]
353      #[allow(clippy::indexing_slicing)]
354      pub(crate) fn new_with_compress_for_test(password: &[u8], compress: $compress_ty) -> Self {
355        let mut key_block = [0u8; $block_size_const];
356        if password.len() > $block_size_const {
357          key_block[..$output_size_const].copy_from_slice(&$test_oneshot(password, compress));
358        } else {
359          key_block[..password.len()].copy_from_slice(password);
360        }
361
362        let (inner_init, outer_init) = hmac_prefix_state(&mut key_block, |ipad, opad| {
363          let mut inner_init = $h0;
364          compress(&mut inner_init, ipad);
365
366          let mut outer_init = $h0;
367          compress(&mut outer_init, opad);
368
369          (inner_init, outer_init)
370        });
371
372        Self {
373          inner_init,
374          outer_init,
375          compress,
376        }
377      }
378    }
379
380    impl Drop for $name {
381      fn drop(&mut self) {
382        for word in self.inner_init.iter_mut().chain(self.outer_init.iter_mut()) {
383          // SAFETY: word is a valid, aligned, dereferenceable pointer to initialized memory.
384          unsafe { core::ptr::write_volatile(word, 0) };
385        }
386        core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
387      }
388    }
389  };
390}
391
392// ─── PBKDF2-HMAC-SHA256 ────────────────────────────────────────────────────
393
394define_pbkdf2_sha2! {
395  /// PBKDF2-HMAC-SHA256 key derivation (RFC 2898).
396  ///
397  /// Pre-computes the HMAC-SHA256 prefix states from the password so that
398  /// subsequent `derive` and `verify` calls run the iteration loop directly
399  /// against the SHA-256 compress function with no per-iteration overhead.
400  ///
401  /// # Examples
402  ///
403  /// ```rust
404  /// use rscrypto::Pbkdf2Sha256;
405  ///
406  /// // Derive a 32-byte key
407  /// let dk = Pbkdf2Sha256::derive_key_array::<32>(b"password", b"salt", 600_000)?;
408  ///
409  /// // Re-use pre-computed state for multiple operations
410  /// let state = Pbkdf2Sha256::new(b"password");
411  /// let dk2 = state.derive_array::<32>(b"salt", 600_000)?;
412  /// assert_eq!(dk, dk2);
413  ///
414  /// // Verify
415  /// assert!(state.verify(b"salt", 600_000, &dk).is_ok());
416  /// # Ok::<(), rscrypto::auth::Pbkdf2Error>(())
417  /// ```
418  Pbkdf2Sha256 {
419    output_size_const: SHA256_OUTPUT_SIZE,
420    block_size_const: SHA256_BLOCK_SIZE,
421    compress_ty: Sha256CompressBlocksFn,
422    digest_ty: Sha256,
423    h0: SHA256_H0,
424    dispatch: sha256_dispatch,
425    f_fn: pbkdf2_sha256_f,
426    iter1_fn: pbkdf2_sha256_iter1,
427    test_oneshot: sha256_oneshot_with_compress,
428    word_ty: u32,
429    recommended_iterations: 600_000,
430  }
431}
432
433/// Test-only: one-shot SHA-256 digest using a specific compress function.
434#[cfg(test)]
435#[allow(clippy::indexing_slicing)]
436fn sha256_oneshot_with_compress(data: &[u8], compress: Sha256CompressBlocksFn) -> [u8; SHA256_OUTPUT_SIZE] {
437  let mut state = SHA256_H0;
438  let mut pos = 0usize;
439  while pos.strict_add(SHA256_BLOCK_SIZE) <= data.len() {
440    compress(&mut state, &data[pos..pos.strict_add(SHA256_BLOCK_SIZE)]);
441    pos = pos.strict_add(SHA256_BLOCK_SIZE);
442  }
443  let mut block = [0u8; SHA256_BLOCK_SIZE];
444  let tail = data.len().strict_sub(pos);
445  block[..tail].copy_from_slice(&data[pos..]);
446  block[tail] = 0x80;
447  if tail >= 56 {
448    compress(&mut state, &block);
449    block = [0u8; SHA256_BLOCK_SIZE];
450  }
451  block[56..64].copy_from_slice(&(data.len() as u64).strict_mul(8).to_be_bytes());
452  compress(&mut state, &block);
453  let mut out = [0u8; SHA256_OUTPUT_SIZE];
454  for (chunk, &word) in out.chunks_exact_mut(4).zip(state.iter()) {
455    chunk.copy_from_slice(&word.to_be_bytes());
456  }
457  out
458}
459
460/// Compute one PBKDF2-SHA256 block: `F(Password, Salt, c, i)`.
461///
462/// Each HMAC iteration in the hot loop runs exactly 2 SHA-256 compress calls
463/// using pre-padded block templates — no hash struct creation, no dispatch
464/// overhead, no padding recomputation.
465#[allow(clippy::indexing_slicing)]
466#[inline(always)]
467fn pbkdf2_sha256_f(
468  compress: Sha256CompressBlocksFn,
469  inner_init: &[u32; 8],
470  outer_init: &[u32; 8],
471  salt: &[u8],
472  iterations: u32,
473  block_index: u32,
474  output: &mut [u8; SHA256_OUTPUT_SIZE],
475) {
476  let mut state: [u32; 8];
477  let mut u_words: [u32; 8];
478  let mut result_words: [u32; 8];
479
480  // ── U1 = HMAC(Password, Salt || INT_32_BE(block_index)) ──────────────
481  state = *inner_init;
482  let msg_len = salt.len().strict_add(4);
483  let total_inner = (SHA256_BLOCK_SIZE as u64).strict_add(msg_len as u64);
484
485  let mut block = [0u8; SHA256_BLOCK_SIZE];
486  if salt.len() <= SHA256_INLINE_SALT_MAX {
487    block[..salt.len()].copy_from_slice(salt);
488    let pos = salt.len().strict_add(4);
489    block[salt.len()..pos].copy_from_slice(&block_index.to_be_bytes());
490    block[pos] = 0x80;
491    block[56..SHA256_BLOCK_SIZE].copy_from_slice(&total_inner.strict_mul(8).to_be_bytes());
492    compress(&mut state, &block);
493  } else {
494    let mut pos = 0usize;
495
496    // Feed salt
497    let mut salt_off = 0usize;
498    while salt_off < salt.len() {
499      let space = SHA256_BLOCK_SIZE.strict_sub(pos);
500      let remaining = salt.len().strict_sub(salt_off);
501      let take = if space < remaining { space } else { remaining };
502      block[pos..pos.strict_add(take)].copy_from_slice(&salt[salt_off..salt_off.strict_add(take)]);
503      pos = pos.strict_add(take);
504      salt_off = salt_off.strict_add(take);
505      if pos == SHA256_BLOCK_SIZE {
506        compress(&mut state, &block);
507        block = [0u8; SHA256_BLOCK_SIZE];
508        pos = 0;
509      }
510    }
511
512    // Feed block_index (4 bytes big-endian)
513    for &b in &block_index.to_be_bytes() {
514      block[pos] = b;
515      pos = pos.strict_add(1);
516      if pos == SHA256_BLOCK_SIZE {
517        compress(&mut state, &block);
518        block = [0u8; SHA256_BLOCK_SIZE];
519        pos = 0;
520      }
521    }
522
523    // SHA-256 padding
524    block[pos] = 0x80;
525    if pos.strict_add(1) > 56 {
526      compress(&mut state, &block);
527      block = [0u8; SHA256_BLOCK_SIZE];
528    }
529    block[56..SHA256_BLOCK_SIZE].copy_from_slice(&total_inner.strict_mul(8).to_be_bytes());
530    compress(&mut state, &block);
531  }
532
533  // Outer hash of U1: single block
534  let mut outer_block = [0u8; SHA256_BLOCK_SIZE];
535  write_u32x8_be(&mut outer_block[..SHA256_OUTPUT_SIZE], &state);
536  outer_block[SHA256_OUTPUT_SIZE] = 0x80;
537  // total outer bytes = 64 (opad, pre-compressed) + 32 (inner hash) = 96 → 768 bits
538  outer_block[56..SHA256_BLOCK_SIZE].copy_from_slice(&768u64.to_be_bytes());
539
540  state = *outer_init;
541  compress(&mut state, &outer_block);
542  u_words = state;
543  result_words = u_words;
544
545  if iterations == 1 {
546    write_u32x8_be(output, &result_words);
547    ct::zeroize_no_fence(&mut outer_block);
548    ct::zeroize_no_fence(&mut block);
549    zeroize_u32x8_no_fence(&mut state);
550    zeroize_u32x8_no_fence(&mut u_words);
551    zeroize_u32x8_no_fence(&mut result_words);
552    core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
553    return;
554  }
555
556  // ── Iterations 2..=c (fixed-size HMAC, 2 compress calls each) ────────
557  // Inner template: [32-byte U] [0x80] [zeros] [768-bit length]
558  let mut inner_block = [0u8; SHA256_BLOCK_SIZE];
559  inner_block[SHA256_OUTPUT_SIZE] = 0x80;
560  inner_block[56..SHA256_BLOCK_SIZE].copy_from_slice(&768u64.to_be_bytes());
561
562  for _ in 1..iterations {
563    // Inner: compress(inner_init, U_{j-1} || padding)
564    write_u32x8_be(&mut inner_block[..SHA256_OUTPUT_SIZE], &u_words);
565    state = *inner_init;
566    compress(&mut state, &inner_block);
567
568    // Serialize inner hash directly into outer block
569    write_u32x8_be(&mut outer_block[..SHA256_OUTPUT_SIZE], &state);
570
571    // Outer: compress(outer_init, inner_hash || padding)
572    state = *outer_init;
573    compress(&mut state, &outer_block);
574
575    u_words = state;
576
577    // XOR into output
578    for (dst, &word) in result_words.iter_mut().zip(u_words.iter()) {
579      *dst ^= word;
580    }
581  }
582
583  write_u32x8_be(output, &result_words);
584
585  // Zeroize sensitive state
586  ct::zeroize_no_fence(&mut inner_block);
587  ct::zeroize_no_fence(&mut outer_block);
588  ct::zeroize_no_fence(&mut block);
589  zeroize_u32x8_no_fence(&mut state);
590  zeroize_u32x8_no_fence(&mut u_words);
591  zeroize_u32x8_no_fence(&mut result_words);
592  core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
593}
594
595#[allow(clippy::indexing_slicing)]
596#[inline(always)]
597fn pbkdf2_sha256_iter1(
598  compress: Sha256CompressBlocksFn,
599  inner_init: &[u32; 8],
600  outer_init: &[u32; 8],
601  salt: &[u8],
602  okm: &mut [u8],
603) {
604  if salt.len() > SHA256_INLINE_SALT_MAX {
605    let mut block_index = 1u32;
606    let mut chunks = okm.chunks_exact_mut(SHA256_OUTPUT_SIZE);
607    for chunk in chunks.by_ref() {
608      // SAFETY: chunks_exact_mut yields slices whose length is exactly SHA256_OUTPUT_SIZE.
609      let full_chunk = unsafe { &mut *(chunk.as_mut_ptr().cast::<[u8; SHA256_OUTPUT_SIZE]>()) };
610      pbkdf2_sha256_f(compress, inner_init, outer_init, salt, 1, block_index, full_chunk);
611      block_index = block_index.strict_add(1);
612    }
613    let tail = chunks.into_remainder();
614    if !tail.is_empty() {
615      let mut block_out = [0u8; SHA256_OUTPUT_SIZE];
616      pbkdf2_sha256_f(compress, inner_init, outer_init, salt, 1, block_index, &mut block_out);
617      tail.copy_from_slice(&block_out[..tail.len()]);
618      ct::zeroize(&mut block_out);
619    }
620    return;
621  }
622
623  let msg_len = salt.len().strict_add(4);
624  let total_inner = (SHA256_BLOCK_SIZE as u64).strict_add(msg_len as u64);
625  let index_pos = salt.len();
626  let pad_pos = index_pos.strict_add(4);
627
628  let mut block = [0u8; SHA256_BLOCK_SIZE];
629  block[..salt.len()].copy_from_slice(salt);
630  block[pad_pos] = 0x80;
631  block[56..SHA256_BLOCK_SIZE].copy_from_slice(&total_inner.strict_mul(8).to_be_bytes());
632
633  let mut outer_block = [0u8; SHA256_BLOCK_SIZE];
634  outer_block[SHA256_OUTPUT_SIZE] = 0x80;
635  outer_block[56..SHA256_BLOCK_SIZE].copy_from_slice(&768u64.to_be_bytes());
636
637  let mut state = [0u32; 8];
638  let mut block_index = 1u32;
639
640  let mut chunks = okm.chunks_exact_mut(SHA256_OUTPUT_SIZE);
641  for chunk in chunks.by_ref() {
642    block[index_pos..pad_pos].copy_from_slice(&block_index.to_be_bytes());
643
644    state = *inner_init;
645    compress(&mut state, &block);
646
647    write_u32x8_be(&mut outer_block[..SHA256_OUTPUT_SIZE], &state);
648    state = *outer_init;
649    compress(&mut state, &outer_block);
650
651    write_u32x8_be(chunk, &state);
652    block_index = block_index.strict_add(1);
653  }
654
655  let tail = chunks.into_remainder();
656  if !tail.is_empty() {
657    block[index_pos..pad_pos].copy_from_slice(&block_index.to_be_bytes());
658
659    state = *inner_init;
660    compress(&mut state, &block);
661
662    write_u32x8_be(&mut outer_block[..SHA256_OUTPUT_SIZE], &state);
663    state = *outer_init;
664    compress(&mut state, &outer_block);
665
666    let mut block_out = [0u8; SHA256_OUTPUT_SIZE];
667    write_u32x8_be(&mut block_out, &state);
668    tail.copy_from_slice(&block_out[..tail.len()]);
669    ct::zeroize_no_fence(&mut block_out);
670  }
671
672  ct::zeroize_no_fence(&mut block);
673  ct::zeroize_no_fence(&mut outer_block);
674  zeroize_u32x8_no_fence(&mut state);
675  core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
676}
677
678// ─── PBKDF2-HMAC-SHA512 ────────────────────────────────────────────────────
679
680define_pbkdf2_sha2! {
681  /// PBKDF2-HMAC-SHA512 key derivation (RFC 2898).
682  ///
683  /// Pre-computes the HMAC-SHA512 prefix states from the password so that
684  /// subsequent `derive` and `verify` calls run the iteration loop directly
685  /// against the SHA-512 compress function with no per-iteration overhead.
686  ///
687  /// # Examples
688  ///
689  /// ```rust
690  /// use rscrypto::Pbkdf2Sha512;
691  ///
692  /// let dk = Pbkdf2Sha512::derive_key_array::<64>(b"password", b"salt", 210_000)?;
693  /// assert!(Pbkdf2Sha512::verify_password(b"password", b"salt", 210_000, &dk).is_ok());
694  /// # Ok::<(), rscrypto::auth::Pbkdf2Error>(())
695  /// ```
696  Pbkdf2Sha512 {
697    output_size_const: SHA512_OUTPUT_SIZE,
698    block_size_const: SHA512_BLOCK_SIZE,
699    compress_ty: Sha512CompressBlocksFn,
700    digest_ty: Sha512,
701    h0: SHA512_H0,
702    dispatch: sha512_dispatch,
703    f_fn: pbkdf2_sha512_f,
704    iter1_fn: pbkdf2_sha512_iter1,
705    test_oneshot: sha512_oneshot_with_compress,
706    word_ty: u64,
707    recommended_iterations: 210_000,
708  }
709}
710
711/// Test-only: one-shot SHA-512 digest using a specific compress function.
712#[cfg(test)]
713#[allow(clippy::indexing_slicing)]
714fn sha512_oneshot_with_compress(data: &[u8], compress: Sha512CompressBlocksFn) -> [u8; SHA512_OUTPUT_SIZE] {
715  let mut state = SHA512_H0;
716  let mut pos = 0usize;
717  while pos.strict_add(SHA512_BLOCK_SIZE) <= data.len() {
718    compress(&mut state, &data[pos..pos.strict_add(SHA512_BLOCK_SIZE)]);
719    pos = pos.strict_add(SHA512_BLOCK_SIZE);
720  }
721  let mut block = [0u8; SHA512_BLOCK_SIZE];
722  let tail = data.len().strict_sub(pos);
723  block[..tail].copy_from_slice(&data[pos..]);
724  block[tail] = 0x80;
725  if tail >= 112 {
726    compress(&mut state, &block);
727    block = [0u8; SHA512_BLOCK_SIZE];
728  }
729  block[112..128].copy_from_slice(&(data.len() as u128).strict_mul(8).to_be_bytes());
730  compress(&mut state, &block);
731  let mut out = [0u8; SHA512_OUTPUT_SIZE];
732  for (chunk, &word) in out.chunks_exact_mut(8).zip(state.iter()) {
733    chunk.copy_from_slice(&word.to_be_bytes());
734  }
735  out
736}
737
738/// Compute one PBKDF2-SHA512 block: `F(Password, Salt, c, i)`.
739#[allow(clippy::indexing_slicing)]
740#[inline(always)]
741fn pbkdf2_sha512_f(
742  compress: Sha512CompressBlocksFn,
743  inner_init: &[u64; 8],
744  outer_init: &[u64; 8],
745  salt: &[u8],
746  iterations: u32,
747  block_index: u32,
748  output: &mut [u8; SHA512_OUTPUT_SIZE],
749) {
750  let mut state: [u64; 8];
751  let mut u_words: [u64; 8];
752  let mut result_words: [u64; 8];
753
754  // ── U1 = HMAC(Password, Salt || INT_32_BE(block_index)) ──────────────
755  state = *inner_init;
756  let msg_len = salt.len().strict_add(4);
757  let total_inner = (SHA512_BLOCK_SIZE as u128).strict_add(msg_len as u128);
758
759  let mut block = [0u8; SHA512_BLOCK_SIZE];
760  if salt.len() <= SHA512_INLINE_SALT_MAX {
761    block[..salt.len()].copy_from_slice(salt);
762    let pos = salt.len().strict_add(4);
763    block[salt.len()..pos].copy_from_slice(&block_index.to_be_bytes());
764    block[pos] = 0x80;
765    block[112..SHA512_BLOCK_SIZE].copy_from_slice(&total_inner.strict_mul(8).to_be_bytes());
766    compress(&mut state, &block);
767  } else {
768    let mut pos = 0usize;
769
770    // Feed salt
771    let mut salt_off = 0usize;
772    while salt_off < salt.len() {
773      let space = SHA512_BLOCK_SIZE.strict_sub(pos);
774      let remaining = salt.len().strict_sub(salt_off);
775      let take = if space < remaining { space } else { remaining };
776      block[pos..pos.strict_add(take)].copy_from_slice(&salt[salt_off..salt_off.strict_add(take)]);
777      pos = pos.strict_add(take);
778      salt_off = salt_off.strict_add(take);
779      if pos == SHA512_BLOCK_SIZE {
780        compress(&mut state, &block);
781        block = [0u8; SHA512_BLOCK_SIZE];
782        pos = 0;
783      }
784    }
785
786    // Feed block_index (4 bytes big-endian)
787    for &b in &block_index.to_be_bytes() {
788      block[pos] = b;
789      pos = pos.strict_add(1);
790      if pos == SHA512_BLOCK_SIZE {
791        compress(&mut state, &block);
792        block = [0u8; SHA512_BLOCK_SIZE];
793        pos = 0;
794      }
795    }
796
797    // SHA-512 padding (16-byte length field)
798    block[pos] = 0x80;
799    if pos.strict_add(1) > 112 {
800      compress(&mut state, &block);
801      block = [0u8; SHA512_BLOCK_SIZE];
802    }
803    block[112..SHA512_BLOCK_SIZE].copy_from_slice(&total_inner.strict_mul(8).to_be_bytes());
804    compress(&mut state, &block);
805  }
806
807  // Outer hash of U1: single block
808  let mut outer_block = [0u8; SHA512_BLOCK_SIZE];
809  write_u64x8_be(&mut outer_block[..SHA512_OUTPUT_SIZE], &state);
810  outer_block[SHA512_OUTPUT_SIZE] = 0x80;
811  // total outer bytes = 128 (opad, pre-compressed) + 64 (inner hash) = 192 → 1536 bits
812  outer_block[112..SHA512_BLOCK_SIZE].copy_from_slice(&1536u128.to_be_bytes());
813
814  state = *outer_init;
815  compress(&mut state, &outer_block);
816  u_words = state;
817  result_words = u_words;
818
819  // ── Iterations 2..=c (fixed-size HMAC, 2 compress calls each) ────────
820  let mut inner_block = [0u8; SHA512_BLOCK_SIZE];
821  inner_block[SHA512_OUTPUT_SIZE] = 0x80;
822  inner_block[112..SHA512_BLOCK_SIZE].copy_from_slice(&1536u128.to_be_bytes());
823
824  for _ in 1..iterations {
825    write_u64x8_be(&mut inner_block[..SHA512_OUTPUT_SIZE], &u_words);
826    state = *inner_init;
827    compress(&mut state, &inner_block);
828
829    write_u64x8_be(&mut outer_block[..SHA512_OUTPUT_SIZE], &state);
830
831    state = *outer_init;
832    compress(&mut state, &outer_block);
833
834    u_words = state;
835
836    for (dst, &word) in result_words.iter_mut().zip(u_words.iter()) {
837      *dst ^= word;
838    }
839  }
840
841  write_u64x8_be(output, &result_words);
842
843  ct::zeroize_no_fence(&mut inner_block);
844  ct::zeroize_no_fence(&mut outer_block);
845  ct::zeroize_no_fence(&mut block);
846  zeroize_u64x8_no_fence(&mut state);
847  zeroize_u64x8_no_fence(&mut u_words);
848  zeroize_u64x8_no_fence(&mut result_words);
849  core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
850}
851
852#[allow(clippy::indexing_slicing)]
853#[inline(always)]
854fn pbkdf2_sha512_iter1(
855  compress: Sha512CompressBlocksFn,
856  inner_init: &[u64; 8],
857  outer_init: &[u64; 8],
858  salt: &[u8],
859  okm: &mut [u8],
860) {
861  if salt.len() > SHA512_INLINE_SALT_MAX {
862    let mut block_index = 1u32;
863    let mut chunks = okm.chunks_exact_mut(SHA512_OUTPUT_SIZE);
864    for chunk in chunks.by_ref() {
865      // SAFETY: chunks_exact_mut yields slices whose length is exactly SHA512_OUTPUT_SIZE.
866      let full_chunk = unsafe { &mut *(chunk.as_mut_ptr().cast::<[u8; SHA512_OUTPUT_SIZE]>()) };
867      pbkdf2_sha512_f(compress, inner_init, outer_init, salt, 1, block_index, full_chunk);
868      block_index = block_index.strict_add(1);
869    }
870    let tail = chunks.into_remainder();
871    if !tail.is_empty() {
872      let mut block_out = [0u8; SHA512_OUTPUT_SIZE];
873      pbkdf2_sha512_f(compress, inner_init, outer_init, salt, 1, block_index, &mut block_out);
874      tail.copy_from_slice(&block_out[..tail.len()]);
875      ct::zeroize(&mut block_out);
876    }
877    return;
878  }
879
880  let msg_len = salt.len().strict_add(4);
881  let total_inner = (SHA512_BLOCK_SIZE as u128).strict_add(msg_len as u128);
882  let index_pos = salt.len();
883  let pad_pos = index_pos.strict_add(4);
884
885  let mut block = [0u8; SHA512_BLOCK_SIZE];
886  block[..salt.len()].copy_from_slice(salt);
887  block[pad_pos] = 0x80;
888  block[112..SHA512_BLOCK_SIZE].copy_from_slice(&total_inner.strict_mul(8).to_be_bytes());
889
890  let mut outer_block = [0u8; SHA512_BLOCK_SIZE];
891  outer_block[SHA512_OUTPUT_SIZE] = 0x80;
892  outer_block[112..SHA512_BLOCK_SIZE].copy_from_slice(&1536u128.to_be_bytes());
893
894  let mut state = [0u64; 8];
895  let mut block_index = 1u32;
896
897  let mut chunks = okm.chunks_exact_mut(SHA512_OUTPUT_SIZE);
898  for chunk in chunks.by_ref() {
899    block[index_pos..pad_pos].copy_from_slice(&block_index.to_be_bytes());
900
901    state = *inner_init;
902    compress(&mut state, &block);
903
904    write_u64x8_be(&mut outer_block[..SHA512_OUTPUT_SIZE], &state);
905    state = *outer_init;
906    compress(&mut state, &outer_block);
907
908    write_u64x8_be(chunk, &state);
909    block_index = block_index.strict_add(1);
910  }
911
912  let tail = chunks.into_remainder();
913  if !tail.is_empty() {
914    block[index_pos..pad_pos].copy_from_slice(&block_index.to_be_bytes());
915
916    state = *inner_init;
917    compress(&mut state, &block);
918
919    write_u64x8_be(&mut outer_block[..SHA512_OUTPUT_SIZE], &state);
920    state = *outer_init;
921    compress(&mut state, &outer_block);
922
923    let mut block_out = [0u8; SHA512_OUTPUT_SIZE];
924    write_u64x8_be(&mut block_out, &state);
925    tail.copy_from_slice(&block_out[..tail.len()]);
926    ct::zeroize_no_fence(&mut block_out);
927  }
928
929  ct::zeroize_no_fence(&mut block);
930  ct::zeroize_no_fence(&mut outer_block);
931  zeroize_u64x8_no_fence(&mut state);
932  core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
933}
934
935#[cfg(test)]
936mod tests {
937  use alloc::vec;
938  use core::sync::atomic::{AtomicUsize, Ordering};
939
940  use super::*;
941
942  // ── RFC 7914 §11 PBKDF2-HMAC-SHA256 test vectors ──────────────────────
943
944  #[test]
945  fn rfc7914_sha256_vector_1() {
946    let mut dk = [0u8; 64];
947    Pbkdf2Sha256::derive_key(b"passwd", b"salt", 1, &mut dk).unwrap();
948    assert_eq!(
949      dk,
950      [
951        0x55, 0xac, 0x04, 0x6e, 0x56, 0xe3, 0x08, 0x9f, 0xec, 0x16, 0x91, 0xc2, 0x25, 0x44, 0xb6, 0x05, 0xf9, 0x41,
952        0x85, 0x21, 0x6d, 0xde, 0x04, 0x65, 0xe6, 0x8b, 0x9d, 0x57, 0xc2, 0x0d, 0xac, 0xbc, 0x49, 0xca, 0x9c, 0xcc,
953        0xf1, 0x79, 0xb6, 0x45, 0x99, 0x16, 0x64, 0xb3, 0x9d, 0x77, 0xef, 0x31, 0x7c, 0x71, 0xb8, 0x45, 0xb1, 0xe3,
954        0x0b, 0xd5, 0x09, 0x11, 0x20, 0x41, 0xd3, 0xa1, 0x97, 0x83,
955      ]
956    );
957  }
958
959  #[cfg(not(miri))]
960  #[test]
961  fn rfc7914_sha256_vector_2() {
962    let mut dk = [0u8; 64];
963    Pbkdf2Sha256::derive_key(b"Password", b"NaCl", 80000, &mut dk).unwrap();
964    assert_eq!(
965      dk,
966      [
967        0x4d, 0xdc, 0xd8, 0xf6, 0x0b, 0x98, 0xbe, 0x21, 0x83, 0x0c, 0xee, 0x5e, 0xf2, 0x27, 0x01, 0xf9, 0x64, 0x1a,
968        0x44, 0x18, 0xd0, 0x4c, 0x04, 0x14, 0xae, 0xff, 0x08, 0x87, 0x6b, 0x34, 0xab, 0x56, 0xa1, 0xd4, 0x25, 0xa1,
969        0x22, 0x58, 0x33, 0x54, 0x9a, 0xdb, 0x84, 0x1b, 0x51, 0xc9, 0xb3, 0x17, 0x6a, 0x27, 0x2b, 0xde, 0xbb, 0xa1,
970        0xd0, 0x78, 0x47, 0x8f, 0x62, 0xb3, 0x97, 0xf3, 0x3c, 0x8d,
971      ]
972    );
973  }
974
975  // ── Oracle: RustCrypto PBKDF2 primitive ────────────────────────────────
976
977  fn oracle_sha256(password: &[u8], salt: &[u8], iterations: u32, out: &mut [u8]) {
978    pbkdf2::pbkdf2_hmac::<sha2::Sha256>(password, salt, iterations, out);
979  }
980
981  fn oracle_sha512(password: &[u8], salt: &[u8], iterations: u32, out: &mut [u8]) {
982    pbkdf2::pbkdf2_hmac::<sha2::Sha512>(password, salt, iterations, out);
983  }
984
985  #[test]
986  fn sha256_matches_oracle() {
987    #[cfg(not(miri))]
988    let cases: &[(&[u8], &[u8], u32, usize)] = &[
989      (b"password", b"salt", 1, 32),
990      (b"password", b"salt", 2, 32),
991      (b"password", b"salt", 4096, 32),
992      (b"password", b"salt", 1, 64),
993      (b"password", b"salt", 100, 64),
994      (b"", b"salt", 1, 32),
995      (b"password", b"", 1, 32),
996      (b"", b"", 1, 32),
997      (b"p", b"s", 1, 1),
998      (b"password", b"salt", 1, 20),
999      (b"password", b"salt", 1, 48),
1000      (b"password", b"salt", 1, 96),
1001      (b"password", b"salt", 1, 128),
1002      // Long salt (multi-block inner hash for U1)
1003      (&[0xAA; 100], b"salt", 1, 32),
1004      // Long salt spanning SHA-256 blocks
1005      (b"password", &[0xBB; 200], 1, 32),
1006      // Key longer than block size (hashed first)
1007      (&[0xCC; 128], b"salt", 1, 32),
1008    ];
1009    #[cfg(miri)]
1010    let cases: &[(&[u8], &[u8], u32, usize)] = &[
1011      (b"password", b"salt", 1, 32),
1012      (b"password", b"salt", 2, 32),
1013      (b"password", b"salt", 16, 64),
1014      (b"", b"", 1, 32),
1015      (b"p", b"s", 1, 1),
1016      (b"password", b"salt", 1, 96),
1017      (&[0xAA; 100], b"salt", 1, 32),
1018      (b"password", &[0xBB; 200], 1, 32),
1019      (&[0xCC; 128], b"salt", 1, 32),
1020    ];
1021
1022    for &(password, salt, iterations, dk_len) in cases {
1023      let mut expected = vec![0u8; dk_len];
1024      oracle_sha256(password, salt, iterations, &mut expected);
1025
1026      let mut actual = vec![0u8; dk_len];
1027      Pbkdf2Sha256::derive_key(password, salt, iterations, &mut actual).unwrap();
1028
1029      assert_eq!(
1030        actual,
1031        expected,
1032        "SHA-256 mismatch: pw_len={} salt_len={} c={} dk_len={}",
1033        password.len(),
1034        salt.len(),
1035        iterations,
1036        dk_len,
1037      );
1038    }
1039  }
1040
1041  #[test]
1042  fn sha512_matches_oracle() {
1043    #[cfg(not(miri))]
1044    let cases: &[(&[u8], &[u8], u32, usize)] = &[
1045      (b"password", b"salt", 1, 64),
1046      (b"password", b"salt", 2, 64),
1047      (b"password", b"salt", 4096, 64),
1048      (b"password", b"salt", 1, 128),
1049      (b"password", b"salt", 100, 128),
1050      (b"", b"salt", 1, 64),
1051      (b"password", b"", 1, 64),
1052      (b"", b"", 1, 64),
1053      (b"p", b"s", 1, 1),
1054      (b"password", b"salt", 1, 20),
1055      (b"password", b"salt", 1, 48),
1056      (b"password", b"salt", 1, 96),
1057      (b"password", b"salt", 1, 192),
1058      // Long salt
1059      (b"password", &[0xBB; 200], 1, 64),
1060      // Key longer than block size
1061      (&[0xCC; 200], b"salt", 1, 64),
1062    ];
1063    #[cfg(miri)]
1064    let cases: &[(&[u8], &[u8], u32, usize)] = &[
1065      (b"password", b"salt", 1, 64),
1066      (b"password", b"salt", 2, 64),
1067      (b"password", b"salt", 16, 128),
1068      (b"", b"", 1, 64),
1069      (b"p", b"s", 1, 1),
1070      (b"password", b"salt", 1, 192),
1071      (b"password", &[0xBB; 200], 1, 64),
1072      (&[0xCC; 200], b"salt", 1, 64),
1073    ];
1074
1075    for &(password, salt, iterations, dk_len) in cases {
1076      let mut expected = vec![0u8; dk_len];
1077      oracle_sha512(password, salt, iterations, &mut expected);
1078
1079      let mut actual = vec![0u8; dk_len];
1080      Pbkdf2Sha512::derive_key(password, salt, iterations, &mut actual).unwrap();
1081
1082      assert_eq!(
1083        actual,
1084        expected,
1085        "SHA-512 mismatch: pw_len={} salt_len={} c={} dk_len={}",
1086        password.len(),
1087        salt.len(),
1088        iterations,
1089        dk_len,
1090      );
1091    }
1092  }
1093
1094  // ── Verify ────────────────────────────────────────────────────────────
1095
1096  #[test]
1097  fn sha256_verify_correct_password() {
1098    let dk = Pbkdf2Sha256::derive_key_array::<32>(b"password", b"salt", 100).unwrap();
1099    assert!(Pbkdf2Sha256::verify_password(b"password", b"salt", 100, &dk).is_ok());
1100  }
1101
1102  #[test]
1103  fn sha256_verify_wrong_password() {
1104    let dk = Pbkdf2Sha256::derive_key_array::<32>(b"password", b"salt", 100).unwrap();
1105    assert!(Pbkdf2Sha256::verify_password(b"wrong", b"salt", 100, &dk).is_err());
1106  }
1107
1108  #[test]
1109  fn sha256_verify_wrong_salt() {
1110    let dk = Pbkdf2Sha256::derive_key_array::<32>(b"password", b"salt", 100).unwrap();
1111    assert!(Pbkdf2Sha256::verify_password(b"password", b"wrong", 100, &dk).is_err());
1112  }
1113
1114  #[test]
1115  fn sha256_verify_wrong_iterations() {
1116    let dk = Pbkdf2Sha256::derive_key_array::<32>(b"password", b"salt", 100).unwrap();
1117    assert!(Pbkdf2Sha256::verify_password(b"password", b"salt", 101, &dk).is_err());
1118  }
1119
1120  #[test]
1121  fn sha512_verify_correct_password() {
1122    let dk = Pbkdf2Sha512::derive_key_array::<64>(b"password", b"salt", 100).unwrap();
1123    assert!(Pbkdf2Sha512::verify_password(b"password", b"salt", 100, &dk).is_ok());
1124  }
1125
1126  #[test]
1127  fn sha512_verify_wrong_password() {
1128    let dk = Pbkdf2Sha512::derive_key_array::<64>(b"password", b"salt", 100).unwrap();
1129    assert!(Pbkdf2Sha512::verify_password(b"wrong", b"salt", 100, &dk).is_err());
1130  }
1131
1132  // ── Error paths ───────────────────────────────────────────────────────
1133
1134  #[test]
1135  fn sha256_zero_iterations_error() {
1136    let mut dk = [0u8; 32];
1137    assert_eq!(
1138      Pbkdf2Sha256::derive_key(b"pw", b"salt", 0, &mut dk),
1139      Err(Pbkdf2Error::InvalidIterations)
1140    );
1141  }
1142
1143  #[test]
1144  fn sha512_zero_iterations_error() {
1145    let mut dk = [0u8; 64];
1146    assert_eq!(
1147      Pbkdf2Sha512::derive_key(b"pw", b"salt", 0, &mut dk),
1148      Err(Pbkdf2Error::InvalidIterations)
1149    );
1150  }
1151
1152  #[test]
1153  fn sha256_empty_output_ok() {
1154    assert!(Pbkdf2Sha256::derive_key(b"pw", b"salt", 1, &mut []).is_ok());
1155  }
1156
1157  #[test]
1158  fn sha512_empty_output_ok() {
1159    assert!(Pbkdf2Sha512::derive_key(b"pw", b"salt", 1, &mut []).is_ok());
1160  }
1161
1162  #[test]
1163  fn sha256_verify_zero_iterations() {
1164    assert!(Pbkdf2Sha256::verify_password(b"pw", b"salt", 0, &[0u8; 32]).is_err());
1165  }
1166
1167  #[test]
1168  fn sha256_verify_empty_expected() {
1169    assert!(Pbkdf2Sha256::verify_password(b"pw", b"salt", 1, &[]).is_err());
1170  }
1171
1172  #[test]
1173  fn sha256_verify_password_covers_output_lengths_and_mismatch_positions() {
1174    let password = [0xA5; 97];
1175    let salt = [0x5A; 200];
1176    #[cfg(not(miri))]
1177    let output_lengths = 1..=96;
1178    #[cfg(miri)]
1179    let output_lengths = [1usize, 2, 31, 32, 33, 63, 64, 65, 95, 96].into_iter();
1180
1181    for out_len in output_lengths {
1182      let mut expected = vec![0u8; out_len];
1183      Pbkdf2Sha256::derive_key(&password, &salt, 2, &mut expected).unwrap();
1184      assert!(Pbkdf2Sha256::verify_password(&password, &salt, 2, &expected).is_ok());
1185
1186      let mut wrong_first = expected.clone();
1187      wrong_first[0] ^= 1;
1188      assert!(Pbkdf2Sha256::verify_password(&password, &salt, 2, &wrong_first).is_err());
1189
1190      let mut wrong_last = expected.clone();
1191      let last = wrong_last.len().strict_sub(1);
1192      wrong_last[last] ^= 1;
1193      assert!(Pbkdf2Sha256::verify_password(&password, &salt, 2, &wrong_last).is_err());
1194    }
1195  }
1196
1197  #[test]
1198  fn sha512_verify_password_covers_output_lengths_and_mismatch_positions() {
1199    let password = [0x3C; 193];
1200    let salt = [0xC3; 260];
1201    #[cfg(not(miri))]
1202    let output_lengths = 1..=192;
1203    #[cfg(miri)]
1204    let output_lengths = [1usize, 2, 63, 64, 65, 127, 128, 129, 191, 192].into_iter();
1205
1206    for out_len in output_lengths {
1207      let mut expected = vec![0u8; out_len];
1208      Pbkdf2Sha512::derive_key(&password, &salt, 2, &mut expected).unwrap();
1209      assert!(Pbkdf2Sha512::verify_password(&password, &salt, 2, &expected).is_ok());
1210
1211      let mut wrong_first = expected.clone();
1212      wrong_first[0] ^= 1;
1213      assert!(Pbkdf2Sha512::verify_password(&password, &salt, 2, &wrong_first).is_err());
1214
1215      let mut wrong_last = expected.clone();
1216      let last = wrong_last.len().strict_sub(1);
1217      wrong_last[last] ^= 1;
1218      assert!(Pbkdf2Sha512::verify_password(&password, &salt, 2, &wrong_last).is_err());
1219    }
1220  }
1221
1222  // ── Streaming state reuse ─────────────────────────────────────────────
1223
1224  #[test]
1225  fn sha256_state_reuse_matches_oneshot() {
1226    let state = Pbkdf2Sha256::new(b"password");
1227    let dk1 = state.derive_array::<32>(b"salt1", 100).unwrap();
1228    let dk2 = state.derive_array::<32>(b"salt2", 100).unwrap();
1229
1230    let oneshot1 = Pbkdf2Sha256::derive_key_array::<32>(b"password", b"salt1", 100).unwrap();
1231    let oneshot2 = Pbkdf2Sha256::derive_key_array::<32>(b"password", b"salt2", 100).unwrap();
1232
1233    assert_eq!(dk1, oneshot1);
1234    assert_eq!(dk2, oneshot2);
1235    assert_ne!(dk1, dk2);
1236  }
1237
1238  // ── c=1 edge case ─────────────────────────────────────────────────────
1239
1240  #[test]
1241  fn sha256_single_iteration() {
1242    let mut expected = [0u8; 32];
1243    oracle_sha256(b"pw", b"salt", 1, &mut expected);
1244    let actual = Pbkdf2Sha256::derive_key_array::<32>(b"pw", b"salt", 1).unwrap();
1245    assert_eq!(actual, expected);
1246  }
1247
1248  #[test]
1249  fn sha512_single_iteration() {
1250    let mut expected = [0u8; 64];
1251    oracle_sha512(b"pw", b"salt", 1, &mut expected);
1252    let actual = Pbkdf2Sha512::derive_key_array::<64>(b"pw", b"salt", 1).unwrap();
1253    assert_eq!(actual, expected);
1254  }
1255
1256  // ── Error traits ──────────────────────────────────────────────────────
1257
1258  #[test]
1259  fn error_is_copy() {
1260    let e = Pbkdf2Error::InvalidIterations;
1261    let e2 = e;
1262    assert_eq!(e, e2);
1263  }
1264
1265  #[test]
1266  fn error_display() {
1267    fn assert_display<T: core::fmt::Display>() {}
1268    assert_display::<Pbkdf2Error>();
1269  }
1270
1271  #[test]
1272  fn error_debug() {
1273    fn assert_debug<T: core::fmt::Debug>() {}
1274    assert_debug::<Pbkdf2Error>();
1275  }
1276
1277  #[test]
1278  fn error_implements_error_trait() {
1279    fn assert_error<T: core::error::Error>() {}
1280    assert_error::<Pbkdf2Error>();
1281  }
1282
1283  // ── Forced-kernel oracle tests ──────────────────────────────────────
1284
1285  use crate::hashes::crypto::{
1286    sha256::kernels::{
1287      Sha256KernelId, compress_blocks_fn as sha256_compress_blocks_fn, required_caps as sha256_required_caps,
1288    },
1289    sha512::kernels::{
1290      ALL as SHA512_KERNELS, Sha512KernelId, compress_blocks_fn as sha512_compress_blocks_fn,
1291      required_caps as sha512_required_caps,
1292    },
1293  };
1294
1295  /// SHA-256 kernel list (sha256 module doesn't define ALL).
1296  const SHA256_KERNELS: &[Sha256KernelId] = &[
1297    Sha256KernelId::Portable,
1298    #[cfg(target_arch = "x86_64")]
1299    Sha256KernelId::X86Sha,
1300    #[cfg(target_arch = "aarch64")]
1301    Sha256KernelId::Aarch64Sha2,
1302    #[cfg(any(target_arch = "riscv64", target_arch = "riscv32"))]
1303    Sha256KernelId::RiscvZknh,
1304    #[cfg(target_arch = "wasm32")]
1305    Sha256KernelId::WasmSimd128,
1306    #[cfg(target_arch = "s390x")]
1307    Sha256KernelId::S390xKimd,
1308  ];
1309
1310  static SHA256_VERIFY_BLOCKS: AtomicUsize = AtomicUsize::new(0);
1311  static SHA512_VERIFY_BLOCKS: AtomicUsize = AtomicUsize::new(0);
1312
1313  fn counting_sha256_compress(state: &mut [u32; 8], blocks: &[u8]) {
1314    SHA256_VERIFY_BLOCKS.fetch_add(blocks.len() / SHA256_BLOCK_SIZE, Ordering::Relaxed);
1315    sha256_compress_blocks_fn(Sha256KernelId::Portable)(state, blocks);
1316  }
1317
1318  fn counting_sha512_compress(state: &mut [u64; 8], blocks: &[u8]) {
1319    SHA512_VERIFY_BLOCKS.fetch_add(blocks.len() / SHA512_BLOCK_SIZE, Ordering::Relaxed);
1320    sha512_compress_blocks_fn(Sha512KernelId::Portable)(state, blocks);
1321  }
1322
1323  fn counted_sha256_verify(
1324    state: &Pbkdf2Sha256,
1325    salt: &[u8],
1326    iterations: u32,
1327    expected: &[u8],
1328  ) -> (Result<(), VerificationError>, usize) {
1329    SHA256_VERIFY_BLOCKS.store(0, Ordering::Relaxed);
1330    let result = state.verify(salt, iterations, expected);
1331    let blocks = SHA256_VERIFY_BLOCKS.swap(0, Ordering::Relaxed);
1332    (result, blocks)
1333  }
1334
1335  fn counted_sha512_verify(
1336    state: &Pbkdf2Sha512,
1337    salt: &[u8],
1338    iterations: u32,
1339    expected: &[u8],
1340  ) -> (Result<(), VerificationError>, usize) {
1341    SHA512_VERIFY_BLOCKS.store(0, Ordering::Relaxed);
1342    let result = state.verify(salt, iterations, expected);
1343    let blocks = SHA512_VERIFY_BLOCKS.swap(0, Ordering::Relaxed);
1344    (result, blocks)
1345  }
1346
1347  fn assert_pbkdf2_sha256_kernel(id: Sha256KernelId) {
1348    let compress = sha256_compress_blocks_fn(id);
1349    let cases: &[(&[u8], &[u8], u32, usize)] = &[
1350      (b"password", b"salt", 1, 32),
1351      (b"password", b"salt", 4, 32),
1352      (b"password", b"salt", 100, 32),
1353      (b"password", b"salt", 1, 64),      // multi-block
1354      (b"", b"salt", 1, 32),              // empty password
1355      (b"password", b"", 1, 32),          // empty salt
1356      (b"p", b"s", 1, 1),                 // minimal output
1357      (b"password", &[0xBB; 200], 1, 32), // long salt
1358      (&[0xCC; 128], b"salt", 1, 32),     // password > block_size (triggers key hashing)
1359    ];
1360
1361    for &(password, salt, iterations, dk_len) in cases {
1362      let mut expected = vec![0u8; dk_len];
1363      oracle_sha256(password, salt, iterations, &mut expected);
1364
1365      let state = Pbkdf2Sha256::new_with_compress_for_test(password, compress);
1366      let mut actual = vec![0u8; dk_len];
1367      state.derive(salt, iterations, &mut actual).unwrap();
1368
1369      assert_eq!(
1370        actual,
1371        expected,
1372        "pbkdf2-sha256 forced mismatch kernel={} pw_len={} salt_len={} c={} dk_len={}",
1373        id.as_str(),
1374        password.len(),
1375        salt.len(),
1376        iterations,
1377        dk_len,
1378      );
1379    }
1380  }
1381
1382  fn assert_pbkdf2_sha512_kernel(id: Sha512KernelId) {
1383    let compress = sha512_compress_blocks_fn(id);
1384    let cases: &[(&[u8], &[u8], u32, usize)] = &[
1385      (b"password", b"salt", 1, 64),
1386      (b"password", b"salt", 4, 64),
1387      (b"password", b"salt", 100, 64),
1388      (b"password", b"salt", 1, 128),     // multi-block
1389      (b"", b"salt", 1, 64),              // empty password
1390      (b"password", b"", 1, 64),          // empty salt
1391      (b"p", b"s", 1, 1),                 // minimal output
1392      (b"password", &[0xBB; 200], 1, 64), // long salt
1393      (&[0xCC; 200], b"salt", 1, 64),     // password > block_size
1394    ];
1395
1396    for &(password, salt, iterations, dk_len) in cases {
1397      let mut expected = vec![0u8; dk_len];
1398      oracle_sha512(password, salt, iterations, &mut expected);
1399
1400      let state = Pbkdf2Sha512::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-sha512 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  #[test]
1418  fn pbkdf2_sha256_forced_kernels_match_oracle() {
1419    let caps = crate::platform::caps();
1420    for &id in SHA256_KERNELS {
1421      if caps.has(sha256_required_caps(id)) {
1422        assert_pbkdf2_sha256_kernel(id);
1423      }
1424    }
1425  }
1426
1427  #[test]
1428  fn pbkdf2_sha512_forced_kernels_match_oracle() {
1429    let caps = crate::platform::caps();
1430    for &id in SHA512_KERNELS {
1431      if caps.has(sha512_required_caps(id)) {
1432        assert_pbkdf2_sha512_kernel(id);
1433      }
1434    }
1435  }
1436
1437  #[test]
1438  fn sha256_verify_keeps_same_compress_work_for_match_and_mismatch_positions() {
1439    let password = [0x11; 89];
1440    let salt = [0x22; 200];
1441    let state = Pbkdf2Sha256::new_with_compress_for_test(&password, counting_sha256_compress);
1442    #[cfg(not(miri))]
1443    let output_lengths = 1..=96;
1444    #[cfg(miri)]
1445    let output_lengths = [1usize, 2, 31, 32, 33, 63, 64, 65, 95, 96].into_iter();
1446
1447    for out_len in output_lengths {
1448      let mut expected = vec![0u8; out_len];
1449      state.derive(&salt, 3, &mut expected).unwrap();
1450
1451      let (ok, ok_blocks) = counted_sha256_verify(&state, &salt, 3, &expected);
1452      assert!(ok.is_ok(), "sha256 verify must accept correct output_len={out_len}");
1453
1454      let mut wrong_first = expected.clone();
1455      wrong_first[0] ^= 1;
1456      let (wrong_first_result, wrong_first_blocks) = counted_sha256_verify(&state, &salt, 3, &wrong_first);
1457      assert!(
1458        wrong_first_result.is_err(),
1459        "sha256 verify must reject first-byte mismatch output_len={out_len}"
1460      );
1461
1462      let mut wrong_last = expected.clone();
1463      let last = wrong_last.len().strict_sub(1);
1464      wrong_last[last] ^= 1;
1465      let (wrong_last_result, wrong_last_blocks) = counted_sha256_verify(&state, &salt, 3, &wrong_last);
1466      assert!(
1467        wrong_last_result.is_err(),
1468        "sha256 verify must reject last-byte mismatch output_len={out_len}"
1469      );
1470
1471      assert!(ok_blocks > 0, "sha256 verify must do real work output_len={out_len}");
1472      assert_eq!(
1473        ok_blocks, wrong_first_blocks,
1474        "sha256 verify must not short-circuit on first-byte mismatch output_len={out_len}"
1475      );
1476      assert_eq!(
1477        ok_blocks, wrong_last_blocks,
1478        "sha256 verify must not short-circuit on last-byte mismatch output_len={out_len}"
1479      );
1480    }
1481  }
1482
1483  #[test]
1484  fn sha512_verify_keeps_same_compress_work_for_match_and_mismatch_positions() {
1485    let password = [0x44; 161];
1486    let salt = [0x55; 260];
1487    let state = Pbkdf2Sha512::new_with_compress_for_test(&password, counting_sha512_compress);
1488    #[cfg(not(miri))]
1489    let output_lengths = 1..=192;
1490    #[cfg(miri)]
1491    let output_lengths = [1usize, 2, 63, 64, 65, 127, 128, 129, 191, 192].into_iter();
1492
1493    for out_len in output_lengths {
1494      let mut expected = vec![0u8; out_len];
1495      state.derive(&salt, 3, &mut expected).unwrap();
1496
1497      let (ok, ok_blocks) = counted_sha512_verify(&state, &salt, 3, &expected);
1498      assert!(ok.is_ok(), "sha512 verify must accept correct output_len={out_len}");
1499
1500      let mut wrong_first = expected.clone();
1501      wrong_first[0] ^= 1;
1502      let (wrong_first_result, wrong_first_blocks) = counted_sha512_verify(&state, &salt, 3, &wrong_first);
1503      assert!(
1504        wrong_first_result.is_err(),
1505        "sha512 verify must reject first-byte mismatch output_len={out_len}"
1506      );
1507
1508      let mut wrong_last = expected.clone();
1509      let last = wrong_last.len().strict_sub(1);
1510      wrong_last[last] ^= 1;
1511      let (wrong_last_result, wrong_last_blocks) = counted_sha512_verify(&state, &salt, 3, &wrong_last);
1512      assert!(
1513        wrong_last_result.is_err(),
1514        "sha512 verify must reject last-byte mismatch output_len={out_len}"
1515      );
1516
1517      assert!(ok_blocks > 0, "sha512 verify must do real work output_len={out_len}");
1518      assert_eq!(
1519        ok_blocks, wrong_first_blocks,
1520        "sha512 verify must not short-circuit on first-byte mismatch output_len={out_len}"
1521      );
1522      assert_eq!(
1523        ok_blocks, wrong_last_blocks,
1524        "sha512 verify must not short-circuit on last-byte mismatch output_len={out_len}"
1525      );
1526    }
1527  }
1528}