lmrc-http-common 0.3.16

Common HTTP utilities and patterns for LMRC Stack applications
Documentation
//! Password hashing and verification utilities

use bcrypt::{hash, verify, BcryptError, DEFAULT_COST};
use thiserror::Error;

/// Password hashing/verification errors
#[derive(Debug, Error)]
pub enum PasswordError {
    #[error("Failed to hash password: {0}")]
    HashError(String),

    #[error("Failed to verify password: {0}")]
    VerifyError(String),

    #[error("Invalid password")]
    InvalidPassword,
}

impl From<BcryptError> for PasswordError {
    fn from(err: BcryptError) -> Self {
        PasswordError::HashError(err.to_string())
    }
}

/// Hash a password using bcrypt
///
/// # Example
///
/// ```rust
/// use lmrc_http_common::auth::hash_password;
///
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let hashed = hash_password("my_secure_password")?;
/// assert_ne!(hashed, "my_secure_password");
/// # Ok(())
/// # }
/// ```
pub fn hash_password(password: &str) -> Result<String, PasswordError> {
    hash(password, DEFAULT_COST).map_err(Into::into)
}

/// Hash a password with a custom cost
///
/// Higher cost = more secure but slower. Default is 12.
/// Recommended range: 10-14
pub fn hash_password_with_cost(password: &str, cost: u32) -> Result<String, PasswordError> {
    hash(password, cost).map_err(Into::into)
}

/// Verify a password against a hash
///
/// # Example
///
/// ```rust
/// use lmrc_http_common::auth::{hash_password, verify_password};
///
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let hashed = hash_password("my_secure_password")?;
/// assert!(verify_password("my_secure_password", &hashed)?);
/// assert!(!verify_password("wrong_password", &hashed)?);
/// # Ok(())
/// # }
/// ```
pub fn verify_password(password: &str, hash: &str) -> Result<bool, PasswordError> {
    verify(password, hash).map_err(|e| PasswordError::VerifyError(e.to_string()))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_hash_and_verify() {
        let password = "test_password_123";
        let hashed = hash_password(password).unwrap();

        assert_ne!(password, hashed);
        assert!(verify_password(password, &hashed).unwrap());
        assert!(!verify_password("wrong_password", &hashed).unwrap());
    }

    #[test]
    fn test_custom_cost() {
        let password = "test_password";
        let hashed = hash_password_with_cost(password, 10).unwrap();

        assert!(verify_password(password, &hashed).unwrap());
    }
}