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}