pbkdf2: heapless MCF hash verification support (#816)

Adds an `alloc` feature which is needed to enable MCF hashing
functionality.

The `PasswordVerifier<mcf::PasswordHashRef>` impl now works without any
dependency on liballoc.
This commit is contained in:
Tony Arcieri
2026-01-11 22:34:08 -07:00
committed by GitHub
parent bf6c9560d0
commit 7a930f8d62
6 changed files with 61 additions and 26 deletions

View File

@@ -55,6 +55,6 @@ jobs:
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
- run: cargo test --no-default-features
- run: cargo test
- run: cargo test --all-features
- uses: RustCrypto/actions/cargo-hack-install@master
- run: cargo hack test --feature-powerset
- run: cargo test --all-features --release

5
Cargo.lock generated
View File

@@ -282,8 +282,9 @@ checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f"
[[package]]
name = "mcf"
version = "0.6.0-rc.2"
source = "git+https://github.com/RustCrypto/formats?branch=base64ct%2Fpbkdf2-alphabet#a2b37fd6975e49ecdee08d640d6258f4b0f488c7"
version = "0.6.0-rc.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "423dc04b93e27ab6399fd28615305105c1621cebb78cbe24f64cb942d440733a"
dependencies = [
"base64ct",
]

View File

@@ -20,5 +20,3 @@ opt-level = 2
argon2 = { path = "./argon2" }
pbkdf2 = { path = "./pbkdf2" }
scrypt = { path = "./scrypt" }
mcf = { git = "https://github.com/RustCrypto/formats", branch = "base64ct/pbkdf2-alphabet" }

View File

@@ -17,8 +17,8 @@ rust-version = "1.85"
digest = { version = "0.11.0-rc.4", features = ["mac"] }
# optional dependencies
hmac = { version = "0.13.0-rc.3", default-features = false, optional = true }
mcf = { version = "0.6.0-rc.2", optional = true }
hmac = { version = "0.13.0-rc.3", optional = true, default-features = false }
mcf = { version = "0.6.0-rc.3", optional = true, default-features = false, features = ["base64"] }
password-hash = { version = "0.6.0-rc.8", 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 }
@@ -33,8 +33,9 @@ belt-hash = "0.2.0-rc.3"
[features]
default = ["hmac"]
alloc = ["mcf?/alloc", "password-hash?/alloc"]
getrandom = ["password-hash/getrandom"]
mcf = ["hmac", "password-hash/alloc", "dep:mcf", "sha2"]
mcf = ["hmac", "sha2", "dep:password-hash", "dep:mcf"]
phc = ["hmac", "password-hash/phc", "sha2"]
rand_core = ["password-hash/rand_core"]

View File

@@ -7,18 +7,24 @@
//! 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};
pub use mcf::PasswordHashRef;
#[cfg(feature = "alloc")]
pub use mcf::PasswordHash;
use crate::{Algorithm, Params, Pbkdf2, pbkdf2_hmac};
use mcf::Base64;
use password_hash::{
CustomizedPasswordHasher, Error, PasswordHasher, PasswordVerifier, Result, Version,
};
use password_hash::{Error, PasswordVerifier, Result};
use sha2::{Sha256, Sha512};
#[cfg(feature = "alloc")]
use password_hash::{CustomizedPasswordHasher, PasswordHasher, Version};
#[cfg(feature = "sha1")]
use sha1::Sha1;
const MAX_SALT_LEN: usize = 64;
#[cfg(feature = "alloc")]
impl CustomizedPasswordHasher<PasswordHash> for Pbkdf2 {
type Params = Params;
@@ -63,12 +69,14 @@ impl CustomizedPasswordHasher<PasswordHash> for Pbkdf2 {
}
}
#[cfg(feature = "alloc")]
impl PasswordHasher<PasswordHash> for Pbkdf2 {
fn hash_password_with_salt(&self, password: &[u8], salt: &[u8]) -> Result<PasswordHash> {
self.hash_password_customized(password, salt, None, None, self.params)
}
}
#[cfg(feature = "alloc")]
impl PasswordVerifier<PasswordHash> for Pbkdf2 {
fn verify_password(&self, password: &[u8], hash: &PasswordHash) -> Result<()> {
self.verify_password(password, hash.as_password_hash_ref())
@@ -89,15 +97,17 @@ impl PasswordVerifier<PasswordHashRef> for Pbkdf2 {
}
// decode salt
let mut salt_buf = [0u8; MAX_SALT_LEN];
let salt = next
.decode_base64(Base64::Pbkdf2)
.decode_base64_into(Base64::Pbkdf2, &mut salt_buf)
.map_err(|_| Error::EncodingInvalid)?;
// decode expected password hash
let mut expected_buf = [0u8; Params::MAX_OUTPUT_LENGTH];
let expected = fields
.next()
.ok_or(Error::EncodingInvalid)?
.decode_base64(Base64::Pbkdf2)
.decode_base64_into(Base64::Pbkdf2, &mut expected_buf)
.map_err(|_| Error::EncodingInvalid)?;
// should be the last field
@@ -105,8 +115,8 @@ impl PasswordVerifier<PasswordHashRef> for Pbkdf2 {
return Err(Error::EncodingInvalid);
}
let mut buffer = [0u8; Params::MAX_OUTPUT_LENGTH];
let out = buffer.get_mut(..expected.len()).ok_or(Error::OutputSize)?;
let mut out_buf = [0u8; Params::MAX_OUTPUT_LENGTH];
let out = out_buf.get_mut(..expected.len()).ok_or(Error::OutputSize)?;
let f = match algorithm {
#[cfg(feature = "sha1")]
@@ -134,14 +144,21 @@ impl PasswordVerifier<PasswordHashRef> for Pbkdf2 {
// TODO(tarcieri): tests for SHA-1
#[cfg(test)]
mod tests {
use super::Error;
use crate::{Params, Pbkdf2};
use mcf::{Base64, PasswordHash};
use password_hash::{CustomizedPasswordHasher, PasswordVerifier};
use crate::Pbkdf2;
use mcf::PasswordHashRef;
use password_hash::{Error, PasswordVerifier};
#[cfg(feature = "alloc")]
use {
crate::Params,
mcf::{Base64, PasswordHash},
password_hash::CustomizedPasswordHasher,
};
// Example adapted from:
// <https://passlib.readthedocs.io/en/stable/lib/passlib.hash.pbkdf2_digest.html>
#[test]
#[cfg(feature = "alloc")]
fn hash_password_sha256() {
const EXAMPLE_PASSWORD: &[u8] = b"password";
const EXAMPLE_ROUNDS: u32 = 8000;
@@ -173,6 +190,7 @@ mod tests {
// Example adapted from:
// <https://github.com/hlandau/passlib/blob/8f820e0/hash/pbkdf2/pbkdf2_test.go>
#[test]
#[cfg(feature = "alloc")]
fn hash_password_sha512() {
const EXAMPLE_PASSWORD: &[u8] = b"abcdefghijklmnop";
const EXAMPLE_ROUNDS: u32 = 25000;
@@ -199,4 +217,23 @@ mod tests {
Err(Error::PasswordInvalid)
);
}
#[test]
fn verify_password_sha256() {
const EXAMPLE_PASSWORD: &[u8] = b"password";
const EXAMPLE_HASH: &str =
"$pbkdf2-sha256$8000$XAuBMIYQQogxRg$tRRlz8hYn63B9LYiCd6PRo6FMiunY9ozmMMI3srxeRE";
let pwhash = PasswordHashRef::new(EXAMPLE_HASH).unwrap();
assert_eq!(
Pbkdf2::SHA256.verify_password(EXAMPLE_PASSWORD, pwhash),
Ok(())
);
assert_eq!(
Pbkdf2::SHA256.verify_password(b"bogus", pwhash),
Err(Error::PasswordInvalid)
);
}
}

View File

@@ -2,12 +2,10 @@ use core::{
fmt::{self, Display},
str::FromStr,
};
use password_hash::{Error, Result};
#[cfg(feature = "phc")]
use password_hash::{
Error, Result,
phc::{self, Decimal, ParamsString},
};
use password_hash::phc::{self, Decimal, ParamsString};
/// PBKDF2 params
#[derive(Copy, Clone, Debug, Eq, PartialEq)]