Skip to main content

doido_model/
password.rs

1//! Secure passwords and tokens (Rails `has_secure_password` / `has_secure_token`).
2//!
3//! Passwords are hashed with bcrypt; a model exposes its stored digest via
4//! [`HasSecurePassword`] to gain [`authenticate`](HasSecurePassword::authenticate).
5
6use bcrypt::{hash, verify, DEFAULT_COST};
7use doido_core::Result;
8
9/// Hash a password with bcrypt at the default cost.
10pub fn hash_password(password: &str) -> Result<String> {
11    hash_password_with_cost(password, DEFAULT_COST)
12}
13
14/// Hash a password with an explicit bcrypt cost (higher = slower/safer).
15pub fn hash_password_with_cost(password: &str, cost: u32) -> Result<String> {
16    hash(password, cost).map_err(|e| doido_core::anyhow::anyhow!("bcrypt hash failed: {e}"))
17}
18
19/// Verify a plaintext password against a bcrypt digest (false on any error).
20pub fn verify_password(password: &str, digest: &str) -> bool {
21    verify(password, digest).unwrap_or(false)
22}
23
24/// Generate an unguessable token (256 bits, hex) for `has_secure_token` and
25/// one-off tokens (password reset, etc.).
26pub fn generate_token() -> String {
27    format!(
28        "{}{}",
29        uuid::Uuid::new_v4().simple(),
30        uuid::Uuid::new_v4().simple()
31    )
32}
33
34/// Gives a model that stores a bcrypt digest an `authenticate` method.
35pub trait HasSecurePassword {
36    /// The stored bcrypt digest (e.g. the `password_digest` column).
37    fn password_digest(&self) -> &str;
38
39    /// Return `true` when `password` matches the stored digest.
40    fn authenticate(&self, password: &str) -> bool {
41        verify_password(password, self.password_digest())
42    }
43}