#![allow(missing_docs)]
#![allow(clippy::unwrap_used, clippy::expect_used)]
use hsh::algorithms::pbkdf2::{Pbkdf2Params, Prf};
use hsh::policy::{Policy, PolicyBuilder, PrimaryAlgorithm};
use hsh::{api, Error, Outcome};
fn fast_test_policy(primary: PrimaryAlgorithm) -> Policy {
PolicyBuilder::from_preset(&Policy::owasp_minimum_2025())
.primary(primary)
.argon2(argon2::Params::new(8, 1, 1, Some(32)).unwrap())
.bcrypt(hsh::algorithms::bcrypt::BcryptParams::new(4))
.scrypt(hsh::algorithms::scrypt::ScryptParams {
log_n: 8,
r: 8,
p: 1,
dk_len: 32,
})
.pbkdf2(Pbkdf2Params {
prf: Prf::Sha256,
iterations: 1,
dk_len: 32,
})
.build()
.unwrap()
}
#[test]
fn bcrypt_rejects_non_utf8_password_bytes() {
let policy = fast_test_policy(PrimaryAlgorithm::Bcrypt);
let bad: &[u8] = &[0xff, 0xfe, 0x80, 0x81];
let err = api::hash(&policy, bad).unwrap_err();
assert!(matches!(err, Error::InvalidPassword(_)));
}
#[test]
fn bcrypt_verify_rejects_non_utf8_password_bytes() {
let policy = fast_test_policy(PrimaryAlgorithm::Bcrypt);
let stored = api::hash(&policy, "real").unwrap();
let bad: &[u8] = &[0xff, 0xfe];
let err =
api::verify_and_upgrade(&policy, bad, &stored).unwrap_err();
assert!(matches!(err, Error::InvalidPassword(_)));
}
#[test]
fn verify_rejects_not_a_phc_string() {
let policy = fast_test_policy(PrimaryAlgorithm::Argon2id);
let err =
api::verify_and_upgrade(&policy, "pw", "garbage").unwrap_err();
assert!(matches!(err, Error::InvalidHashString(_)));
}
#[test]
fn verify_rejects_unknown_algorithm_in_phc() {
let policy = fast_test_policy(PrimaryAlgorithm::Argon2id);
let bogus = "$crypt$YWFhYWFhYWFhYWFhYWFhYQ$dGVzdA";
let err =
api::verify_and_upgrade(&policy, "pw", bogus).unwrap_err();
assert!(matches!(
err,
Error::UnsupportedAlgorithm(_) | Error::InvalidHashString(_)
));
}
#[test]
fn verify_accepts_argon2i_phc_string() {
use argon2::password_hash::{PasswordHasher, SaltString};
use argon2::{Algorithm, Argon2, Version};
use rand_core::OsRng;
let salt = SaltString::generate(&mut OsRng);
let params = argon2::Params::new(8, 1, 1, Some(32)).unwrap();
let engine =
Argon2::new(Algorithm::Argon2i, Version::V0x13, params);
let phc = engine.hash_password(b"pw", &salt).unwrap().to_string();
assert!(phc.starts_with("$argon2i$"));
let policy = fast_test_policy(PrimaryAlgorithm::Argon2id);
let outcome = api::verify_and_upgrade(&policy, "pw", &phc).unwrap();
assert!(outcome.is_valid());
assert!(outcome.needs_rehash());
}
#[test]
fn verify_accepts_argon2d_phc_string() {
use argon2::password_hash::{PasswordHasher, SaltString};
use argon2::{Algorithm, Argon2, Version};
use rand_core::OsRng;
let salt = SaltString::generate(&mut OsRng);
let params = argon2::Params::new(8, 1, 1, Some(32)).unwrap();
let engine =
Argon2::new(Algorithm::Argon2d, Version::V0x13, params);
let phc = engine.hash_password(b"pw", &salt).unwrap().to_string();
assert!(phc.starts_with("$argon2d$"));
let policy = fast_test_policy(PrimaryAlgorithm::Argon2id);
let outcome = api::verify_and_upgrade(&policy, "pw", &phc).unwrap();
assert!(outcome.is_valid());
assert!(outcome.needs_rehash());
}
#[test]
fn verify_rejects_argon2i_with_wrong_password() {
use argon2::password_hash::{PasswordHasher, SaltString};
use argon2::{Algorithm, Argon2, Version};
use rand_core::OsRng;
let salt = SaltString::generate(&mut OsRng);
let params = argon2::Params::new(8, 1, 1, Some(32)).unwrap();
let phc = Argon2::new(Algorithm::Argon2i, Version::V0x13, params)
.hash_password(b"real", &salt)
.unwrap()
.to_string();
let policy = fast_test_policy(PrimaryAlgorithm::Argon2id);
let outcome =
api::verify_and_upgrade(&policy, "wrong", &phc).unwrap();
assert!(matches!(outcome, Outcome::Invalid));
}
#[test]
fn verify_rejects_pbkdf2_phc_missing_iteration_count() {
let policy = fast_test_policy(PrimaryAlgorithm::Pbkdf2);
let phc = "$pbkdf2-sha256$l=32$YWFhYWFhYWFhYWFhYWFhYQ$dGVzdA";
let err = api::verify_and_upgrade(&policy, "pw", phc).unwrap_err();
assert!(matches!(err, Error::InvalidHashString(_)));
}
#[test]
fn verify_rejects_pbkdf2_phc_bad_iteration_count() {
let policy = fast_test_policy(PrimaryAlgorithm::Pbkdf2);
let phc =
"$pbkdf2-sha256$i=not-a-number$YWFhYWFhYWFhYWFhYWFhYQ$dGVzdA";
let err = api::verify_and_upgrade(&policy, "pw", phc).unwrap_err();
assert!(matches!(err, Error::InvalidHashString(_)));
}
#[test]
fn pbkdf2_sha512_phc_round_trip() {
let policy = PolicyBuilder::from_preset(&fast_test_policy(
PrimaryAlgorithm::Pbkdf2,
))
.pbkdf2(Pbkdf2Params {
prf: Prf::Sha512,
iterations: 1,
dk_len: 64,
})
.build()
.unwrap();
let stored = api::hash(&policy, "pw").unwrap();
assert!(stored.starts_with("$pbkdf2-sha512$"));
let outcome =
api::verify_and_upgrade(&policy, "pw", &stored).unwrap();
assert!(outcome.is_valid());
}
#[test]
fn pbkdf2_phc_with_explicit_l_parameter() {
let policy = fast_test_policy(PrimaryAlgorithm::Pbkdf2);
let stored = api::hash(&policy, "pw").unwrap();
assert!(stored.contains("i="));
assert!(stored.contains("l="));
let outcome =
api::verify_and_upgrade(&policy, "pw", &stored).unwrap();
assert!(outcome.is_valid());
}
#[test]
fn verify_rejects_pbkdf2_phc_with_bad_dk_len() {
let policy = fast_test_policy(PrimaryAlgorithm::Pbkdf2);
let phc = "$pbkdf2-sha256$i=1,l=not-a-number$YWFhYWFhYWFhYWFhYWFhYQ$dGVzdA";
let err = api::verify_and_upgrade(&policy, "pw", phc).unwrap_err();
assert!(matches!(err, Error::InvalidHashString(_)));
}
#[test]
fn verify_pbkdf2_phc_ignores_unknown_parameter() {
let policy = fast_test_policy(PrimaryAlgorithm::Pbkdf2);
let stored = api::hash(&policy, "pw").unwrap();
let outcome =
api::verify_and_upgrade(&policy, "pw", &stored).unwrap();
assert!(outcome.is_valid());
}
#[test]
fn verify_unsupported_phc_algorithm() {
let policy = fast_test_policy(PrimaryAlgorithm::Argon2id);
for bogus in [
"$blake2b$x$YWFhYWFhYWFhYWFhYWFhYQ$dGVzdA",
"$sha256$x$YWFhYWFhYWFhYWFhYWFhYQ$dGVzdA",
] {
let err =
api::verify_and_upgrade(&policy, "pw", bogus).unwrap_err();
assert!(matches!(
err,
Error::UnsupportedAlgorithm(_)
| Error::InvalidHashString(_)
));
}
}
#[test]
fn verify_dispatches_other_arm_on_unknown_algorithm() {
let policy = fast_test_policy(PrimaryAlgorithm::Argon2id);
let bogus = "$argon2u$v=19$m=8,t=1,p=1$YWFhYWFhYWFhYWFhYWFhYQ$dGVzdGRlc3RkZXN0ZGVzdGRlc3RkZXN0";
let err =
api::verify_and_upgrade(&policy, "pw", bogus).unwrap_err();
assert!(matches!(
err,
Error::UnsupportedAlgorithm(_) | Error::InvalidHashString(_)
));
}
#[test]
fn verify_pbkdf2_phc_with_unknown_parameter_key() {
let policy = fast_test_policy(PrimaryAlgorithm::Pbkdf2);
let stored = api::hash(&policy, "pw").unwrap();
let corrupted = stored.replace("$i=", "$foo=bar,i=");
if corrupted != stored {
let _ = api::verify_and_upgrade(&policy, "pw", &corrupted);
}
}
#[test]
fn bcrypt_mismatch_under_bcrypt_policy_returns_invalid() {
let policy = fast_test_policy(PrimaryAlgorithm::Bcrypt);
let stored = api::hash(&policy, "right").unwrap();
let outcome =
api::verify_and_upgrade(&policy, "wrong", &stored).unwrap();
assert!(matches!(outcome, Outcome::Invalid));
}
#[test]
fn verify_pbkdf2_phc_with_corrupted_iteration_count() {
let policy = fast_test_policy(PrimaryAlgorithm::Pbkdf2);
let stored = api::hash(&policy, "pw").unwrap();
let corrupted = regex_replace_first(&stored, "i=1,", "i=zzz,")
.unwrap_or(stored.clone());
if corrupted != stored {
let err = api::verify_and_upgrade(&policy, "pw", &corrupted)
.unwrap_err();
assert!(matches!(err, Error::InvalidHashString(_)));
}
}
#[test]
fn verify_pbkdf2_phc_with_corrupted_dk_len() {
let policy = fast_test_policy(PrimaryAlgorithm::Pbkdf2);
let stored = api::hash(&policy, "pw").unwrap();
let corrupted = regex_replace_first(&stored, "l=32", "l=abc")
.unwrap_or(stored.clone());
if corrupted != stored {
let err = api::verify_and_upgrade(&policy, "pw", &corrupted)
.unwrap_err();
assert!(matches!(err, Error::InvalidHashString(_)));
}
}
fn regex_replace_first(
s: &str,
needle: &str,
replacement: &str,
) -> Option<String> {
s.find(needle).map(|i| {
let mut out =
String::with_capacity(s.len() + replacement.len());
out.push_str(&s[..i]);
out.push_str(replacement);
out.push_str(&s[i + needle.len()..]);
out
})
}
#[cfg(feature = "pepper")]
mod pepper {
use super::*;
use hsh_kms::{KeyVersion, LocalPepper};
use std::sync::Arc;
fn peppered_policy() -> Policy {
let pepper: Arc<dyn hsh_kms::Pepper> = Arc::new(
LocalPepper::builder()
.add(
KeyVersion::new(1),
b"pepper-key-bytes-16+++++".to_vec(),
)
.current(KeyVersion::new(1))
.build()
.unwrap(),
);
PolicyBuilder::from_preset(&fast_test_policy(
PrimaryAlgorithm::Argon2id,
))
.pepper_arc(pepper)
.build()
.unwrap()
}
#[test]
fn pepper_prefix_without_colon_separator() {
let policy = peppered_policy();
let bogus = "hsh-pepper:nope";
let err =
api::verify_and_upgrade(&policy, "pw", bogus).unwrap_err();
assert!(matches!(err, Error::InvalidHashString(_)));
}
#[test]
fn pepper_prefix_with_non_integer_version() {
let policy = peppered_policy();
let bogus = "hsh-pepper:abc:$argon2id$dummy";
let err =
api::verify_and_upgrade(&policy, "pw", bogus).unwrap_err();
assert!(matches!(err, Error::InvalidHashString(_)));
}
}