mirror of
https://github.com/RustCrypto/password-hashes.git
synced 2026-01-25 04:06:23 +00:00
pbkdf2: impl PasswordVerifier<mcf::PasswordHash> (#809)
Adds support for verifying password hashes in MCF format.
This commit is contained in:
@@ -1,22 +1,25 @@
|
||||
//! Implementation of the `password-hash` traits for Modular Crypt Format (MCF) password hash
|
||||
//! strings which begin with `$7$`:
|
||||
//! strings which begin with `$pbkdf$`, `$pbkdf-sha256$`, or `$pbkdf-sha512`:
|
||||
//!
|
||||
//! <https://man.archlinux.org/man/crypt.5#scrypt>
|
||||
//! <https://passlib.readthedocs.io/en/stable/lib/passlib.hash.pbkdf2_digest.html>
|
||||
//!
|
||||
//! PBKDF2's MCF strings can be distinguished from PHC strings by whether the parameters
|
||||
//! field contains `rounds=` or not: if the number of rounds does NOT contain `rounds=`, but just a
|
||||
//! bare number of rounds, then it's MCF format. If it DOES contain `rounds=`, then it's PHC.
|
||||
|
||||
pub use mcf::{PasswordHash, PasswordHashRef};
|
||||
|
||||
use crate::{Algorithm, Params, Pbkdf2, pbkdf2_hmac};
|
||||
use alloc::string::String;
|
||||
use alloc::{string::String, vec::Vec};
|
||||
use mcf::Base64;
|
||||
use password_hash::{CustomizedPasswordHasher, Error, PasswordHasher, Result, Version};
|
||||
use password_hash::{
|
||||
CustomizedPasswordHasher, Error, PasswordHasher, PasswordVerifier, Result, Version,
|
||||
};
|
||||
use sha2::{Sha256, Sha512};
|
||||
|
||||
#[cfg(feature = "sha1")]
|
||||
use sha1::Sha1;
|
||||
|
||||
#[cfg(test)]
|
||||
use alloc::vec::Vec;
|
||||
|
||||
impl CustomizedPasswordHasher<PasswordHash> for Pbkdf2 {
|
||||
type Params = Params;
|
||||
|
||||
@@ -75,10 +78,67 @@ impl PasswordHasher<PasswordHash> for Pbkdf2 {
|
||||
}
|
||||
}
|
||||
|
||||
impl PasswordVerifier<PasswordHash> for Pbkdf2 {
|
||||
fn verify_password(&self, password: &[u8], hash: &PasswordHash) -> Result<()> {
|
||||
self.verify_password(password, hash.as_password_hash_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl PasswordVerifier<PasswordHashRef> for Pbkdf2 {
|
||||
fn verify_password(&self, password: &[u8], hash: &PasswordHashRef) -> Result<()> {
|
||||
let algorithm = hash.id().parse::<Algorithm>()?;
|
||||
let mut fields = hash.fields();
|
||||
let mut next = fields.next().ok_or(Error::EncodingInvalid)?;
|
||||
let mut params = Params::default();
|
||||
|
||||
// decode params
|
||||
if let Ok(p) = next.as_str().parse::<Params>() {
|
||||
params = p;
|
||||
next = fields.next().ok_or(Error::EncodingInvalid)?;
|
||||
}
|
||||
|
||||
let salt = base64_decode(next.as_str())?;
|
||||
|
||||
// decode expected password hash
|
||||
let expected = fields
|
||||
.next()
|
||||
.ok_or(Error::EncodingInvalid)
|
||||
.and_then(|field| base64_decode(field.as_str()))?;
|
||||
|
||||
// should be the last field
|
||||
if fields.next().is_some() {
|
||||
return Err(Error::EncodingInvalid);
|
||||
}
|
||||
|
||||
let mut buffer = [0u8; Params::MAX_LENGTH];
|
||||
let out = buffer.get_mut(..expected.len()).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);
|
||||
|
||||
// TODO(tarcieri): use `subtle` or `ctutils` for comparison
|
||||
if out
|
||||
.iter()
|
||||
.zip(expected.iter())
|
||||
.fold(0, |acc, (a, b)| acc | (a ^ b))
|
||||
== 0
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::PasswordInvalid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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('.', "+"))
|
||||
@@ -92,10 +152,10 @@ fn base64_encode(bytes: &[u8]) -> String {
|
||||
// TODO(tarcieri): tests for SHA-1 and SHA-512
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::base64_decode;
|
||||
use super::{Error, base64_decode};
|
||||
use crate::{Params, Pbkdf2};
|
||||
use mcf::PasswordHash;
|
||||
use password_hash::CustomizedPasswordHasher;
|
||||
use password_hash::{CustomizedPasswordHasher, PasswordVerifier};
|
||||
|
||||
// Example adapted from:
|
||||
// <https://passlib.readthedocs.io/en/stable/lib/passlib.hash.pbkdf2_digest.html>
|
||||
@@ -110,12 +170,22 @@ mod tests {
|
||||
let salt = base64_decode(EXAMPLE_SALT).unwrap();
|
||||
let params = Params::new(EXAMPLE_ROUNDS);
|
||||
|
||||
let actual_hash: PasswordHash = Pbkdf2::default()
|
||||
let actual_hash: PasswordHash = Pbkdf2::SHA256
|
||||
.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);
|
||||
|
||||
assert_eq!(
|
||||
Pbkdf2::SHA256.verify_password(EXAMPLE_PASSWORD, &actual_hash),
|
||||
Ok(())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Pbkdf2::SHA256.verify_password(b"bogus", &actual_hash),
|
||||
Err(Error::PasswordInvalid)
|
||||
);
|
||||
}
|
||||
|
||||
// Example adapted from:
|
||||
@@ -136,5 +206,15 @@ mod tests {
|
||||
|
||||
let expected_hash = PasswordHash::new(EXAMPLE_HASH).unwrap();
|
||||
assert_eq!(expected_hash, actual_hash);
|
||||
|
||||
assert_eq!(
|
||||
Pbkdf2::SHA512.verify_password(EXAMPLE_PASSWORD, &actual_hash),
|
||||
Ok(())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Pbkdf2::SHA512.verify_password(b"bogus", &actual_hash),
|
||||
Err(Error::PasswordInvalid)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user