diff --git a/pbkdf2/src/lib.rs b/pbkdf2/src/lib.rs index f11a5f64..b04e1f9d 100644 --- a/pbkdf2/src/lib.rs +++ b/pbkdf2/src/lib.rs @@ -93,6 +93,9 @@ //! # } //! ``` +#[cfg(feature = "mcf")] +extern crate alloc; + #[cfg(feature = "mcf")] pub mod mcf; #[cfg(feature = "phc")] @@ -270,6 +273,15 @@ pub struct Pbkdf2 { params: Params, } +#[cfg(any(feature = "mcf", feature = "phc"))] +impl Pbkdf2 { + /// PBKDF2 configured with SHA-256 as the default. + pub const SHA256: Self = Self::new(Algorithm::Pbkdf2Sha256, Params::RECOMMENDED); + + /// PBKDF2 configured with SHA-512 as the default. + pub const SHA512: Self = Self::new(Algorithm::Pbkdf2Sha512, Params::RECOMMENDED); +} + #[cfg(any(feature = "mcf", feature = "phc"))] impl Pbkdf2 { /// Initialize [`Pbkdf2`] with default parameters. diff --git a/pbkdf2/src/mcf.rs b/pbkdf2/src/mcf.rs index d8461e12..dfd531a9 100644 --- a/pbkdf2/src/mcf.rs +++ b/pbkdf2/src/mcf.rs @@ -6,6 +6,7 @@ pub use mcf::{PasswordHash, PasswordHashRef}; use crate::{Algorithm, Params, Pbkdf2, pbkdf2_hmac}; +use alloc::string::String; use mcf::Base64; use password_hash::{CustomizedPasswordHasher, Error, PasswordHasher, Result, Version}; use sha2::{Sha256, Sha512}; @@ -13,8 +14,8 @@ 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; +#[cfg(test)] +use alloc::vec::Vec; impl CustomizedPasswordHasher for Pbkdf2 { type Params = Params; @@ -38,7 +39,7 @@ impl CustomizedPasswordHasher for Pbkdf2 { let mut buffer = [0u8; Params::MAX_LENGTH]; let out = buffer - .get_mut(..params.output_length) + .get_mut(..params.output_len()) .ok_or(Error::OutputSize)?; let f = match algorithm { @@ -48,15 +49,21 @@ impl CustomizedPasswordHasher for Pbkdf2 { Algorithm::Pbkdf2Sha512 => pbkdf2_hmac::, }; - f(password, salt, params.rounds, out); + 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); + + mcf_hash + .push_str(&base64_encode(salt)) + .map_err(|_| Error::EncodingInvalid)?; + + mcf_hash + .push_str(&base64_encode(out)) + .map_err(|_| Error::EncodingInvalid)?; Ok(mcf_hash) } @@ -68,26 +75,39 @@ impl PasswordHasher for Pbkdf2 { } } +// Base64 support: PBKDF2 uses a variant of standard unpadded Base64 which substitutes the `+` +// character for `.` and this is a distinct encoding from the bcrypt and crypt Base64 variants. + +#[cfg(test)] +fn base64_decode(base64: &str) -> Result> { + Base64::B64 + .decode_vec(&base64.replace('.', "+")) + .map_err(|_| Error::EncodingInvalid) +} + +fn base64_encode(bytes: &[u8]) -> String { + Base64::B64.encode_string(bytes).replace('+', ".") +} + // TODO(tarcieri): tests for SHA-1 and SHA-512 #[cfg(test)] mod tests { - use super::PBKDF2_BASE64; + use super::base64_decode; 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(); + 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"; + + let salt = base64_decode(EXAMPLE_SALT).unwrap(); let params = Params::new(EXAMPLE_ROUNDS); let actual_hash: PasswordHash = Pbkdf2::default() @@ -97,4 +117,24 @@ mod tests { let expected_hash = PasswordHash::new(EXAMPLE_HASH).unwrap(); assert_eq!(expected_hash, actual_hash); } + + // Example adapted from: + // + #[test] + fn hash_password_sha512() { + const EXAMPLE_PASSWORD: &[u8] = b"abcdefghijklmnop"; + const EXAMPLE_ROUNDS: u32 = 25000; + const EXAMPLE_SALT: &str = "O4fwPmdMyRmDUIrx/h9jTA"; + const EXAMPLE_HASH: &str = "$pbkdf2-sha512$25000$O4fwPmdMyRmDUIrx/h9jTA$Xlp267ZwEbG4aOpN3Bve/ATo3rFA7WH8iMdS16Xbe9rc6P5welk1yiXEMPy7.BFp0qsncipHumaW1trCWVvq/A"; + + let salt = base64_decode(EXAMPLE_SALT).unwrap(); + let params = Params::new_with_output_len(EXAMPLE_ROUNDS, 64); + + let actual_hash: PasswordHash = Pbkdf2::SHA512 + .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 2a2199de..91fa6404 100644 --- a/pbkdf2/src/params.rs +++ b/pbkdf2/src/params.rs @@ -14,10 +14,10 @@ use password_hash::{ #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct Params { /// Number of rounds - pub rounds: u32, + rounds: u32, /// Size of the output (in bytes) - pub output_length: usize, + output_len: usize, } impl Params { @@ -41,15 +41,33 @@ impl Params { /// [OWASP cheat sheet]: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html pub const RECOMMENDED: Self = Params { rounds: Self::RECOMMENDED_ROUNDS, - output_length: Self::RECOMMENDED_LENGTH, + output_len: Self::RECOMMENDED_LENGTH, }; /// Create new params with the given number of rounds. - pub fn new(rounds: u32) -> Self { + pub const fn new(rounds: u32) -> Self { let mut ret = Self::RECOMMENDED; ret.rounds = rounds; ret } + + /// Create new params with a customized output length. + pub const fn new_with_output_len(rounds: u32, output_length: usize) -> Self { + Self { + rounds, + output_len: output_length, + } + } + + /// Get the number of rounds. + pub const fn rounds(self) -> u32 { + self.rounds + } + + /// Get the output length. + pub const fn output_len(self) -> usize { + self.output_len + } } impl Default for Params { @@ -87,17 +105,17 @@ impl TryFrom<&ParamsString> for Params { .map_err(|_| Error::ParamInvalid { name: "i" })? } "l" => { - let output_length = value + let len = value .decimal() .ok() .and_then(|dec| dec.try_into().ok()) .ok_or(Error::ParamInvalid { name: "l" })?; - if output_length > Self::MAX_LENGTH { + if len > Self::MAX_LENGTH { return Err(Error::ParamInvalid { name: "l" }); } - params.output_length = output_length; + params.output_len = len; } _ => return Err(Error::ParamsInvalid), } @@ -119,7 +137,7 @@ impl TryFrom<&phc::PasswordHash> for Params { let params = Self::try_from(&hash.params)?; if let Some(hash) = &hash.hash { - if hash.len() != params.output_length { + if hash.len() != params.output_len { return Err(Error::OutputSize); } } @@ -144,7 +162,7 @@ impl TryFrom<&Params> for ParamsString { fn try_from(input: &Params) -> password_hash::Result { let mut output = ParamsString::new(); - for (name, value) in [("i", input.rounds), ("l", input.output_length as Decimal)] { + for (name, value) in [("i", input.rounds), ("l", input.output_len as Decimal)] { output .add_decimal(name, value) .map_err(|_| Error::ParamInvalid { name })?; diff --git a/pbkdf2/src/phc.rs b/pbkdf2/src/phc.rs index 235f4b75..e5b13226 100644 --- a/pbkdf2/src/phc.rs +++ b/pbkdf2/src/phc.rs @@ -37,7 +37,7 @@ impl CustomizedPasswordHasher for Pbkdf2 { let mut buffer = [0u8; Params::MAX_LENGTH]; let out = buffer - .get_mut(..params.output_length) + .get_mut(..params.output_len()) .ok_or(Error::OutputSize)?; let f = match algorithm { @@ -47,7 +47,7 @@ impl CustomizedPasswordHasher for Pbkdf2 { Algorithm::Pbkdf2Sha512 => pbkdf2_hmac::, }; - f(password, &salt, params.rounds, out); + f(password, &salt, params.rounds(), out); let output = Output::new(out)?; Ok(PasswordHash { @@ -88,10 +88,7 @@ mod tests { /// dkLen = 40 #[test] fn hash_with_default_algorithm() { - let params = Params { - rounds: 4096, - output_length: 40, - }; + let params = Params::new_with_output_len(4096, 40); let pwhash: PasswordHash = Pbkdf2::default() .hash_password_customized(PASSWORD, SALT, None, None, params)