Skip to content

Commit a5b3e25

Browse files
authored
pbkdf2: impl PasswordVerifier<mcf::PasswordHash> (#809)
Adds support for verifying password hashes in MCF format.
1 parent 7c232a7 commit a5b3e25

1 file changed

Lines changed: 91 additions & 11 deletions

File tree

pbkdf2/src/mcf.rs

Lines changed: 91 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
//! Implementation of the `password-hash` traits for Modular Crypt Format (MCF) password hash
2-
//! strings which begin with `$7$`:
2+
//! strings which begin with `$pbkdf$`, `$pbkdf-sha256$`, or `$pbkdf-sha512`:
33
//!
4-
//! <https://man.archlinux.org/man/crypt.5#scrypt>
4+
//! <https://passlib.readthedocs.io/en/stable/lib/passlib.hash.pbkdf2_digest.html>
5+
//!
6+
//! PBKDF2's MCF strings can be distinguished from PHC strings by whether the parameters
7+
//! field contains `rounds=` or not: if the number of rounds does NOT contain `rounds=`, but just a
8+
//! bare number of rounds, then it's MCF format. If it DOES contain `rounds=`, then it's PHC.
59
610
pub use mcf::{PasswordHash, PasswordHashRef};
711

812
use crate::{Algorithm, Params, Pbkdf2, pbkdf2_hmac};
9-
use alloc::string::String;
13+
use alloc::{string::String, vec::Vec};
1014
use mcf::Base64;
11-
use password_hash::{CustomizedPasswordHasher, Error, PasswordHasher, Result, Version};
15+
use password_hash::{
16+
CustomizedPasswordHasher, Error, PasswordHasher, PasswordVerifier, Result, Version,
17+
};
1218
use sha2::{Sha256, Sha512};
1319

1420
#[cfg(feature = "sha1")]
1521
use sha1::Sha1;
1622

17-
#[cfg(test)]
18-
use alloc::vec::Vec;
19-
2023
impl CustomizedPasswordHasher<PasswordHash> for Pbkdf2 {
2124
type Params = Params;
2225

@@ -75,10 +78,67 @@ impl PasswordHasher<PasswordHash> for Pbkdf2 {
7578
}
7679
}
7780

81+
impl PasswordVerifier<PasswordHash> for Pbkdf2 {
82+
fn verify_password(&self, password: &[u8], hash: &PasswordHash) -> Result<()> {
83+
self.verify_password(password, hash.as_password_hash_ref())
84+
}
85+
}
86+
87+
impl PasswordVerifier<PasswordHashRef> for Pbkdf2 {
88+
fn verify_password(&self, password: &[u8], hash: &PasswordHashRef) -> Result<()> {
89+
let algorithm = hash.id().parse::<Algorithm>()?;
90+
let mut fields = hash.fields();
91+
let mut next = fields.next().ok_or(Error::EncodingInvalid)?;
92+
let mut params = Params::default();
93+
94+
// decode params
95+
if let Ok(p) = next.as_str().parse::<Params>() {
96+
params = p;
97+
next = fields.next().ok_or(Error::EncodingInvalid)?;
98+
}
99+
100+
let salt = base64_decode(next.as_str())?;
101+
102+
// decode expected password hash
103+
let expected = fields
104+
.next()
105+
.ok_or(Error::EncodingInvalid)
106+
.and_then(|field| base64_decode(field.as_str()))?;
107+
108+
// should be the last field
109+
if fields.next().is_some() {
110+
return Err(Error::EncodingInvalid);
111+
}
112+
113+
let mut buffer = [0u8; Params::MAX_LENGTH];
114+
let out = buffer.get_mut(..expected.len()).ok_or(Error::OutputSize)?;
115+
116+
let f = match algorithm {
117+
#[cfg(feature = "sha1")]
118+
Algorithm::Pbkdf2Sha1 => pbkdf2_hmac::<Sha1>,
119+
Algorithm::Pbkdf2Sha256 => pbkdf2_hmac::<Sha256>,
120+
Algorithm::Pbkdf2Sha512 => pbkdf2_hmac::<Sha512>,
121+
};
122+
123+
f(password, &salt, params.rounds(), out);
124+
125+
// TODO(tarcieri): use `subtle` or `ctutils` for comparison
126+
if out
127+
.iter()
128+
.zip(expected.iter())
129+
.fold(0, |acc, (a, b)| acc | (a ^ b))
130+
== 0
131+
{
132+
Ok(())
133+
} else {
134+
Err(Error::PasswordInvalid)
135+
}
136+
}
137+
}
138+
78139
// Base64 support: PBKDF2 uses a variant of standard unpadded Base64 which substitutes the `+`
79140
// character for `.` and this is a distinct encoding from the bcrypt and crypt Base64 variants.
80141

81-
#[cfg(test)]
82142
fn base64_decode(base64: &str) -> Result<Vec<u8>> {
83143
Base64::B64
84144
.decode_vec(&base64.replace('.', "+"))
@@ -92,10 +152,10 @@ fn base64_encode(bytes: &[u8]) -> String {
92152
// TODO(tarcieri): tests for SHA-1 and SHA-512
93153
#[cfg(test)]
94154
mod tests {
95-
use super::base64_decode;
155+
use super::{Error, base64_decode};
96156
use crate::{Params, Pbkdf2};
97157
use mcf::PasswordHash;
98-
use password_hash::CustomizedPasswordHasher;
158+
use password_hash::{CustomizedPasswordHasher, PasswordVerifier};
99159

100160
// Example adapted from:
101161
// <https://passlib.readthedocs.io/en/stable/lib/passlib.hash.pbkdf2_digest.html>
@@ -110,12 +170,22 @@ mod tests {
110170
let salt = base64_decode(EXAMPLE_SALT).unwrap();
111171
let params = Params::new(EXAMPLE_ROUNDS);
112172

113-
let actual_hash: PasswordHash = Pbkdf2::default()
173+
let actual_hash: PasswordHash = Pbkdf2::SHA256
114174
.hash_password_with_params(EXAMPLE_PASSWORD, salt.as_slice(), params)
115175
.unwrap();
116176

117177
let expected_hash = PasswordHash::new(EXAMPLE_HASH).unwrap();
118178
assert_eq!(expected_hash, actual_hash);
179+
180+
assert_eq!(
181+
Pbkdf2::SHA256.verify_password(EXAMPLE_PASSWORD, &actual_hash),
182+
Ok(())
183+
);
184+
185+
assert_eq!(
186+
Pbkdf2::SHA256.verify_password(b"bogus", &actual_hash),
187+
Err(Error::PasswordInvalid)
188+
);
119189
}
120190

121191
// Example adapted from:
@@ -136,5 +206,15 @@ mod tests {
136206

137207
let expected_hash = PasswordHash::new(EXAMPLE_HASH).unwrap();
138208
assert_eq!(expected_hash, actual_hash);
209+
210+
assert_eq!(
211+
Pbkdf2::SHA512.verify_password(EXAMPLE_PASSWORD, &actual_hash),
212+
Ok(())
213+
);
214+
215+
assert_eq!(
216+
Pbkdf2::SHA512.verify_password(b"bogus", &actual_hash),
217+
Err(Error::PasswordInvalid)
218+
);
139219
}
140220
}

0 commit comments

Comments
 (0)