Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions pbkdf2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@
//! # }
//! ```

#[cfg(feature = "mcf")]
extern crate alloc;

#[cfg(feature = "mcf")]
pub mod mcf;
#[cfg(feature = "phc")]
Expand Down Expand Up @@ -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.
Expand Down
70 changes: 55 additions & 15 deletions pbkdf2/src/mcf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@
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};

#[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<PasswordHash> for Pbkdf2 {
type Params = Params;
Expand All @@ -38,7 +39,7 @@ impl CustomizedPasswordHasher<PasswordHash> 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 {
Expand All @@ -48,15 +49,21 @@ impl CustomizedPasswordHasher<PasswordHash> for Pbkdf2 {
Algorithm::Pbkdf2Sha512 => pbkdf2_hmac::<Sha512>,
};

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)
}
Expand All @@ -68,26 +75,39 @@ impl PasswordHasher<PasswordHash> 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<Vec<u8>> {
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:
// <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();
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()
Expand All @@ -97,4 +117,24 @@ mod tests {
let expected_hash = PasswordHash::new(EXAMPLE_HASH).unwrap();
assert_eq!(expected_hash, actual_hash);
}

// Example adapted from:
// <https://github.com/hlandau/passlib/blob/8f820e0/hash/pbkdf2/pbkdf2_test.go>
#[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);
}
}
36 changes: 27 additions & 9 deletions pbkdf2/src/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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),
}
Expand All @@ -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);
}
}
Expand All @@ -144,7 +162,7 @@ impl TryFrom<&Params> for ParamsString {
fn try_from(input: &Params) -> password_hash::Result<ParamsString> {
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 })?;
Expand Down
9 changes: 3 additions & 6 deletions pbkdf2/src/phc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ impl CustomizedPasswordHasher<PasswordHash> 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 {
Expand All @@ -47,7 +47,7 @@ impl CustomizedPasswordHasher<PasswordHash> for Pbkdf2 {
Algorithm::Pbkdf2Sha512 => pbkdf2_hmac::<Sha512>,
};

f(password, &salt, params.rounds, out);
f(password, &salt, params.rounds(), out);
let output = Output::new(out)?;

Ok(PasswordHash {
Expand Down Expand Up @@ -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)
Expand Down