Skip to main content

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}