noema-actix-webapi 0.1.0

Actix-web backend runtime on Noema (modules, sqlx, UoW, swagger, WebSocket dispatch)
use argon2::Argon2;
use argon2::password_hash::{
    PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng,
};
use noema::core::{Container, Injectable};

/// Injectable hash port. Default impl is Argon2 (PHC string, random salt).
///
/// Registered once in this crate (`Argon2Hasher`). The app cannot `dependency_as!` another
/// `Hasher` impl. A different algorithm is a separate app type.
pub trait Hasher: Send + Sync {
    fn hash(&self, data: &[u8]) -> String;
    fn verify(&self, data: &[u8], hashed: &str) -> bool;
}

/// Argon2id [`Hasher`]. `hash` emits a PHC string; the same input hashes differently each time (salt).
#[derive(Clone, Copy, Default)]
pub struct Argon2Hasher;

impl Hasher for Argon2Hasher {
    fn hash(&self, data: &[u8]) -> String {
        let salt = SaltString::generate(&mut OsRng);
        Argon2::default()
            .hash_password(data, &salt)
            .expect("argon2 hash")
            .to_string()
    }

    fn verify(&self, data: &[u8], hashed: &str) -> bool {
        let Ok(parsed) = PasswordHash::new(hashed) else {
            return false;
        };
        Argon2::default().verify_password(data, &parsed).is_ok()
    }
}

impl Injectable<Container> for Argon2Hasher {
    fn inject(_: &Container) -> Self {
        Self
    }
}

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

    #[test]
    fn hash_and_verify() {
        let hasher = Argon2Hasher;
        let hashed = hasher.hash(b"secret");
        assert_ne!(hashed, hasher.hash(b"secret"));
        assert!(hasher.verify(b"secret", &hashed));
        assert!(!hasher.verify(b"other", &hashed));
        assert!(!hasher.verify(b"secret", "not-a-phc"));
    }

    #[test]
    fn hasher_resolve_is_singleton() {
        let a = noema::resolve::<dyn Hasher + Send + Sync>();
        let b = noema::resolve::<dyn Hasher + Send + Sync>();
        assert!(std::sync::Arc::ptr_eq(&a, &b));
        assert!(a.verify(b"x", &a.hash(b"x")));
    }
}