auth/hashing.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier, Params, password_hash::SaltString};
use rand_core::OsRng;
use crate::config::PasswordHashConfig;
/// Hashes a plain-text password using the Argon2id algorithm and the provided configuration.
///
/// A cryptographically secure random salt is generated automatically and included in the final hash.
/// The output hash is encoded in the [PHC string format](https://github.com/P-H-C/phc-string-format)
/// and can be safely stored in a database for later verification.
///
/// # Arguments
///
/// * `password` - The plain-text password to hash.
/// * `cfg` - A reference to a `PasswordHashConfig` struct containing Argon2 parameters.
///
/// # Returns
///
/// A `Result` containing the encoded password hash on success, or a `argon2::password_hash::Error` on failure.
///
/// # Example
///
/// ```rust
/// use auth::hashing::hash_password;
/// use auth::config::PasswordHashConfig;
///
/// let cfg = PasswordHashConfig {
/// mem_cost: 65536,
/// time_cost: 3,
/// lanes: 4,
/// hash_length: 32,
/// };
///
/// let password = "my_secure_password";
/// let hash = hash_password(password, &cfg).expect("Failed to hash password");
///
/// println!("Password hash: {}", hash);
/// assert!(hash.starts_with("$argon2id$"));
/// ```
pub fn hash_password(password: &str, cfg: &PasswordHashConfig) -> Result<String, argon2::password_hash::Error> {
let salt = SaltString::generate(&mut OsRng);
let params = Params::new(
cfg.mem_cost,
cfg.time_cost,
cfg.lanes,
Some(cfg.hash_length),
)?;
let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
let password_hash = argon2.hash_password(password.as_bytes(), &salt)?;
Ok(password_hash.to_string()) // includes salt in the output
}
/// Verifies that a plain-text password matches a previously generated Argon2 hash.
///
/// The hash must be in [PHC string format](https://github.com/P-H-C/phc-string-format),
/// which includes information about the algorithm, salt, and parameters used. This is the format
/// produced by [`hash_password`](fn.hash_password.html).
///
/// # Arguments
///
/// * `hash` - A PHC-encoded Argon2 hash string (e.g., from your database).
/// * `password` - The plain-text password to verify.
///
/// # Returns
///
/// A `Result` containing `true` if the password matches the hash, or `false` if it does not.
/// Returns an error if the hash cannot be parsed or if verification fails unexpectedly.
///
/// # Example
///
/// ```rust
/// use auth::hashing::{hash_password, verify_password};
/// use auth::config::PasswordHashConfig;
///
/// let cfg = PasswordHashConfig {
/// mem_cost: 65536,
/// time_cost: 3,
/// lanes: 4,
/// hash_length: 32,
/// };
///
/// let password = "my_secure_password";
/// let hash = hash_password(password, &cfg).expect("Hashing failed");
///
/// let is_valid = verify_password(&hash, password).expect("Verification failed");
/// assert!(is_valid);
///
/// let wrong = "wrong_password";
/// let is_valid = verify_password(&hash, wrong).expect("Verification failed");
/// assert!(!is_valid);
/// ```
pub fn verify_password(hash: &str, password: &str) -> Result<bool, argon2::password_hash::Error> {
let parsed_hash = PasswordHash::new(hash)?;
let argon2 = Argon2::default();
Ok(argon2.verify_password(password.as_bytes(), &parsed_hash).is_ok())
}
#[cfg(test)]
mod tests {
use super::*;
fn get_hash_config() -> PasswordHashConfig {
PasswordHashConfig {
mem_cost: 65536,
time_cost: 3,
lanes: 4,
hash_length: 32,
}
}
#[test]
fn hash_password_should_succeed() {
let password = "super_secure_password";
let result = hash_password(password, &get_hash_config());
assert!(result.is_ok(), "Password hashing should succeed");
let hash = result.unwrap();
assert!(
hash.starts_with("$argon2id$"),
"Hash should use argon2id and be correctly formatted"
);
}
#[test]
fn verify_password_should_succeed() {
let password = "super_secure_password";
let hash = hash_password(password, &get_hash_config()).expect("Password hashing did not succeed");
let is_valid = verify_password(&hash, password).expect("Password verification generated an unexpected error");
assert!(is_valid, "Password verification failed (but success was expected");
}
#[test]
fn verify_password_should_fail() {
let good_password = "super_secure_password";
let hash = hash_password(good_password, &get_hash_config()).expect("Password hashing did not succeed");
let bad_password = "wrong_password";
let is_valid = verify_password(&hash, bad_password).expect("Password verification generated an unexpected error");
assert!(!is_valid, "Password verification succeeded (but failure was expected");
}
}