Rust wrapper: implement password-hash traits

This commit is contained in:
Josh Holtrop
2026-04-20 13:50:41 -04:00
parent 1c9555c121
commit c08c16ee8f
6 changed files with 559 additions and 1 deletions
+41
View File
@@ -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",
@@ -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"
+1 -1
View File
@@ -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
@@ -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;
@@ -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$<salt>$<hash>
```
# 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<Ident> for Algorithm {
type Error = Error;
fn try_from(ident: Ident) -> Result<Self> {
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<Self> {
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<PasswordHash> for Pbkdf2 {
fn hash_password_with_salt(&self, password: &[u8], salt: &[u8]) -> Result<PasswordHash> {
self.hash_password_customized(password, salt, None, None, self.params.clone())
}
}
impl password_hash::CustomizedPasswordHasher<PasswordHash> for Pbkdf2 {
type Params = Params;
fn hash_password_customized(
&self,
password: &[u8],
salt: &[u8],
algorithm: Option<&str>,
version: Option<Version>,
params: Params,
) -> Result<PasswordHash> {
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),
})
}
}
@@ -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);
}