hsh/algorithms/pbkdf2.rs
1// Copyright © 2023-2026 Hash (HSH) library contributors. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! PBKDF2-HMAC-SHA-256 / SHA-512 wrapper.
5//!
6//! PBKDF2 is the only password-hashing KDF that has a FIPS 140-3
7//! validated implementation today (via `aws-lc-rs`). It is the right
8//! choice when compliance dictates and Argon2id is unavailable.
9//!
10//! ## Routing
11//!
12//! - **Default build** (no `fips` feature): pure-Rust RustCrypto
13//! `pbkdf2`. Sufficient for any caller that doesn't have a FIPS
14//! 140-3 compliance requirement.
15//! - **`fips` feature enabled**: derivations route through the
16//! `hsh-backend-awslc` companion crate, which wraps `aws-lc-rs`
17//! PBKDF2 inside the AWS-LC FIPS 3.0 module (CMVP Cert #4759).
18//! Public API stays identical; only the underlying primitive
19//! provider changes. See ADR-0004 and `doc/FIPS.md`.
20
21use crate::error::{Error, Result};
22use crate::models::hash_algorithm::HashingAlgorithm;
23use serde::{Deserialize, Serialize};
24
25/// Default derived-key length in bytes.
26pub const DEFAULT_OUTPUT_LEN: usize = 32;
27
28/// Hash function variant used by PBKDF2.
29#[derive(
30 Clone,
31 Copy,
32 Debug,
33 Default,
34 Eq,
35 Hash,
36 Ord,
37 PartialEq,
38 PartialOrd,
39 Serialize,
40 Deserialize,
41)]
42pub enum Prf {
43 /// PBKDF2-HMAC-SHA-256 (FIPS-validated via `aws-lc-rs`).
44 #[default]
45 Sha256,
46 /// PBKDF2-HMAC-SHA-512 (FIPS-validated via `aws-lc-rs`).
47 Sha512,
48}
49
50impl Prf {
51 /// Returns the PHC algorithm identifier (`"pbkdf2-sha256"` etc.).
52 #[must_use]
53 pub const fn phc_id(self) -> &'static str {
54 match self {
55 Self::Sha256 => "pbkdf2-sha256",
56 Self::Sha512 => "pbkdf2-sha512",
57 }
58 }
59}
60
61/// PBKDF2 parameters.
62#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
63pub struct Pbkdf2Params {
64 /// PRF (HMAC-SHA-256 by default — the FIPS-validated path).
65 pub prf: Prf,
66 /// Iteration count. **OWASP-2025** minimums:
67 /// - SHA-256: **600 000**
68 /// - SHA-512: **210 000**
69 pub iterations: u32,
70 /// Derived-key length in bytes. Default: 32.
71 pub dk_len: usize,
72}
73
74impl Default for Pbkdf2Params {
75 fn default() -> Self {
76 Self::owasp_minimum_2025()
77 }
78}
79
80impl Pbkdf2Params {
81 /// OWASP Password Storage Cheat Sheet 2025 minimum for
82 /// PBKDF2-HMAC-SHA-256: `iterations = 600_000`, `dk_len = 32`.
83 #[must_use]
84 pub const fn owasp_minimum_2025() -> Self {
85 Self {
86 prf: Prf::Sha256,
87 iterations: 600_000,
88 dk_len: DEFAULT_OUTPUT_LEN,
89 }
90 }
91
92 /// OWASP-2025 minimum for the SHA-512 PRF: `iterations = 210_000`,
93 /// `dk_len = 32`.
94 #[must_use]
95 pub const fn owasp_minimum_2025_sha512() -> Self {
96 Self {
97 prf: Prf::Sha512,
98 iterations: 210_000,
99 dk_len: DEFAULT_OUTPUT_LEN,
100 }
101 }
102}
103
104/// Marker type for the PBKDF2 hashing algorithm.
105#[derive(
106 Clone,
107 Copy,
108 Debug,
109 Eq,
110 Hash,
111 Ord,
112 PartialEq,
113 PartialOrd,
114 Serialize,
115 Deserialize,
116)]
117pub struct Pbkdf2;
118
119impl HashingAlgorithm for Pbkdf2 {
120 fn hash_password(password: &str, salt: &str) -> Result<Vec<u8>> {
121 Self::hash_with(
122 password.as_bytes(),
123 salt.as_bytes(),
124 Pbkdf2Params::default(),
125 )
126 }
127}
128
129impl Pbkdf2 {
130 /// Derives `dk_len` bytes from `password` and `salt` under the
131 /// supplied [`Pbkdf2Params`]. Both inputs are accepted as raw byte
132 /// slices — PBKDF2 doesn't impose a UTF-8 constraint.
133 pub fn hash_with(
134 password: &[u8],
135 salt: &[u8],
136 params: Pbkdf2Params,
137 ) -> Result<Vec<u8>> {
138 if params.iterations < 1 {
139 return Err(Error::InvalidParameter(
140 "iterations must be >= 1".into(),
141 ));
142 }
143 if params.dk_len == 0 {
144 return Err(Error::InvalidParameter(
145 "dk_len must be > 0".into(),
146 ));
147 }
148
149 // When the `fips` feature is enabled, route through the
150 // AWS-LC FIPS 3.0 module via the `hsh-backend-awslc` crate.
151 // Otherwise fall back to the pure-Rust RustCrypto path. The
152 // observable output is identical for the same inputs — both
153 // paths implement RFC 8018 PBKDF2 — but only the FIPS route
154 // satisfies CMVP-validated-module compliance requirements.
155 #[cfg(feature = "fips")]
156 {
157 aws_lc::derive(password, salt, params)
158 }
159 #[cfg(not(feature = "fips"))]
160 {
161 rust_crypto::derive(password, salt, params)
162 }
163 }
164}
165
166#[cfg(feature = "fips")]
167mod aws_lc {
168 //! PBKDF2 derive via `hsh-backend-awslc` → `aws-lc-rs` → AWS-LC
169 //! FIPS 3.0 module (CMVP Cert #4759).
170
171 use super::{Pbkdf2Params, Prf};
172 use crate::error::{Error, HashingErrorKind, Result};
173 use hsh_backend_awslc::{pbkdf2_derive, Prf as AwslcPrf};
174
175 pub(super) fn derive(
176 password: &[u8],
177 salt: &[u8],
178 params: Pbkdf2Params,
179 ) -> Result<Vec<u8>> {
180 let prf = match params.prf {
181 Prf::Sha256 => AwslcPrf::Sha256,
182 Prf::Sha512 => AwslcPrf::Sha512,
183 };
184 pbkdf2_derive(
185 password,
186 salt,
187 prf,
188 params.iterations,
189 params.dk_len,
190 )
191 .map_err(|e| {
192 Error::hashing(HashingErrorKind::Pbkdf2, e.to_string())
193 })
194 }
195}
196
197// The pure-Rust module stays compiled even when the `fips` feature is
198// on, so parity tests can compare both paths. The `allow(dead_code)`
199// suppresses the workspace-level `dead_code = deny` lint when only the
200// FIPS path is reachable from the dispatcher above.
201#[allow(dead_code)]
202mod rust_crypto {
203 //! Pure-Rust PBKDF2 derive via the RustCrypto `pbkdf2` crate.
204
205 use super::{Pbkdf2Params, Prf};
206 use crate::error::{Error, HashingErrorKind, Result};
207 use hmac::Hmac;
208 use sha2::{Sha256, Sha512};
209
210 pub(super) fn derive(
211 password: &[u8],
212 salt: &[u8],
213 params: Pbkdf2Params,
214 ) -> Result<Vec<u8>> {
215 let mut out = vec![0u8; params.dk_len];
216 match params.prf {
217 Prf::Sha256 => pbkdf2::pbkdf2::<Hmac<Sha256>>(
218 password,
219 salt,
220 params.iterations,
221 &mut out,
222 )
223 .map_err(|e| {
224 Error::hashing(HashingErrorKind::Pbkdf2, e.to_string())
225 })?,
226 Prf::Sha512 => pbkdf2::pbkdf2::<Hmac<Sha512>>(
227 password,
228 salt,
229 params.iterations,
230 &mut out,
231 )
232 .map_err(|e| {
233 Error::hashing(HashingErrorKind::Pbkdf2, e.to_string())
234 })?,
235 }
236 Ok(out)
237 }
238}