Skip to main content

pylon_auth/
password.rs

1//! Argon2id password hashing + verification.
2//!
3//! Kept tiny on purpose — no in-memory store, no plugin glue. Password
4//! hashes live on the application's own entity (conventionally a
5//! `passwordHash` column on `User`), so persistence is the same story
6//! as every other row. Router endpoints under `/api/auth/password/*`
7//! call these helpers to mint the hash + verify at login.
8
9use argon2::{
10    password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
11    Argon2, PasswordHash, PasswordVerifier,
12};
13
14/// Hash a password using Argon2id with a random salt. Returns a
15/// PHC-format string carrying the algorithm, params, salt, and hash.
16pub fn hash_password(password: &str) -> String {
17    let salt = SaltString::generate(&mut OsRng);
18    Argon2::default()
19        .hash_password(password.as_bytes(), &salt)
20        .expect("argon2 hash should succeed")
21        .to_string()
22}
23
24/// Verify a password against an Argon2 PHC-format hash. Constant-time
25/// comparison is handled internally by Argon2's `verify_password`.
26pub fn verify_password(password: &str, hash: &str) -> bool {
27    let parsed = match PasswordHash::new(hash) {
28        Ok(h) => h,
29        Err(_) => return false,
30    };
31    Argon2::default()
32        .verify_password(password.as_bytes(), &parsed)
33        .is_ok()
34}
35
36/// A PHC-format hash of a throwaway string — used to equalize response
37/// timing when a login is attempted with an email that isn't registered.
38/// Without this, `known-email + wrong-password` takes ~50ms (Argon2) and
39/// `unknown-email` takes <1ms, letting an attacker enumerate the user
40/// set by response time alone.
41pub fn dummy_hash() -> &'static str {
42    "$argon2id$v=19$m=19456,t=2,p=1$YWFhYWFhYWFhYWFhYWFhYQ$b3W/3pZzm6S8w5qYvJ8y3A"
43}