Skip to main content

rusmes_auth/
lib.rs

1//! # rusmes-auth
2//!
3//! Pluggable authentication backends for the RusMES mail server.
4//!
5//! ## Overview
6//!
7//! `rusmes-auth` provides a unified [`AuthBackend`] trait that abstracts over multiple
8//! authentication strategies. All backends implement the same async interface, allowing
9//! them to be composed, swapped, or wrapped by middleware such as the brute-force
10//! protector found in the [`security`] module.
11//!
12//! ## Backends
13//!
14//! | Backend | Module | Notes |
15//! |---------|--------|-------|
16//! | File (htpasswd-style) | [`mod@file`] | bcrypt hashes, atomic writes |
17//! | LDAP / Active Directory | [`backends::ldap`] | connection pooling, group filtering |
18//! | SQL (SQLite / Postgres / MySQL) | [`backends::sql`] | bcrypt + Argon2 + SCRAM-SHA-256 |
19//! | OAuth2 / OIDC | [`backends::oauth2`] | JWT validation, XOAUTH2 SASL |
20//! | PAM | [`backends::pam`] | **feature-gated** (`pam-auth`), C bindings |
21//!
22//! ## SASL Mechanisms
23//!
24//! The [`sasl`] module implements RFC-compliant SASL mechanisms on top of any `AuthBackend`:
25//!
26//! - `PLAIN` (RFC 4616)
27//! - `LOGIN` (obsolete but widely supported)
28//! - `CRAM-MD5` (RFC 2195)
29//! - `SCRAM-SHA-256` (RFC 5802 / RFC 7677)
30//! - `XOAUTH2` (RFC 7628)
31//!
32//! ## Security
33//!
34//! The [`security`] module provides:
35//!
36//! - Brute-force / account-lockout protection (progressive lockout)
37//! - Per-IP rate limiting
38//! - Password strength validation (entropy, character class, banned list)
39//! - In-memory audit logging
40//!
41//! ## Feature Flags
42//!
43//! | Feature | Default | Description |
44//! |---------|---------|-------------|
45//! | `pam-auth` | **no** | Enables PAM backend (requires C `libpam`). Disabled by default to satisfy the Pure Rust policy and avoid RUSTSEC-2023-0040/0059. |
46//!
47//! ## Example
48//!
49//! ```rust,no_run
50//! use rusmes_auth::file::FileAuthBackend;
51//! use rusmes_auth::AuthBackend;
52//! use rusmes_proto::Username;
53//!
54//! # async fn example() -> anyhow::Result<()> {
55//! let backend = FileAuthBackend::new("/etc/rusmes/passwd").await?;
56//! let user = Username::new("alice".to_string())?;
57//! let ok = backend.authenticate(&user, "s3cr3t").await?;
58//! println!("authenticated: {ok}");
59//! # Ok(())
60//! # }
61//! ```
62
63use async_trait::async_trait;
64use rusmes_proto::Username;
65
66pub mod backends;
67pub mod file;
68pub mod sasl;
69pub mod security;
70
71#[async_trait]
72pub trait AuthBackend: Send + Sync {
73    /// Authenticate a user with username and password
74    async fn authenticate(&self, username: &Username, password: &str) -> anyhow::Result<bool>;
75
76    /// Verify if a username maps to a valid identity
77    async fn verify_identity(&self, username: &Username) -> anyhow::Result<bool>;
78
79    /// List all users (for admin CLI)
80    async fn list_users(&self) -> anyhow::Result<Vec<Username>>;
81
82    /// Create a new user with the given password
83    async fn create_user(&self, username: &Username, password: &str) -> anyhow::Result<()>;
84
85    /// Delete a user
86    async fn delete_user(&self, username: &Username) -> anyhow::Result<()>;
87
88    /// Change a user's password
89    async fn change_password(&self, username: &Username, new_password: &str) -> anyhow::Result<()>;
90
91    // ========================================================================
92    // SCRAM-SHA-256 Support (Optional)
93    // ========================================================================
94    // IMPORTANT: SCRAM-SHA-256 requires different credential storage than
95    // bcrypt. The following methods provide SCRAM-specific credential access.
96    // Default implementations return errors to maintain backward compatibility.
97
98    /// Get SCRAM-SHA-256 parameters (salt, iteration count) for a user
99    ///
100    /// Returns (salt, iterations) if SCRAM credentials are stored.
101    /// Default implementation returns an error indicating SCRAM is not supported.
102    async fn get_scram_params(&self, _username: &str) -> anyhow::Result<(Vec<u8>, u32)> {
103        Err(anyhow::anyhow!(
104            "SCRAM-SHA-256 credential storage not implemented in this AuthBackend"
105        ))
106    }
107
108    /// Get SCRAM-SHA-256 StoredKey for a user
109    ///
110    /// StoredKey = SHA256(ClientKey) where ClientKey = HMAC(SaltedPassword, "Client Key")
111    /// Default implementation returns an error indicating SCRAM is not supported.
112    async fn get_scram_stored_key(&self, _username: &str) -> anyhow::Result<Vec<u8>> {
113        Err(anyhow::anyhow!(
114            "SCRAM-SHA-256 credential storage not implemented in this AuthBackend"
115        ))
116    }
117
118    /// Get SCRAM-SHA-256 ServerKey for a user
119    ///
120    /// ServerKey = HMAC(SaltedPassword, "Server Key")
121    /// Default implementation returns an error indicating SCRAM is not supported.
122    async fn get_scram_server_key(&self, _username: &str) -> anyhow::Result<Vec<u8>> {
123        Err(anyhow::anyhow!(
124            "SCRAM-SHA-256 credential storage not implemented in this AuthBackend"
125        ))
126    }
127
128    /// Store SCRAM-SHA-256 credentials for a user
129    ///
130    /// This should store: salt, iterations, StoredKey, and ServerKey
131    /// Default implementation returns an error indicating SCRAM is not supported.
132    async fn store_scram_credentials(
133        &self,
134        _username: &Username,
135        _salt: Vec<u8>,
136        _iterations: u32,
137        _stored_key: Vec<u8>,
138        _server_key: Vec<u8>,
139    ) -> anyhow::Result<()> {
140        Err(anyhow::anyhow!(
141            "SCRAM-SHA-256 credential storage not implemented in this AuthBackend"
142        ))
143    }
144
145    // ========================================================================
146    // APOP (MD5 Digest) Support (Optional)
147    // ========================================================================
148    // APOP requires access to plaintext password to compute MD5 digest.
149    // This is incompatible with bcrypt and most secure password storage.
150    // Backends can optionally support APOP by storing plaintext passwords
151    // separately (not recommended for production).
152
153    /// Get plaintext password for APOP authentication
154    ///
155    /// Returns the plaintext password if available.
156    /// Default implementation returns an error indicating APOP is not supported.
157    ///
158    /// WARNING: This method exposes plaintext passwords and should only be used
159    /// for APOP authentication. Consider disabling APOP in production environments.
160    async fn get_apop_secret(&self, _username: &Username) -> anyhow::Result<String> {
161        Err(anyhow::anyhow!(
162            "APOP authentication not supported by this AuthBackend"
163        ))
164    }
165}