Skip to main content

rws_server/crypto/
mod.rs

1//! Password hashing and secure token generation.
2//!
3//! Enabled by the `crypto` Cargo feature:
4//!
5//! ```toml
6//! rust-web-server = { version = "17", features = ["crypto"] }
7//! ```
8//!
9//! # Password hashing
10//!
11//! Uses Argon2id (OWASP recommended) with a random 128-bit salt. The output
12//! is a self-describing PHC string that includes the algorithm, parameters,
13//! salt, and hash — store it as-is in your database.
14//!
15//! ```rust
16//! # extern crate rws_server as rust_web_server;
17//! use rust_web_server::crypto::{hash_password, verify_password};
18//!
19//! let hash = hash_password("hunter2").unwrap();
20//! assert!(verify_password("hunter2", &hash).unwrap());
21//! assert!(!verify_password("wrong", &hash).unwrap());
22//! ```
23//!
24//! # Secure token generation
25//!
26//! ```rust
27//! # extern crate rws_server as rust_web_server;
28//! use rust_web_server::crypto::generate_token;
29//!
30//! let token = generate_token(32); // 32 random bytes → 64-char lowercase hex string
31//! ```
32
33use argon2::{
34    password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
35    Argon2,
36};
37use rand_core::{OsRng, RngCore};
38
39/// Error returned by crypto operations.
40#[derive(Debug)]
41pub struct CryptoError(pub String);
42
43impl std::fmt::Display for CryptoError {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        write!(f, "CryptoError: {}", self.0)
46    }
47}
48
49impl std::error::Error for CryptoError {}
50
51/// Hash `password` using Argon2id with a random salt.
52///
53/// Returns a PHC string (`$argon2id$v=19$...`) suitable for storing in a
54/// database. The salt is embedded in the string — no separate storage needed.
55///
56/// # Errors
57///
58/// Returns `CryptoError` if the hashing operation fails (extremely unlikely
59/// with valid inputs).
60pub fn hash_password(password: &str) -> Result<String, CryptoError> {
61    let salt = SaltString::generate(&mut OsRng);
62    Argon2::default()
63        .hash_password(password.as_bytes(), &salt)
64        .map(|h| h.to_string())
65        .map_err(|e| CryptoError(e.to_string()))
66}
67
68/// Verify `password` against a PHC hash string produced by [`hash_password`].
69///
70/// Returns `Ok(true)` if the password matches, `Ok(false)` if it does not.
71/// Comparison is constant-time to prevent timing attacks.
72///
73/// # Errors
74///
75/// Returns `CryptoError` if `hash` is not a valid PHC string.
76pub fn verify_password(password: &str, hash: &str) -> Result<bool, CryptoError> {
77    let parsed = PasswordHash::new(hash)
78        .map_err(|e| CryptoError(format!("invalid hash string: {}", e)))?;
79    Ok(Argon2::default()
80        .verify_password(password.as_bytes(), &parsed)
81        .is_ok())
82}
83
84/// Generate `n_bytes` of cryptographically secure random bytes and return them
85/// as a lowercase hex string of length `n_bytes * 2`.
86///
87/// Suitable for password-reset tokens, email verification codes, and API keys.
88/// Use at least 16 bytes (32 hex chars) for tokens; 32 bytes (64 hex chars) for
89/// high-value secrets like API keys.
90pub fn generate_token(n_bytes: usize) -> String {
91    let mut bytes = vec![0u8; n_bytes];
92    OsRng.fill_bytes(&mut bytes);
93    bytes.iter().map(|b| format!("{:02x}", b)).collect()
94}
95
96#[cfg(test)]
97mod tests;