#![allow(missing_docs)]
#![allow(clippy::unwrap_used, clippy::expect_used)]
use hsh::algorithms::bcrypt::BcryptParams;
use hsh::algorithms::pbkdf2::{Pbkdf2Params, Prf};
use hsh::algorithms::scrypt::ScryptParams;
use hsh::policy::{Policy, PolicyBuilder, PrimaryAlgorithm};
use hsh::{api, Outcome};
use proptest::prelude::*;
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(BcryptParams::new(4))
.scrypt(ScryptParams {
log_n: 8,
r: 8,
p: 1,
dk_len: 32,
})
.pbkdf2(Pbkdf2Params {
prf: Prf::Sha256,
iterations: 1,
dk_len: 32,
})
.build()
.expect("fast test policy")
}
fn password_strategy() -> impl Strategy<Value = String> {
"[ -~]{8,72}".prop_filter("non-empty", |s| !s.is_empty())
}
proptest! {
#![proptest_config(ProptestConfig {
// Keep wall-time bounded — Argon2id/bcrypt/scrypt are deliberately slow.
// Even at fast_test_policy() params, scrypt N=2^8 is hundreds of ms.
cases: 6,
.. ProptestConfig::default()
})]
#[test]
fn argon2id_round_trip_holds(pwd in password_strategy()) {
let p = fast_test_policy(PrimaryAlgorithm::Argon2id);
let stored = api::hash(&p, &pwd).unwrap();
let outcome = api::verify_and_upgrade(&p, &pwd, &stored).unwrap();
let is_valid = matches!(outcome, Outcome::Valid { .. });
prop_assert!(is_valid);
}
#[test]
fn argon2id_rejects_distinct_passwords(
a in password_strategy(),
b in password_strategy(),
) {
prop_assume!(a != b);
let p = fast_test_policy(PrimaryAlgorithm::Argon2id);
let stored = api::hash(&p, &a).unwrap();
let outcome = api::verify_and_upgrade(&p, &b, &stored).unwrap();
let is_invalid = matches!(outcome, Outcome::Invalid);
prop_assert!(is_invalid);
}
#[test]
fn bcrypt_round_trip_holds(pwd in password_strategy()) {
let p = fast_test_policy(PrimaryAlgorithm::Bcrypt);
let stored = api::hash(&p, &pwd).unwrap();
let outcome = api::verify_and_upgrade(&p, &pwd, &stored).unwrap();
let is_valid = matches!(outcome, Outcome::Valid { .. });
prop_assert!(is_valid);
}
#[test]
fn scrypt_round_trip_holds(pwd in password_strategy()) {
let p = fast_test_policy(PrimaryAlgorithm::Scrypt);
let stored = api::hash(&p, &pwd).unwrap();
let outcome = api::verify_and_upgrade(&p, &pwd, &stored).unwrap();
let is_valid = matches!(outcome, Outcome::Valid { .. });
prop_assert!(is_valid);
}
#[test]
fn salts_make_each_hash_distinct(pwd in password_strategy()) {
let p = fast_test_policy(PrimaryAlgorithm::Argon2id);
let a = api::hash(&p, &pwd).unwrap();
let b = api::hash(&p, &pwd).unwrap();
prop_assert_ne!(a, b);
}
#[test]
fn bcrypt_rejects_oversize_input(extra in 1usize..32usize) {
let pwd = "x".repeat(72 + extra);
let p = fast_test_policy(PrimaryAlgorithm::Bcrypt);
let err = api::hash(&p, &pwd).unwrap_err();
let is_invalid_password = matches!(err, hsh::Error::InvalidPassword(_));
prop_assert!(is_invalid_password);
}
#[test]
fn short_passwords_are_rejected(len in 0usize..8usize) {
let pwd = "x".repeat(len);
let r = hsh::models::hash::Hash::new(&pwd, "abcdefghijklmnop", "argon2id");
prop_assert!(r.is_err());
}
}