pbkdf2: initial MCF (Customized)PasswordHasher support (#806)

Initial support for computing password hash strings in Modular Crypt
Format (MCF) instead of the PHC string format.

Tested against a vector from passlib:

https://passlib.readthedocs.io/en/stable/lib/passlib.hash.pbkdf2_digest.html

TODO: `PasswordVerifier` support
This commit is contained in:
Tony Arcieri
2026-01-11 16:04:45 -07:00
committed by GitHub
parent 62dd4ad654
commit 74c160d5b5
16 changed files with 326 additions and 156 deletions

View File

@@ -8,5 +8,5 @@ publish = false
[dependencies]
password-hash = "0.6.0-rc.3"
argon2 = { path = "../argon2" }
pbkdf2 = { path = "../pbkdf2", features = ["password-hash"] }
pbkdf2 = { path = "../pbkdf2", features = ["phc"] }
scrypt = { path = "../scrypt", features = ["phc"] }

4
Cargo.lock generated
View File

@@ -318,8 +318,7 @@ dependencies = [
[[package]]
name = "password-hash"
version = "0.6.0-rc.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c351143b5ab27b1f1d24712f21ea4d0458fe74f60dd5839297dabcc2ecd24d58"
source = "git+https://github.com/RustCrypto/traits#e90f599772cfeb6adf375eebf4ce3c320f9b7f1b"
dependencies = [
"getrandom",
"phc",
@@ -334,6 +333,7 @@ dependencies = [
"digest",
"hex-literal",
"hmac",
"mcf",
"password-hash",
"sha1",
"sha2",

View File

@@ -20,3 +20,5 @@ opt-level = 2
argon2 = { path = "./argon2" }
pbkdf2 = { path = "./pbkdf2" }
scrypt = { path = "./scrypt" }
password-hash = { git = "https://github.com/RustCrypto/traits" }

View File

@@ -22,7 +22,7 @@ password-hash = { version = "0.6.0-rc.7", features = ["alloc", "getrandom", "phc
# optional dependencies
argon2 = { version = "0.6.0-rc.5", optional = true, default-features = false, features = ["alloc", "password-hash"] }
pbkdf2 = { version = "0.13.0-rc.5", optional = true, default-features = false, features = ["password-hash"] }
pbkdf2 = { version = "0.13.0-rc.5", optional = true, default-features = false, features = ["phc"] }
scrypt = { version = "0.12.0-rc.6", optional = true, default-features = false, features = ["phc"] }
[features]

View File

@@ -122,7 +122,7 @@ pub fn is_hash_obsolete(hash: &str) -> Result<bool, ParseError> {
|| hash.params != default_params_string::<scrypt::Params>());
#[cfg(feature = "pbkdf2")]
return Ok(hash.algorithm != *pbkdf2::Algorithm::default().ident()
return Ok(hash.algorithm != pbkdf2::Algorithm::default().into()
|| hash.params != default_params_string::<pbkdf2::Params>());
Ok(true)

View File

@@ -18,7 +18,8 @@ digest = { version = "0.11.0-rc.4", features = ["mac"] }
# optional dependencies
hmac = { version = "0.13.0-rc.3", default-features = false, optional = true }
password-hash = { version = "0.6.0-rc.7", default-features = false, optional = true, features = ["phc"] }
mcf = { version = "0.6.0-rc.2", optional = true }
password-hash = { version = "0.6.0-rc.7", default-features = false, optional = true }
sha1 = { version = "0.11.0-rc.3", default-features = false, optional = true }
sha2 = { version = "0.11.0-rc.3", default-features = false, optional = true }
@@ -32,8 +33,9 @@ belt-hash = "0.2.0-rc.3"
[features]
default = ["hmac"]
getrandom = ["password-hash", "password-hash/getrandom"]
password-hash = ["hmac", "dep:password-hash", "sha2"]
getrandom = ["password-hash/getrandom"]
mcf = ["hmac", "password-hash/alloc", "dep:mcf", "sha2"]
phc = ["hmac", "password-hash/phc", "sha2"]
rand_core = ["password-hash/rand_core"]
[package.metadata.docs.rs]

View File

@@ -2,7 +2,10 @@ use core::{
fmt::{self, Display},
str::FromStr,
};
use password_hash::{Error, phc::Ident};
use password_hash::Error;
#[cfg(feature = "phc")]
use password_hash::phc::Ident;
/// PBKDF2 variants.
///
@@ -24,13 +27,25 @@ pub enum Algorithm {
impl Algorithm {
/// PBKDF2 (SHA-1) algorithm identifier
#[cfg(feature = "sha1")]
pub const PBKDF2_SHA1_IDENT: Ident = Ident::new_unwrap("pbkdf2");
pub const PBKDF2_SHA1_ID: &'static str = "pbkdf2";
/// PBKDF2 (SHA-256) algorithm identifier
pub const PBKDF2_SHA256_IDENT: Ident = Ident::new_unwrap("pbkdf2-sha256");
pub const PBKDF2_SHA256_ID: &'static str = "pbkdf2-sha256";
/// PBKDF2 (SHA-512) algorithm identifier
pub const PBKDF2_SHA512_IDENT: Ident = Ident::new_unwrap("pbkdf2-sha512");
pub const PBKDF2_SHA512_ID: &'static str = "pbkdf2-sha512";
/// PBKDF2 (SHA-1) algorithm identifier
#[cfg(all(feature = "phc", feature = "sha1"))]
pub(crate) const PBKDF2_SHA1_IDENT: Ident = Ident::new_unwrap(Self::PBKDF2_SHA1_ID);
/// PBKDF2 (SHA-256) algorithm identifier
#[cfg(feature = "phc")]
pub(crate) const PBKDF2_SHA256_IDENT: Ident = Ident::new_unwrap(Self::PBKDF2_SHA256_ID);
/// PBKDF2 (SHA-512) algorithm identifier
#[cfg(feature = "phc")]
pub(crate) const PBKDF2_SHA512_IDENT: Ident = Ident::new_unwrap(Self::PBKDF2_SHA512_ID);
/// Default algorithm suggested by the [OWASP cheat sheet]:
///
@@ -45,25 +60,20 @@ impl Algorithm {
id.as_ref().parse()
}
/// Get the [`Ident`] that corresponds to this PBKDF2 [`Algorithm`].
pub fn ident(&self) -> &'static Ident {
/// Get the Modular Crypt Format algorithm identifier for this algorithm.
pub const fn to_str(self) -> &'static str {
match self {
#[cfg(feature = "sha1")]
Algorithm::Pbkdf2Sha1 => &Self::PBKDF2_SHA1_IDENT,
Algorithm::Pbkdf2Sha256 => &Self::PBKDF2_SHA256_IDENT,
Algorithm::Pbkdf2Sha512 => &Self::PBKDF2_SHA512_IDENT,
Algorithm::Pbkdf2Sha1 => Self::PBKDF2_SHA1_ID,
Algorithm::Pbkdf2Sha256 => Self::PBKDF2_SHA256_ID,
Algorithm::Pbkdf2Sha512 => Self::PBKDF2_SHA512_ID,
}
}
/// Get the identifier string for this PBKDF2 [`Algorithm`].
pub fn as_str(&self) -> &str {
self.ident().as_str()
}
}
impl AsRef<str> for Algorithm {
fn as_ref(&self) -> &str {
self.as_str()
self.to_str()
}
}
@@ -75,7 +85,7 @@ impl Default for Algorithm {
impl Display for Algorithm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
f.write_str(self.to_str())
}
}
@@ -87,9 +97,15 @@ impl FromStr for Algorithm {
}
}
#[cfg(feature = "phc")]
impl From<Algorithm> for Ident {
fn from(alg: Algorithm) -> Ident {
*alg.ident()
match alg {
#[cfg(feature = "sha1")]
Algorithm::Pbkdf2Sha1 => Algorithm::PBKDF2_SHA1_IDENT,
Algorithm::Pbkdf2Sha256 => Algorithm::PBKDF2_SHA256_IDENT,
Algorithm::Pbkdf2Sha512 => Algorithm::PBKDF2_SHA512_IDENT,
}
}
}
@@ -97,11 +113,11 @@ impl<'a> TryFrom<&'a str> for Algorithm {
type Error = Error;
fn try_from(name: &'a str) -> password_hash::Result<Algorithm> {
match name.try_into() {
match name {
#[cfg(feature = "sha1")]
Ok(Self::PBKDF2_SHA1_IDENT) => Ok(Algorithm::Pbkdf2Sha1),
Ok(Self::PBKDF2_SHA256_IDENT) => Ok(Algorithm::Pbkdf2Sha256),
Ok(Self::PBKDF2_SHA512_IDENT) => Ok(Algorithm::Pbkdf2Sha512),
Self::PBKDF2_SHA1_ID => Ok(Algorithm::Pbkdf2Sha1),
Self::PBKDF2_SHA256_ID => Ok(Algorithm::Pbkdf2Sha256),
Self::PBKDF2_SHA512_ID => Ok(Algorithm::Pbkdf2Sha512),
_ => Err(Error::Algorithm),
}
}

View File

@@ -21,6 +21,10 @@
//!
//! [KDF]: https://github.com/RustCrypto/KDFs
//!
//! ## Low-level API
//!
//! This API operates directly on byte slices:
//!
//! ```
//! # #[cfg(feature = "hmac")] {
//! use hex_literal::hex;
@@ -57,15 +61,21 @@
//! rand_core = { version = "0.6", features = ["std"] }
//! ```
//!
//! ## PHC string API
//!
//! This crate can produce and verify password hash strings encoded in the Password Hashing
//! Competition (PHC) string format.
//!
//! The following example demonstrates the high-level password hashing API:
//!
#![cfg_attr(feature = "password-hash", doc = "```")]
#![cfg_attr(not(feature = "password-hash"), doc = "```ignore")]
#![cfg_attr(all(feature = "getrandom", feature = "phc"), doc = "```")]
#![cfg_attr(not(all(feature = "getrandom", feature = "phc")), doc = "```ignore")]
//! # fn main() -> Result<(), Box<dyn core::error::Error>> {
//! // NOTE: example requires `getrandom` feature is enabled
//!
//! use pbkdf2::{
//! password_hash::{PasswordHasher, PasswordVerifier, phc::PasswordHash},
//! password_hash::{PasswordHasher, PasswordVerifier},
//! phc::PasswordHash,
//! Pbkdf2
//! };
//!
@@ -73,33 +83,34 @@
//! let password = b"hunter2"; // Bad password; don't actually use!
//!
//! // Hash password to PHC string ($pbkdf2-sha256$...)
//! let password_hash = pbkdf2.hash_password(password)?.to_string();
//! let pwhash: PasswordHash = pbkdf2.hash_password(password)?;
//! let pwhash_string = pwhash.to_string();
//!
//! // Verify password against PHC string
//! let parsed_hash = PasswordHash::new(&password_hash)?;
//! assert!(pbkdf2.verify_password(password, &parsed_hash).is_ok());
//! let parsed_hash = PasswordHash::new(&pwhash_string)?;
//! pbkdf2.verify_password(password, &parsed_hash)?;
//! # Ok(())
//! # }
//! ```
#[cfg(feature = "password-hash")]
extern crate alloc;
#[cfg(feature = "mcf")]
pub mod mcf;
#[cfg(feature = "phc")]
pub mod phc;
#[cfg(feature = "password-hash")]
pub use password_hash;
#[cfg(feature = "password-hash")]
#[cfg(any(feature = "mcf", feature = "phc"))]
mod algorithm;
#[cfg(feature = "password-hash")]
#[cfg(any(feature = "mcf", feature = "phc"))]
mod params;
#[cfg(feature = "password-hash")]
mod phc;
#[cfg(any(feature = "mcf", feature = "phc"))]
pub use crate::{algorithm::Algorithm, params::Params};
#[cfg(feature = "hmac")]
pub use hmac;
#[cfg(feature = "password-hash")]
pub use crate::{algorithm::Algorithm, params::Params, phc::Pbkdf2};
#[cfg(any(feature = "mcf", feature = "phc"))]
pub use password_hash;
#[cfg(any(feature = "mcf", feature = "phc"))]
pub use password_hash::{PasswordHasher, PasswordVerifier};
use digest::{FixedOutput, InvalidLength, KeyInit, Update, typenum::Unsigned};
@@ -242,3 +253,47 @@ where
pbkdf2_hmac::<D>(password, salt, rounds, &mut buf);
buf
}
/// PBKDF2 type for use with the [`PasswordHasher`] and [`PasswordVerifier`] traits, which
/// implements support for password hash strings.
///
/// Supports the following password hash string formats, gated under the following crate features:
/// - `mcf`: support for the Modular Crypt Format
/// - `phc`: support for the Password Hashing Competition string format
#[cfg(any(feature = "mcf", feature = "phc"))]
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub struct Pbkdf2 {
/// Algorithm to use
algorithm: Algorithm,
/// Default parameters to use.
params: Params,
}
#[cfg(any(feature = "mcf", feature = "phc"))]
impl Pbkdf2 {
/// Initialize [`Pbkdf2`] with default parameters.
pub const fn new(algorithm: Algorithm, params: Params) -> Self {
Self { algorithm, params }
}
}
#[cfg(any(feature = "mcf", feature = "phc"))]
impl From<Algorithm> for Pbkdf2 {
fn from(algorithm: Algorithm) -> Self {
Self {
algorithm,
params: Params::default(),
}
}
}
#[cfg(any(feature = "mcf", feature = "phc"))]
impl From<Params> for Pbkdf2 {
fn from(params: Params) -> Self {
Self {
algorithm: Algorithm::default(),
params,
}
}
}

100
pbkdf2/src/mcf.rs Normal file
View File

@@ -0,0 +1,100 @@
//! Implementation of the `password-hash` traits for Modular Crypt Format (MCF) password hash
//! strings which begin with `$7$`:
//!
//! <https://man.archlinux.org/man/crypt.5#scrypt>
pub use mcf::{PasswordHash, PasswordHashRef};
use crate::{Algorithm, Params, Pbkdf2, pbkdf2_hmac};
use mcf::Base64;
use password_hash::{CustomizedPasswordHasher, Error, PasswordHasher, Result, Version};
use sha2::{Sha256, Sha512};
#[cfg(feature = "sha1")]
use sha1::Sha1;
/// Base64 variant used by PBKDF2's MCF implementation: unpadded standard Base64.
const PBKDF2_BASE64: Base64 = Base64::B64;
impl CustomizedPasswordHasher<PasswordHash> for Pbkdf2 {
type Params = Params;
fn hash_password_customized(
&self,
password: &[u8],
salt: &[u8],
alg_id: Option<&str>,
version: Option<Version>,
params: Params,
) -> Result<PasswordHash> {
let algorithm = alg_id
.map(Algorithm::try_from)
.transpose()?
.unwrap_or(self.algorithm);
if version.is_some() {
return Err(Error::Version);
}
let mut buffer = [0u8; Params::MAX_LENGTH];
let out = buffer
.get_mut(..params.output_length)
.ok_or(Error::OutputSize)?;
let f = match algorithm {
#[cfg(feature = "sha1")]
Algorithm::Pbkdf2Sha1 => pbkdf2_hmac::<Sha1>,
Algorithm::Pbkdf2Sha256 => pbkdf2_hmac::<Sha256>,
Algorithm::Pbkdf2Sha512 => pbkdf2_hmac::<Sha512>,
};
f(password, salt, params.rounds, out);
let mut mcf_hash = PasswordHash::from_id(algorithm.to_str()).expect("should have valid ID");
mcf_hash
.push_displayable(params)
.map_err(|_| Error::EncodingInvalid)?;
mcf_hash.push_base64(salt, PBKDF2_BASE64);
mcf_hash.push_base64(out, PBKDF2_BASE64);
Ok(mcf_hash)
}
}
impl 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)
}
}
// TODO(tarcieri): tests for SHA-1 and SHA-512
#[cfg(test)]
mod tests {
use super::PBKDF2_BASE64;
use crate::{Params, Pbkdf2};
use mcf::PasswordHash;
use password_hash::CustomizedPasswordHasher;
// Example adapted from:
// <https://passlib.readthedocs.io/en/stable/lib/passlib.hash.pbkdf2_digest.html>
const EXAMPLE_PASSWORD: &[u8] = b"password";
const EXAMPLE_ROUNDS: u32 = 8000;
const EXAMPLE_SALT: &str = "XAuBMIYQQogxRg";
const EXAMPLE_HASH: &str =
"$pbkdf2-sha256$8000$XAuBMIYQQogxRg$tRRlz8hYn63B9LYiCd6PRo6FMiunY9ozmMMI3srxeRE";
#[test]
fn hash_password_sha256() {
let salt = PBKDF2_BASE64.decode_vec(EXAMPLE_SALT).unwrap();
let params = Params::new(EXAMPLE_ROUNDS);
let actual_hash: PasswordHash = Pbkdf2::default()
.hash_password_with_params(EXAMPLE_PASSWORD, salt.as_slice(), params)
.unwrap();
let expected_hash = PasswordHash::new(EXAMPLE_HASH).unwrap();
assert_eq!(expected_hash, actual_hash);
}
}

View File

@@ -1,10 +1,13 @@
use core::{
fmt::{self, Display, Formatter},
fmt::{self, Display},
num::ParseIntError,
str::FromStr,
};
#[cfg(feature = "phc")]
use password_hash::{
Error,
phc::{Decimal, ParamsString, PasswordHash},
phc::{self, Decimal, ParamsString},
};
/// PBKDF2 params
@@ -18,6 +21,12 @@ pub struct Params {
}
impl Params {
/// Maximum supported output length.
pub const MAX_LENGTH: usize = 64;
/// Recommended output length.
pub const RECOMMENDED_LENGTH: usize = 32;
/// Recommended number of PBKDF2 rounds (used by default).
///
/// This number is adopted from the [OWASP cheat sheet]:
@@ -25,15 +34,22 @@ impl Params {
/// > Use PBKDF2 with a work factor of 600,000 or more
///
/// [OWASP cheat sheet]: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
pub const RECOMMENDED_ROUNDS: usize = 600_000;
pub const RECOMMENDED_ROUNDS: u32 = 600_000;
/// Recommended PBKDF2 parameters adapted from the [OWASP cheat sheet].
///
/// [OWASP cheat sheet]: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
pub const RECOMMENDED: Self = Params {
rounds: Self::RECOMMENDED_ROUNDS as u32,
output_length: 32,
rounds: Self::RECOMMENDED_ROUNDS,
output_length: Self::RECOMMENDED_LENGTH,
};
/// Create new params with the given number of rounds.
pub fn new(rounds: u32) -> Self {
let mut ret = Self::RECOMMENDED;
ret.rounds = rounds;
ret
}
}
impl Default for Params {
@@ -43,20 +59,20 @@ impl Default for Params {
}
impl Display for Params {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
ParamsString::try_from(self).map_err(|_| fmt::Error)?.fmt(f)
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.rounds)
}
}
impl FromStr for Params {
type Err = Error;
type Err = ParseIntError;
fn from_str(s: &str) -> password_hash::Result<Self> {
let params_string = ParamsString::from_str(s).map_err(|_| Error::ParamsInvalid)?;
Self::try_from(&params_string)
fn from_str(s: &str) -> Result<Self, ParseIntError> {
u32::from_str(s).map(Params::new)
}
}
#[cfg(feature = "phc")]
impl TryFrom<&ParamsString> for Params {
type Error = Error;
@@ -71,11 +87,17 @@ impl TryFrom<&ParamsString> for Params {
.map_err(|_| Error::ParamInvalid { name: "i" })?
}
"l" => {
params.output_length = value
let output_length = value
.decimal()
.ok()
.and_then(|dec| dec.try_into().ok())
.ok_or(Error::ParamInvalid { name: "l" })?;
if output_length > Self::MAX_LENGTH {
return Err(Error::ParamInvalid { name: "l" });
}
params.output_length = output_length;
}
_ => return Err(Error::ParamsInvalid),
}
@@ -85,10 +107,11 @@ impl TryFrom<&ParamsString> for Params {
}
}
impl TryFrom<&PasswordHash> for Params {
#[cfg(feature = "phc")]
impl TryFrom<&phc::PasswordHash> for Params {
type Error = Error;
fn try_from(hash: &PasswordHash) -> password_hash::Result<Self> {
fn try_from(hash: &phc::PasswordHash) -> password_hash::Result<Self> {
if hash.version.is_some() {
return Err(Error::Version);
}
@@ -105,6 +128,7 @@ impl TryFrom<&PasswordHash> for Params {
}
}
#[cfg(feature = "phc")]
impl TryFrom<Params> for ParamsString {
type Error = Error;
@@ -113,6 +137,7 @@ impl TryFrom<Params> for ParamsString {
}
}
#[cfg(feature = "phc")]
impl TryFrom<&Params> for ParamsString {
type Error = Error;

View File

@@ -1,50 +1,17 @@
//! Implementation of the `password-hash` crate API.
use crate::{Algorithm, Params, pbkdf2_hmac};
pub use password_hash::phc::PasswordHash;
use crate::{Algorithm, Params, Pbkdf2, pbkdf2_hmac};
use password_hash::{
CustomizedPasswordHasher, Error, PasswordHasher, Result,
phc::{Output, PasswordHash, Salt},
phc::{Output, Salt},
};
use sha2::{Sha256, Sha512};
#[cfg(feature = "sha1")]
use sha1::Sha1;
/// PBKDF2 type for use with [`PasswordHasher`].
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub struct Pbkdf2 {
/// Algorithm to use
algorithm: Algorithm,
/// Default parameters to use.
params: Params,
}
impl Pbkdf2 {
/// Initialize [`Pbkdf2`] with default parameters.
pub const fn new(algorithm: Algorithm, params: Params) -> Self {
Self { algorithm, params }
}
}
impl From<Algorithm> for Pbkdf2 {
fn from(algorithm: Algorithm) -> Self {
Self {
algorithm,
params: Params::default(),
}
}
}
impl From<Params> for Pbkdf2 {
fn from(params: Params) -> Self {
Self {
algorithm: Algorithm::default(),
params,
}
}
}
impl CustomizedPasswordHasher<PasswordHash> for Pbkdf2 {
type Params = Params;
@@ -68,7 +35,7 @@ impl CustomizedPasswordHasher<PasswordHash> for Pbkdf2 {
let salt = Salt::new(salt)?;
let mut buffer = [0u8; Output::MAX_LENGTH];
let mut buffer = [0u8; Params::MAX_LENGTH];
let out = buffer
.get_mut(..params.output_length)
.ok_or(Error::OutputSize)?;
@@ -84,7 +51,7 @@ impl CustomizedPasswordHasher<PasswordHash> for Pbkdf2 {
let output = Output::new(out)?;
Ok(PasswordHash {
algorithm: *algorithm.ident(),
algorithm: algorithm.into(),
version: None,
params: params.try_into()?,
salt: Some(salt),
@@ -98,3 +65,54 @@ impl PasswordHasher<PasswordHash> for Pbkdf2 {
self.hash_password_customized(password, salt, None, None, self.params)
}
}
#[cfg(test)]
mod tests {
use super::PasswordHash;
use crate::{Params, Pbkdf2};
use hex_literal::hex;
use password_hash::CustomizedPasswordHasher;
const PASSWORD: &[u8] = b"passwordPASSWORDpassword";
const SALT: &[u8] = b"saltSALTsaltSALTsaltSALTsaltSALTsalt";
const EXPECTED_HASH: &str = "$pbkdf2-sha256$i=4096,l=40\
$c2FsdFNBTFRzYWx0U0FMVHNhbHRTQUxUc2FsdFNBTFRzYWx0\
$NIyJ28vTKy8y2BS4EW6EzysXNH68GAAYHE4qH7jdU+HGNVGMfaxH6Q";
/// Test with `algorithm: None` - uses default PBKDF2-SHA256
///
/// Input:
/// - P = "passwordPASSWORDpassword" (24 octets)
/// - S = "saltSALTsaltSALTsaltSALTsaltSALTsalt" (36 octets)
/// c = 4096
/// dkLen = 40
#[test]
fn hash_with_default_algorithm() {
let params = Params {
rounds: 4096,
output_length: 40,
};
let pwhash: PasswordHash = Pbkdf2::default()
.hash_password_customized(PASSWORD, SALT, None, None, params)
.unwrap();
assert_eq!(
pwhash.algorithm,
crate::algorithm::Algorithm::Pbkdf2Sha256.into()
);
assert_eq!(pwhash.salt.unwrap().as_ref(), SALT);
assert_eq!(Params::try_from(&pwhash).unwrap(), params);
let expected_output = hex!(
"34 8c 89 db cb d3 2b 2f
32 d8 14 b8 11 6e 84 cf
2b 17 34 7e bc 18 00 18
1c 4e 2a 1f b8 dd 53 e1
c6 35 51 8c 7d ac 47 e9 "
);
assert_eq!(pwhash.hash.unwrap().as_ref(), expected_output);
assert_eq!(pwhash, EXPECTED_HASH.parse().unwrap());
}
}

View File

@@ -3,47 +3,4 @@
//!
//! PHC PBKDF2-SHA256 vectors adapted from: https://stackoverflow.com/a/5136918
#![cfg(feature = "password-hash")]
use hex_literal::hex;
use pbkdf2::{Algorithm, Params, Pbkdf2, password_hash::CustomizedPasswordHasher};
const PASSWORD: &[u8] = b"passwordPASSWORDpassword";
const SALT: &[u8] = b"saltSALTsaltSALTsaltSALTsaltSALTsalt";
const EXPECTED_HASH: &str = "$pbkdf2-sha256$i=4096,\
l=40$c2FsdFNBTFRzYWx0U0FMVHNhbHRTQUxUc2FsdFNBTFRzYWx0$NIyJ28vTKy8y2BS4EW6EzysXNH68GAAYHE4qH7jdU\
+HGNVGMfaxH6Q";
/// Test with `algorithm: None` - uses default PBKDF2-SHA256
///
/// Input:
/// - P = "passwordPASSWORDpassword" (24 octets)
/// - S = "saltSALTsaltSALTsaltSALTsaltSALTsalt" (36 octets)
/// c = 4096
/// dkLen = 40
#[test]
fn hash_with_default_algorithm() {
let params = Params {
rounds: 4096,
output_length: 40,
};
let hash = Pbkdf2::default()
.hash_password_customized(PASSWORD, SALT, None, None, params)
.unwrap();
assert_eq!(hash.algorithm, *Algorithm::Pbkdf2Sha256.ident());
assert_eq!(hash.salt.unwrap().as_ref(), SALT);
assert_eq!(Params::try_from(&hash).unwrap(), params);
let expected_output = hex!(
"34 8c 89 db cb d3 2b 2f
32 d8 14 b8 11 6e 84 cf
2b 17 34 7e bc 18 00 18
1c 4e 2a 1f b8 dd 53 e1
c6 35 51 8c 7d ac 47 e9 "
);
assert_eq!(hash.hash.unwrap().as_ref(), expected_output);
assert_eq!(hash, EXPECTED_HASH.parse().unwrap());
}
#![cfg(feature = "phc")]

View File

@@ -38,7 +38,7 @@
//!
//! // Verify password against PHC string
//! let parsed_hash = PasswordHash::new(&hash_string)?;
//! assert!(scrypt.verify_password(password, &parsed_hash).is_ok());
//! scrypt.verify_password(password, &parsed_hash)?;
//! # Ok(())
//! # }
//! ```

View File

@@ -2,7 +2,7 @@ use core::{fmt, str::FromStr};
use password_hash::Error;
/// SHA-crypt algorithm variants: SHA-256 or SHA-512.
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
#[non_exhaustive]
pub enum Algorithm {
/// SHA-256-crypt: SHA-crypt instantiated with SHA-256.
@@ -35,28 +35,23 @@ impl Algorithm {
}
/// Get the Modular Crypt Format algorithm identifier for this algorithm.
pub const fn ident(&self) -> &'static str {
pub const fn to_str(self) -> &'static str {
match self {
Algorithm::Sha256Crypt => Self::SHA256_CRYPT_IDENT,
Algorithm::Sha512Crypt => Self::SHA512_CRYPT_IDENT,
}
}
/// Get the identifier string for this PBKDF2 [`Algorithm`].
pub fn as_str(&self) -> &'static str {
self.ident()
}
}
impl AsRef<str> for Algorithm {
fn as_ref(&self) -> &str {
self.as_str()
self.to_str()
}
}
impl fmt::Display for Algorithm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
f.write_str(self.to_str())
}
}

View File

@@ -41,7 +41,7 @@ impl CustomizedPasswordHasher<PasswordHash> for ShaCrypt {
params: Params,
) -> Result<PasswordHash> {
let alg = alg_id
.map(|id| id.parse::<Algorithm>())
.map(Algorithm::try_from)
.transpose()?
.unwrap_or(self.algorithm);
@@ -51,7 +51,7 @@ impl CustomizedPasswordHasher<PasswordHash> for ShaCrypt {
// We compute the function over the Base64-encoded salt
let salt = Base64ShaCrypt::encode_string(salt);
let mut mcf_hash = PasswordHash::from_id(alg.ident()).expect("should have valid ID");
let mut mcf_hash = PasswordHash::from_id(alg.to_str()).expect("should have valid ID");
mcf_hash
.push_displayable(params)