Meliputi: - File-level //! doc comment: tujuan file, alur kerja, komponen utama - Function-level /// doc comment: apa, parameter, return, flow, edge cases - Struct/enum/trait /// doc comment: peran, field docs - Tracing logging (tracing::info!/debug!/trace!/warn!/error!) di setiap fungsi - Inline comments untuk variable dan branching logic penting - Seluruh 8 crates di workspace: zesdex-backend, zesdex-cms, zesdex-entities, zesdex-iam, zesdex-infra, zesdex-ipc, zesdex-middleware, zesdex-utils - Build: 0 errors, 242/242 tests passed
85 lines
2.8 KiB
Rust
85 lines
2.8 KiB
Rust
//! Argon2 password hashing and verification utilities.
|
|
//!
|
|
//! Uses the `argon2` crate (Argon2id variant) with default parameters,
|
|
//! which provide a good security / performance trade-off for interactive
|
|
//! authentication.
|
|
|
|
use anyhow::Result;
|
|
use argon2::{
|
|
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
|
|
Argon2,
|
|
};
|
|
use rand_core::OsRng;
|
|
use tracing;
|
|
|
|
/// Hash a plaintext password using Argon2id with a random salt.
|
|
///
|
|
/// The returned string is in the PHC string format
|
|
/// (`$argon2id$v=19$...`) and can be stored directly in the database.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns an error if the argon2 library fails (extremely rare —
|
|
/// typically indicates an OOM or system-level crypto failure).
|
|
pub fn hash_password(password: &str) -> Result<String> {
|
|
let salt = SaltString::generate(&mut OsRng); // cryptographic random salt
|
|
let argon2 = Argon2::default(); // Argon2id with default params
|
|
let hash = argon2
|
|
.hash_password(password.as_bytes(), &salt)
|
|
.map_err(|e| anyhow::anyhow!("failed to hash password: {e}"))?;
|
|
tracing::debug!("password hashed successfully");
|
|
Ok(hash.to_string())
|
|
}
|
|
|
|
/// Verify a plaintext password against a previously-hashed PHC string.
|
|
///
|
|
/// Returns `Ok(true)` if the password matches, `Ok(false)` if it does not,
|
|
/// and `Err` if the hash string is malformed.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns an error if the hash string is not a valid PHC string or if
|
|
/// the argon2 library encounters an internal failure.
|
|
pub fn verify_password(password: &str, hash: &str) -> Result<bool> {
|
|
let parsed_hash = PasswordHash::new(hash)
|
|
.map_err(|e| anyhow::anyhow!("failed to parse password hash: {e}"))?;
|
|
let argon2 = Argon2::default(); // Argon2id with default params
|
|
let valid = argon2
|
|
.verify_password(password.as_bytes(), &parsed_hash)
|
|
.is_ok();
|
|
tracing::debug!("password verification result: {valid}");
|
|
Ok(valid)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_hash_and_verify() {
|
|
let password = "my-secure-password-123!";
|
|
let hash = hash_password(password).unwrap();
|
|
assert!(verify_password(password, &hash).unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn test_wrong_password_fails() {
|
|
let hash = hash_password("correct-password").unwrap();
|
|
assert!(!verify_password("wrong-password", &hash).unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn test_hashes_are_different() {
|
|
let h1 = hash_password("same-password").unwrap();
|
|
let h2 = hash_password("same-password").unwrap();
|
|
// Different salts → different hashes.
|
|
assert_ne!(h1, h2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_invalid_hash_returns_error() {
|
|
let result = verify_password("password", "not-a-valid-hash");
|
|
assert!(result.is_err());
|
|
}
|
|
}
|