mirror of
https://github.com/RustCrypto/password-hashes.git
synced 2026-01-25 04:06:23 +00:00
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:
@@ -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
4
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -20,3 +20,5 @@ opt-level = 2
|
||||
argon2 = { path = "./argon2" }
|
||||
pbkdf2 = { path = "./pbkdf2" }
|
||||
scrypt = { path = "./scrypt" }
|
||||
|
||||
password-hash = { git = "https://github.com/RustCrypto/traits" }
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
100
pbkdf2/src/mcf.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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(¶ms_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;
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user