diff --git a/.readme/Cargo.toml b/.readme/Cargo.toml index 6907078..d67e13b 100644 --- a/.readme/Cargo.toml +++ b/.readme/Cargo.toml @@ -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"] } diff --git a/Cargo.lock b/Cargo.lock index aca1df6..c70950c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 550af35..b65310e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,3 +20,5 @@ opt-level = 2 argon2 = { path = "./argon2" } pbkdf2 = { path = "./pbkdf2" } scrypt = { path = "./scrypt" } + +password-hash = { git = "https://github.com/RustCrypto/traits" } diff --git a/password-auth/Cargo.toml b/password-auth/Cargo.toml index e254a7e..68ab019 100644 --- a/password-auth/Cargo.toml +++ b/password-auth/Cargo.toml @@ -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] diff --git a/password-auth/src/lib.rs b/password-auth/src/lib.rs index f987eb8..438936d 100644 --- a/password-auth/src/lib.rs +++ b/password-auth/src/lib.rs @@ -122,7 +122,7 @@ pub fn is_hash_obsolete(hash: &str) -> Result { || hash.params != default_params_string::()); #[cfg(feature = "pbkdf2")] - return Ok(hash.algorithm != *pbkdf2::Algorithm::default().ident() + return Ok(hash.algorithm != pbkdf2::Algorithm::default().into() || hash.params != default_params_string::()); Ok(true) diff --git a/pbkdf2/Cargo.toml b/pbkdf2/Cargo.toml index dd56f35..f9b641c 100644 --- a/pbkdf2/Cargo.toml +++ b/pbkdf2/Cargo.toml @@ -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] diff --git a/pbkdf2/src/algorithm.rs b/pbkdf2/src/algorithm.rs index e649b14..538cd14 100644 --- a/pbkdf2/src/algorithm.rs +++ b/pbkdf2/src/algorithm.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 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 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 { - 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), } } diff --git a/pbkdf2/src/lib.rs b/pbkdf2/src/lib.rs index eef6fcb..f11a5f6 100644 --- a/pbkdf2/src/lib.rs +++ b/pbkdf2/src/lib.rs @@ -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> { //! // 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::(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 for Pbkdf2 { + fn from(algorithm: Algorithm) -> Self { + Self { + algorithm, + params: Params::default(), + } + } +} + +#[cfg(any(feature = "mcf", feature = "phc"))] +impl From for Pbkdf2 { + fn from(params: Params) -> Self { + Self { + algorithm: Algorithm::default(), + params, + } + } +} diff --git a/pbkdf2/src/mcf.rs b/pbkdf2/src/mcf.rs new file mode 100644 index 0000000..d8461e1 --- /dev/null +++ b/pbkdf2/src/mcf.rs @@ -0,0 +1,100 @@ +//! Implementation of the `password-hash` traits for Modular Crypt Format (MCF) password hash +//! strings which begin with `$7$`: +//! +//! + +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 for Pbkdf2 { + type Params = Params; + + fn hash_password_customized( + &self, + password: &[u8], + salt: &[u8], + alg_id: Option<&str>, + version: Option, + params: Params, + ) -> Result { + 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::, + Algorithm::Pbkdf2Sha256 => pbkdf2_hmac::, + Algorithm::Pbkdf2Sha512 => pbkdf2_hmac::, + }; + + 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 for Pbkdf2 { + fn hash_password_with_salt(&self, password: &[u8], salt: &[u8]) -> Result { + 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: + // + + 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); + } +} diff --git a/pbkdf2/src/params.rs b/pbkdf2/src/params.rs index 5135fec..2a2199d 100644 --- a/pbkdf2/src/params.rs +++ b/pbkdf2/src/params.rs @@ -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 { - let params_string = ParamsString::from_str(s).map_err(|_| Error::ParamsInvalid)?; - Self::try_from(¶ms_string) + fn from_str(s: &str) -> Result { + 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 { + fn try_from(hash: &phc::PasswordHash) -> password_hash::Result { if hash.version.is_some() { return Err(Error::Version); } @@ -105,6 +128,7 @@ impl TryFrom<&PasswordHash> for Params { } } +#[cfg(feature = "phc")] impl TryFrom for ParamsString { type Error = Error; @@ -113,6 +137,7 @@ impl TryFrom for ParamsString { } } +#[cfg(feature = "phc")] impl TryFrom<&Params> for ParamsString { type Error = Error; diff --git a/pbkdf2/src/phc.rs b/pbkdf2/src/phc.rs index 7baa39e..235f4b7 100644 --- a/pbkdf2/src/phc.rs +++ b/pbkdf2/src/phc.rs @@ -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 for Pbkdf2 { - fn from(algorithm: Algorithm) -> Self { - Self { - algorithm, - params: Params::default(), - } - } -} - -impl From for Pbkdf2 { - fn from(params: Params) -> Self { - Self { - algorithm: Algorithm::default(), - params, - } - } -} - impl CustomizedPasswordHasher for Pbkdf2 { type Params = Params; @@ -68,7 +35,7 @@ impl CustomizedPasswordHasher 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 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 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()); + } +} diff --git a/pbkdf2/tests/mod.rs b/pbkdf2/tests/pbkdf2.rs similarity index 100% rename from pbkdf2/tests/mod.rs rename to pbkdf2/tests/pbkdf2.rs diff --git a/pbkdf2/tests/phc.rs b/pbkdf2/tests/phc.rs index cf4f1cb..9da1ba3 100644 --- a/pbkdf2/tests/phc.rs +++ b/pbkdf2/tests/phc.rs @@ -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")] diff --git a/scrypt/src/lib.rs b/scrypt/src/lib.rs index 72087c4..3876dca 100644 --- a/scrypt/src/lib.rs +++ b/scrypt/src/lib.rs @@ -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(()) //! # } //! ``` diff --git a/sha-crypt/src/algorithm.rs b/sha-crypt/src/algorithm.rs index c2a5f6a..1dd5311 100644 --- a/sha-crypt/src/algorithm.rs +++ b/sha-crypt/src/algorithm.rs @@ -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 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()) } } diff --git a/sha-crypt/src/mcf.rs b/sha-crypt/src/mcf.rs index 12d5220..889499e 100644 --- a/sha-crypt/src/mcf.rs +++ b/sha-crypt/src/mcf.rs @@ -41,7 +41,7 @@ impl CustomizedPasswordHasher for ShaCrypt { params: Params, ) -> Result { let alg = alg_id - .map(|id| id.parse::()) + .map(Algorithm::try_from) .transpose()? .unwrap_or(self.algorithm); @@ -51,7 +51,7 @@ impl CustomizedPasswordHasher 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)