diff --git a/wrapper/rust/wolfssl-wolfcrypt/Cargo.lock b/wrapper/rust/wolfssl-wolfcrypt/Cargo.lock index ea88736bdf..0241697ad3 100644 --- a/wrapper/rust/wolfssl-wolfcrypt/Cargo.lock +++ b/wrapper/rust/wolfssl-wolfcrypt/Cargo.lock @@ -22,6 +22,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bindgen" version = "0.72.1" @@ -105,6 +111,12 @@ dependencies = [ "libloading", ] +[[package]] +name = "cmov" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" + [[package]] name = "crypto-common" version = "0.1.7" @@ -125,6 +137,15 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "digest" version = "0.11.2" @@ -229,6 +250,25 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "password-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aab41826031698d6ffcd9cff78ef56ef998e39dc7e5067cdfebe373842d4723b" +dependencies = [ + "phc", +] + +[[package]] +name = "phc" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44dc769b75f93afdddd8c7fa12d685292ddeff1e66f7f0f3a234cf1818afe892" +dependencies = [ + "base64ct", + "ctutils", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -424,6 +464,7 @@ dependencies = [ "bindgen", "cipher", "digest", + "password-hash", "rand_core 0.10.0", "regex", "signature", diff --git a/wrapper/rust/wolfssl-wolfcrypt/Cargo.toml b/wrapper/rust/wolfssl-wolfcrypt/Cargo.toml index 9defa79fee..70067dc978 100644 --- a/wrapper/rust/wolfssl-wolfcrypt/Cargo.toml +++ b/wrapper/rust/wolfssl-wolfcrypt/Cargo.toml @@ -17,6 +17,7 @@ aead = ["dep:aead"] cipher = ["dep:cipher"] digest = ["dep:digest"] signature = ["dep:signature"] +password-hash = ["dep:password-hash", "password-hash/phc"] [dependencies] rand_core = { version = "0.10", optional = true, default-features = false } @@ -25,12 +26,14 @@ cipher = { version = "0.5", optional = true, default-features = false } digest = { version = "0.11", optional = true, default-features = false, features = ["block-api"] } signature = { version = "2.2", optional = true, default-features = false } zeroize = { version = "1.3", default-features = false, features = ["derive"] } +password-hash = { version = "0.6.1", optional = true, default-features = false } [dev-dependencies] aead = { version = "0.5", features = ["alloc", "dev"] } cipher = "0.5" digest = { version = "0.11", features = ["dev"] } signature = "2.2" +password-hash = { version = "0.6.1", features = ["phc"] } [build-dependencies] bindgen = "0.72.1" diff --git a/wrapper/rust/wolfssl-wolfcrypt/Makefile b/wrapper/rust/wolfssl-wolfcrypt/Makefile index 51dc4c801e..1ebb06bb97 100644 --- a/wrapper/rust/wolfssl-wolfcrypt/Makefile +++ b/wrapper/rust/wolfssl-wolfcrypt/Makefile @@ -1,4 +1,4 @@ -FEATURES := rand_core,aead,cipher,digest,signature +FEATURES := rand_core,aead,cipher,digest,signature,password-hash CARGO_FEATURE_FLAGS := --features $(FEATURES) .PHONY: all diff --git a/wrapper/rust/wolfssl-wolfcrypt/src/lib.rs b/wrapper/rust/wolfssl-wolfcrypt/src/lib.rs index ec817dcf4c..6ab0488902 100644 --- a/wrapper/rust/wolfssl-wolfcrypt/src/lib.rs +++ b/wrapper/rust/wolfssl-wolfcrypt/src/lib.rs @@ -64,6 +64,8 @@ pub mod rsa; #[cfg(feature = "signature")] pub mod rsa_pkcs1v15; pub mod sha; +#[cfg(feature = "password-hash")] +pub mod pbkdf2_password_hash; #[cfg(feature = "digest")] pub mod sha_digest; diff --git a/wrapper/rust/wolfssl-wolfcrypt/src/pbkdf2_password_hash.rs b/wrapper/rust/wolfssl-wolfcrypt/src/pbkdf2_password_hash.rs new file mode 100644 index 0000000000..0a778b46d3 --- /dev/null +++ b/wrapper/rust/wolfssl-wolfcrypt/src/pbkdf2_password_hash.rs @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/*! +RustCrypto `password-hash` trait implementations for wolfCrypt PBKDF2. + +This module provides [`Pbkdf2`], a type that implements the +[`PasswordHasher`] and [`CustomizedPasswordHasher`] traits from the +`password-hash` crate, backed by the wolfCrypt PBKDF2 implementation. +The blanket [`PasswordVerifier`] implementation is also available, +allowing verification of existing password hashes. + +Password hashes are represented in the +[PHC string format](https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md): + +```text +$pbkdf2-sha256$i=600000$$ +``` + +# Supported algorithms + +| Algorithm ID | Hash function | +|-----------------|---------------| +| `pbkdf2-sha256` | HMAC-SHA-256 | +| `pbkdf2-sha384` | HMAC-SHA-384 | +| `pbkdf2-sha512` | HMAC-SHA-512 | + +[`PasswordHasher`]: password_hash::PasswordHasher +[`CustomizedPasswordHasher`]: password_hash::CustomizedPasswordHasher +[`PasswordVerifier`]: password_hash::PasswordVerifier +*/ + +#![cfg(all(feature = "password-hash", hmac, kdf_pbkdf2))] + +use password_hash::phc::{Ident, Output, ParamsString, PasswordHash, Salt}; +use password_hash::{CustomizedPasswordHasher, Error, Result, Version}; + +use crate::hmac::HMAC; +use crate::kdf; + +const PBKDF2_SHA256_IDENT: Ident = Ident::new_unwrap("pbkdf2-sha256"); +const PBKDF2_SHA384_IDENT: Ident = Ident::new_unwrap("pbkdf2-sha384"); +const PBKDF2_SHA512_IDENT: Ident = Ident::new_unwrap("pbkdf2-sha512"); + +/// Minimum number of PBKDF2 rounds. +pub const MIN_ROUNDS: u32 = 1_000; + +/// Default number of PBKDF2 rounds (OWASP recommendation for SHA-256). +pub const DEFAULT_ROUNDS: u32 = 600_000; + +/// Default output length in bytes. +pub const DEFAULT_OUTPUT_LEN: usize = 32; + +/// PBKDF2 algorithm variant. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub enum Algorithm { + /// PBKDF2 with HMAC-SHA-256. + #[default] + Pbkdf2Sha256, + /// PBKDF2 with HMAC-SHA-384. + Pbkdf2Sha384, + /// PBKDF2 with HMAC-SHA-512. + Pbkdf2Sha512, +} + +impl Algorithm { + /// Get the PHC string format identifier for this algorithm. + pub fn ident(self) -> Ident { + match self { + Algorithm::Pbkdf2Sha256 => PBKDF2_SHA256_IDENT, + Algorithm::Pbkdf2Sha384 => PBKDF2_SHA384_IDENT, + Algorithm::Pbkdf2Sha512 => PBKDF2_SHA512_IDENT, + } + } + + fn hmac_type(self) -> i32 { + match self { + Algorithm::Pbkdf2Sha256 => HMAC::TYPE_SHA256, + Algorithm::Pbkdf2Sha384 => HMAC::TYPE_SHA384, + Algorithm::Pbkdf2Sha512 => HMAC::TYPE_SHA512, + } + } +} + +impl TryFrom for Algorithm { + type Error = Error; + + fn try_from(ident: Ident) -> Result { + if ident == PBKDF2_SHA256_IDENT { + Ok(Algorithm::Pbkdf2Sha256) + } else if ident == PBKDF2_SHA384_IDENT { + Ok(Algorithm::Pbkdf2Sha384) + } else if ident == PBKDF2_SHA512_IDENT { + Ok(Algorithm::Pbkdf2Sha512) + } else { + Err(Error::Algorithm) + } + } +} + +/// PBKDF2 parameters. +#[derive(Clone, Debug)] +pub struct Params { + /// Number of iterations (rounds). + pub rounds: u32, + /// Desired output hash length in bytes. + pub output_len: usize, +} + +impl Default for Params { + fn default() -> Self { + Params { + rounds: DEFAULT_ROUNDS, + output_len: DEFAULT_OUTPUT_LEN, + } + } +} + +impl TryFrom<&PasswordHash> for Params { + type Error = Error; + + fn try_from(hash: &PasswordHash) -> Result { + let rounds = hash + .params + .get_decimal("i") + .ok_or(Error::ParamInvalid { name: "i" })?; + + if rounds < MIN_ROUNDS { + return Err(Error::ParamInvalid { name: "i" }); + } + + let output_len = if let Some(ref h) = hash.hash { + h.len() + } else if let Some(l) = hash.params.get_decimal("l") { + l as usize + } else { + return Err(Error::ParamInvalid { name: "l" }); + }; + + Ok(Params { rounds, output_len }) + } +} + +/// PBKDF2 password hasher backed by wolfCrypt. +/// +/// Implements the [`PasswordHasher`](password_hash::PasswordHasher) and +/// [`CustomizedPasswordHasher`] traits. A blanket +/// [`PasswordVerifier`](password_hash::PasswordVerifier) implementation is +/// provided by the `password-hash` crate. +/// +/// # Example +/// +/// ```rust +/// #[cfg(all(hmac, kdf_pbkdf2))] +/// { +/// use password_hash::PasswordHasher; +/// use wolfssl_wolfcrypt::pbkdf2_password_hash::Pbkdf2; +/// +/// let hasher = Pbkdf2::default(); +/// let salt = b"0123456789abcdef"; // 16 bytes +/// let hash = hasher.hash_password_with_salt(b"password", salt) +/// .expect("hashing failed"); +/// } +/// ``` +#[derive(Clone, Debug, Default)] +pub struct Pbkdf2 { + /// Algorithm to use for hashing. + pub algorithm: Algorithm, + /// Default parameters. + pub params: Params, +} + +impl password_hash::PasswordHasher for Pbkdf2 { + fn hash_password_with_salt(&self, password: &[u8], salt: &[u8]) -> Result { + self.hash_password_customized(password, salt, None, None, self.params.clone()) + } +} + +impl password_hash::CustomizedPasswordHasher for Pbkdf2 { + type Params = Params; + + fn hash_password_customized( + &self, + password: &[u8], + salt: &[u8], + algorithm: Option<&str>, + version: Option, + params: Params, + ) -> Result { + if version.is_some() { + return Err(Error::Version); + } + + let algorithm = match algorithm { + Some(s) => { + let ident = Ident::new(s).map_err(|_| Error::Algorithm)?; + Algorithm::try_from(ident)? + } + None => self.algorithm, + }; + + if params.rounds < MIN_ROUNDS { + return Err(Error::ParamInvalid { name: "i" }); + } + + let iterations = i32::try_from(params.rounds) + .map_err(|_| Error::ParamInvalid { name: "i" })?; + + let salt = Salt::new(salt)?; + + let mut out_buf = [0u8; Output::MAX_LENGTH]; + let out_slice = &mut out_buf[..params.output_len]; + kdf::pbkdf2(password, salt.as_ref(), iterations, algorithm.hmac_type(), out_slice) + .map_err(|_| Error::Crypto)?; + let output = Output::new(out_slice)?; + + let mut phc_params = ParamsString::new(); + phc_params.add_decimal("i", params.rounds)?; + + Ok(PasswordHash { + algorithm: algorithm.ident(), + version: None, + params: phc_params, + salt: Some(salt), + hash: Some(output), + }) + } +} diff --git a/wrapper/rust/wolfssl-wolfcrypt/tests/test_pbkdf2_password_hash.rs b/wrapper/rust/wolfssl-wolfcrypt/tests/test_pbkdf2_password_hash.rs new file mode 100644 index 0000000000..6a4c45388b --- /dev/null +++ b/wrapper/rust/wolfssl-wolfcrypt/tests/test_pbkdf2_password_hash.rs @@ -0,0 +1,267 @@ +#![cfg(all(feature = "password-hash", hmac, kdf_pbkdf2))] + +mod common; + +use password_hash::phc::PasswordHash; +use password_hash::{CustomizedPasswordHasher, PasswordHasher, PasswordVerifier}; +use wolfssl_wolfcrypt::pbkdf2_password_hash::*; + +#[test] +fn test_hash_and_verify() { + common::setup(); + + let hasher = Pbkdf2 { + algorithm: Algorithm::Pbkdf2Sha256, + params: Params { + rounds: 4096, + output_len: 32, + }, + }; + + let salt = b"0123456789abcdef"; // 16 bytes + let password = b"hunter2"; + + let hash = hasher + .hash_password_with_salt(password, salt) + .expect("hashing failed"); + + assert_eq!(hash.algorithm, Algorithm::Pbkdf2Sha256.ident()); + assert!(hash.salt.is_some()); + assert!(hash.hash.is_some()); + assert_eq!(hash.hash.as_ref().unwrap().len(), 32); + + // Verify correct password succeeds + hasher + .verify_password(password, &hash) + .expect("verification of correct password failed"); + + // Verify wrong password fails + let result = hasher.verify_password(b"wrong_password", &hash); + assert!(result.is_err()); +} + +#[test] +fn test_hash_roundtrip_phc_string() { + common::setup(); + + let hasher = Pbkdf2 { + algorithm: Algorithm::Pbkdf2Sha256, + params: Params { + rounds: 4096, + output_len: 32, + }, + }; + + let salt = b"0123456789abcdef"; + let password = b"password"; + + let hash = hasher + .hash_password_with_salt(password, salt) + .expect("hashing failed"); + + // Serialize to PHC string and parse back + let phc_string = hash.to_string(); + assert!(phc_string.starts_with("$pbkdf2-sha256$")); + + let parsed = PasswordHash::new(&phc_string).expect("parsing PHC string failed"); + + // Verify with the parsed hash + hasher + .verify_password(password, &parsed) + .expect("verification of parsed hash failed"); +} + +#[test] +fn test_default_params() { + common::setup(); + + let hasher = Pbkdf2::default(); + assert_eq!(hasher.algorithm, Algorithm::Pbkdf2Sha256); + assert_eq!(hasher.params.rounds, DEFAULT_ROUNDS); + assert_eq!(hasher.params.output_len, DEFAULT_OUTPUT_LEN); +} + +#[test] +fn test_sha384_algorithm() { + common::setup(); + + let hasher = Pbkdf2 { + algorithm: Algorithm::Pbkdf2Sha384, + params: Params { + rounds: 4096, + output_len: 48, + }, + }; + + let salt = b"0123456789abcdef"; + let password = b"password"; + + let hash = hasher + .hash_password_with_salt(password, salt) + .expect("hashing with SHA-384 failed"); + assert_eq!(hash.algorithm, Algorithm::Pbkdf2Sha384.ident()); + assert_eq!(hash.hash.as_ref().unwrap().len(), 48); + + hasher + .verify_password(password, &hash) + .expect("SHA-384 verification failed"); +} + +#[test] +fn test_sha512_algorithm() { + common::setup(); + + let hasher = Pbkdf2 { + algorithm: Algorithm::Pbkdf2Sha512, + params: Params { + rounds: 4096, + output_len: 64, + }, + }; + + let salt = b"0123456789abcdef"; + let password = b"password"; + + let hash = hasher + .hash_password_with_salt(password, salt) + .expect("hashing with SHA-512 failed"); + assert_eq!(hash.algorithm, Algorithm::Pbkdf2Sha512.ident()); + assert_eq!(hash.hash.as_ref().unwrap().len(), 64); + + hasher + .verify_password(password, &hash) + .expect("SHA-512 verification failed"); +} + +#[test] +fn test_customized_hash() { + common::setup(); + + let hasher = Pbkdf2::default(); + + let salt = b"0123456789abcdef"; + let password = b"password"; + let custom_params = Params { + rounds: 8192, + output_len: 48, + }; + + let hash = hasher + .hash_password_with_params(password, salt, custom_params) + .expect("customized hashing failed"); + + assert_eq!(hash.hash.as_ref().unwrap().len(), 48); + assert_eq!(hash.params.get_decimal("i"), Some(8192)); + + hasher + .verify_password(password, &hash) + .expect("customized hash verification failed"); +} + +#[test] +fn test_customized_hash_with_algorithm_override() { + common::setup(); + + let hasher = Pbkdf2::default(); + + let salt = b"0123456789abcdef"; + let password = b"password"; + let params = Params { + rounds: 4096, + output_len: 64, + }; + + let hash = hasher + .hash_password_customized(password, salt, Some("pbkdf2-sha512"), None, params) + .expect("algorithm override failed"); + + assert_eq!(hash.algorithm, Algorithm::Pbkdf2Sha512.ident()); + assert_eq!(hash.hash.as_ref().unwrap().len(), 64); + + // Verify with a Pbkdf2 instance using the matching algorithm + let verifier = Pbkdf2 { + algorithm: Algorithm::Pbkdf2Sha512, + ..Pbkdf2::default() + }; + verifier + .verify_password(password, &hash) + .expect("verification with algorithm override failed"); +} + +#[test] +fn test_version_rejected() { + common::setup(); + + let hasher = Pbkdf2::default(); + let salt = b"0123456789abcdef"; + + let result = + hasher.hash_password_customized(b"password", salt, None, Some(1), Params::default()); + assert!(result.is_err()); +} + +#[test] +fn test_unknown_algorithm_rejected() { + common::setup(); + + let hasher = Pbkdf2::default(); + let salt = b"0123456789abcdef"; + + let result = hasher.hash_password_customized( + b"password", + salt, + Some("argon2id"), + None, + Params::default(), + ); + assert!(result.is_err()); +} + +#[test] +fn test_deterministic_output() { + common::setup(); + + let hasher = Pbkdf2 { + algorithm: Algorithm::Pbkdf2Sha256, + params: Params { + rounds: 4096, + output_len: 32, + }, + }; + + let salt = b"0123456789abcdef"; + let password = b"password"; + + let hash1 = hasher + .hash_password_with_salt(password, salt) + .expect("first hash failed"); + let hash2 = hasher + .hash_password_with_salt(password, salt) + .expect("second hash failed"); + + assert_eq!(hash1.hash, hash2.hash); +} + +#[test] +fn test_different_salts_produce_different_hashes() { + common::setup(); + + let hasher = Pbkdf2 { + algorithm: Algorithm::Pbkdf2Sha256, + params: Params { + rounds: 4096, + output_len: 32, + }, + }; + + let password = b"password"; + + let hash1 = hasher + .hash_password_with_salt(password, b"salt_aaaaaaaaaa01") + .expect("first hash failed"); + let hash2 = hasher + .hash_password_with_salt(password, b"salt_aaaaaaaaaa02") + .expect("second hash failed"); + + assert_ne!(hash1.hash, hash2.hash); +}