use argon2::password_hash::{
PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng,
};
use argon2::{Algorithm, Argon2, Params, Version};
use crate::config::auth::Argon2Config;
pub(super) fn generate_scram_salt() -> Vec<u8> {
use argon2::password_hash::rand_core::RngCore;
let mut salt = vec![0u8; 16];
OsRng.fill_bytes(&mut salt);
salt
}
pub(super) fn compute_scram_salted_password(password: &str, salt: &[u8]) -> Vec<u8> {
pgwire::api::auth::sasl::scram::gen_salted_password(password, salt, 4096)
}
fn build_argon2(cfg: &Argon2Config) -> crate::Result<Argon2<'static>> {
let params = Params::new(
cfg.memory_kib,
cfg.time_cost,
cfg.parallelism,
Some(cfg.output_len),
)
.map_err(|e| crate::Error::Internal {
detail: format!("invalid argon2 params: {e}"),
})?;
Ok(Argon2::new(Algorithm::Argon2id, Version::V0x13, params))
}
pub(super) fn hash_password_argon2(password: &str, cfg: &Argon2Config) -> crate::Result<String> {
let salt = SaltString::generate(&mut OsRng);
let argon2 = build_argon2(cfg)?;
let hash = argon2
.hash_password(password.as_bytes(), &salt)
.map_err(|e| crate::Error::Internal {
detail: format!("argon2 hashing failed: {e}"),
})?;
Ok(hash.to_string())
}
pub(super) enum VerifyOutcome {
Ok { rehash: Option<String> },
WrongPassword,
BadStoredHash,
}
pub(super) fn verify_argon2_with_rehash(
stored_hash: &str,
password: &str,
cfg: &Argon2Config,
) -> VerifyOutcome {
let parsed = match PasswordHash::new(stored_hash) {
Ok(h) => h,
Err(_) => return VerifyOutcome::BadStoredHash,
};
let verify_ok = Argon2::default()
.verify_password(password.as_bytes(), &parsed)
.is_ok();
if !verify_ok {
return VerifyOutcome::WrongPassword;
}
let stored_params = extract_params(&parsed);
let needs_rehash = stored_params.is_some_and(|sp| params_are_weaker(&sp, cfg));
if needs_rehash {
match hash_password_argon2(password, cfg) {
Ok(new_hash) => VerifyOutcome::Ok {
rehash: Some(new_hash),
},
Err(e) => {
tracing::warn!(error = %e, "argon2 rehash failed; continuing without rehash");
VerifyOutcome::Ok { rehash: None }
}
}
} else {
VerifyOutcome::Ok { rehash: None }
}
}
struct StoredParams {
memory_kib: u32,
time_cost: u32,
parallelism: u32,
}
fn extract_params(hash: &PasswordHash<'_>) -> Option<StoredParams> {
let get = |name: &str| -> Option<u32> {
hash.params.get_decimal(name)
};
Some(StoredParams {
memory_kib: get("m")?,
time_cost: get("t")?,
parallelism: get("p")?,
})
}
fn params_are_weaker(stored: &StoredParams, cfg: &Argon2Config) -> bool {
stored.memory_kib < cfg.memory_kib
|| stored.time_cost < cfg.time_cost
|| stored.parallelism < cfg.parallelism
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::auth::Argon2Config;
fn weak_cfg() -> Argon2Config {
Argon2Config {
memory_kib: 8,
time_cost: 1,
parallelism: 1,
output_len: 32,
}
}
fn strong_cfg() -> Argon2Config {
Argon2Config {
memory_kib: 16,
time_cost: 2,
parallelism: 1,
output_len: 32,
}
}
#[test]
fn weak_hash_triggers_rehash_with_strong_params() {
let password = "correct_horse_battery";
let stored = hash_password_argon2(password, &weak_cfg()).expect("hash");
let outcome = verify_argon2_with_rehash(&stored, password, &strong_cfg());
let new_hash = match outcome {
VerifyOutcome::Ok { rehash: Some(h) } => h,
VerifyOutcome::Ok { rehash: None } => {
panic!("expected rehash but got Ok without rehash")
}
VerifyOutcome::WrongPassword => panic!("expected Ok, got WrongPassword"),
VerifyOutcome::BadStoredHash => panic!("expected Ok, got BadStoredHash"),
};
let new_parsed = PasswordHash::new(&new_hash).expect("parseable PHC");
let sp = extract_params(&new_parsed).expect("params present");
assert_eq!(sp.memory_kib, strong_cfg().memory_kib, "memory upgraded");
assert_eq!(sp.time_cost, strong_cfg().time_cost, "time_cost upgraded");
assert_eq!(sp.parallelism, strong_cfg().parallelism, "parallelism");
}
#[test]
fn strong_hash_no_rehash_with_same_params() {
let password = "correct_horse_battery";
let stored = hash_password_argon2(password, &strong_cfg()).expect("hash");
let stored_before = stored.clone();
let outcome = verify_argon2_with_rehash(&stored, password, &strong_cfg());
match outcome {
VerifyOutcome::Ok { rehash: None } => {}
VerifyOutcome::Ok { rehash: Some(_) } => {
panic!("unexpected rehash when params are equal")
}
VerifyOutcome::WrongPassword => panic!("expected Ok, got WrongPassword"),
VerifyOutcome::BadStoredHash => panic!("expected Ok, got BadStoredHash"),
}
assert_eq!(stored, stored_before);
}
#[test]
fn wrong_password_no_rehash() {
let stored = hash_password_argon2("correct", &weak_cfg()).expect("hash");
let outcome = verify_argon2_with_rehash(&stored, "wrong", &strong_cfg());
assert!(
matches!(outcome, VerifyOutcome::WrongPassword),
"expected WrongPassword"
);
}
#[test]
fn garbage_phc_returns_bad_stored_hash() {
let outcome =
verify_argon2_with_rehash("$notavalidphcstring$$$garbage", "password", &strong_cfg());
assert!(
matches!(outcome, VerifyOutcome::BadStoredHash),
"expected BadStoredHash"
);
}
#[test]
fn strong_hash_no_downgrade_when_config_is_weaker() {
let password = "no_downgrade_test";
let stored = hash_password_argon2(password, &strong_cfg()).expect("hash");
let outcome = verify_argon2_with_rehash(&stored, password, &weak_cfg());
match outcome {
VerifyOutcome::Ok { rehash: None } => {}
VerifyOutcome::Ok { rehash: Some(_) } => {
panic!("no-downgrade violated: rehash triggered with weaker config")
}
VerifyOutcome::WrongPassword => panic!("expected Ok"),
VerifyOutcome::BadStoredHash => panic!("expected Ok, got BadStoredHash"),
}
}
}