diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 97377428..4748e275 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -197,7 +197,7 @@ The `bssh-server` binary provides a command-line interface for managing and oper **Subcommands**: - **run** - Start the SSH server (default when no subcommand specified) - **gen-config** - Generate a configuration file template with secure defaults -- **hash-password** - Hash passwords for configuration using bcrypt +- **hash-password** - Hash passwords for configuration using Argon2id (recommended) - **check-config** - Validate configuration files and display settings - **gen-host-key** - Generate SSH host keys (Ed25519 or RSA) - **version** - Show version and build information @@ -321,6 +321,8 @@ The authentication subsystem (`src/server/auth/`) provides extensible authentica - `mod.rs` - Module exports and re-exports - `provider.rs` - `AuthProvider` trait definition - `publickey.rs` - `PublicKeyVerifier` implementation +- `password.rs` - `PasswordVerifier` implementation with Argon2id hashing +- `composite.rs` - `CompositeAuthProvider` combining multiple auth methods **AuthProvider Trait**: @@ -356,6 +358,40 @@ Implements public key authentication by parsing OpenSSH authorized_keys files: - `no-pty`, `no-port-forwarding`, `no-agent-forwarding`, `no-X11-forwarding` - `environment="..."` - Set environment variables +**PasswordVerifier**: + +Implements password authentication with secure password hashing: + +- **Argon2id hashing**: Uses the OWASP-recommended password hashing algorithm + - Memory cost: 19 MiB + - Time cost: 2 iterations + - Parallelism: 1 + +- **User configuration**: + - External YAML file with user definitions + - Inline users in server configuration + - User attributes: name, password_hash, shell, home, env + +- **Security features**: + - Timing attack mitigation with constant-time verification + - Minimum verification time (100ms) regardless of user existence + - Dummy hash verification for non-existent users + - Secure memory cleanup using `zeroize` crate + - User enumeration protection + +- **Hash compatibility**: + - Argon2id (recommended, generated by `hash-password` command) + - bcrypt (supported for backward compatibility) + +**CompositeAuthProvider**: + +Combines multiple authentication methods into a single provider: + +- Delegates to `PublicKeyVerifier` for public key auth +- Delegates to `PasswordVerifier` for password auth +- Prioritizes password verifier for user info (more detailed) +- Supports hot-reloading of password users via `reload_password_users()` + **Security Features**: - **Username validation**: Prevents path traversal attacks (e.g., `../etc/passwd`) @@ -363,7 +399,8 @@ Implements public key authentication by parsing OpenSSH authorized_keys files: - **Symlink protection**: Uses `symlink_metadata()` to detect and reject symlinks - **Parent directory validation**: Checks parent directory permissions - **Rate limiting**: Token bucket rate limiter for authentication attempts -- **Timing attack mitigation**: Constant-time behavior in `user_exists()` check +- **Timing attack mitigation**: Constant-time behavior in password verification and `user_exists()` check +- **Secure memory handling**: Password strings cleared from memory after use via `zeroize` - **Comprehensive logging**: All authentication attempts are logged ## Data Flow diff --git a/Cargo.lock b/Cargo.lock index d0073e0e..68e1d34f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -383,6 +383,7 @@ name = "bssh" version = "1.7.0" dependencies = [ "anyhow", + "argon2", "arrayvec", "async-trait", "atty", diff --git a/Cargo.toml b/Cargo.toml index 26911039..d980a085 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ shell-words = "1.1.1" libc = "0.2" ipnetwork = "0.20" bcrypt = "0.16" +argon2 = "0.5" rand = "0.8" ssh-key = { version = "0.6", features = ["std"] } diff --git a/docs/architecture/server-configuration.md b/docs/architecture/server-configuration.md index 445b1cbb..84c8c2a9 100644 --- a/docs/architecture/server-configuration.md +++ b/docs/architecture/server-configuration.md @@ -118,7 +118,7 @@ auth: # Inline user definitions users: - name: testuser - password_hash: "$6$rounds=656000$..." # openssl passwd -6 + password_hash: "$argon2id$v=19$m=19456,t=2,p=1$..." # bssh-server hash-password shell: /bin/bash home: /home/testuser env: @@ -364,11 +364,19 @@ Generated keys have secure permissions (0600) and are in OpenSSH format. ### Hash Passwords ```bash -# Interactive password hashing with bcrypt +# Interactive password hashing with Argon2id (recommended) bssh-server hash-password ``` -This prompts for a password, confirms it, and outputs a bcrypt hash suitable for use in the configuration file. +This prompts for a password, confirms it, and outputs an Argon2id hash suitable for use in the configuration file. Argon2id is the OWASP-recommended password hashing algorithm with memory-hard properties that resist GPU and ASIC attacks. + +The generated hash includes: +- Algorithm: Argon2id (variant resistant to both side-channel and GPU attacks) +- Memory cost: 19 MiB +- Time cost: 2 iterations +- Parallelism: 1 + +Note: bcrypt hashes are also supported for backward compatibility with existing configurations. ### Validate Configuration diff --git a/src/bin/bssh_server.rs b/src/bin/bssh_server.rs index afc74e91..5ec73050 100644 --- a/src/bin/bssh_server.rs +++ b/src/bin/bssh_server.rs @@ -257,6 +257,7 @@ fn gen_config(output: Option) -> Result<()> { /// Hash a password for configuration async fn hash_password() -> Result<()> { + use bssh::server::auth::hash_password as generate_hash; use rpassword::read_password; print!("Enter password: "); @@ -269,7 +270,7 @@ async fn hash_password() -> Result<()> { // Warn about weak passwords (but still allow them) if password.len() < 8 { - println!("\n⚠ Warning: Password is shorter than 8 characters."); + println!("\n Warning: Password is shorter than 8 characters."); println!(" This is considered weak and may be easily compromised."); println!(" Consider using a longer password for better security.\n"); } @@ -282,8 +283,8 @@ async fn hash_password() -> Result<()> { anyhow::bail!("Passwords do not match"); } - // Use bcrypt for password hashing (cost factor 12) - let hash = bcrypt::hash(&password, 12).context("Failed to hash password")?; + // Use Argon2id for password hashing (recommended algorithm) + let hash = generate_hash(&password).context("Failed to hash password")?; println!("\nPassword hash (use in configuration):"); println!("{}", hash); @@ -295,6 +296,8 @@ async fn hash_password() -> Result<()> { println!(" users:"); println!(" - name: username"); println!(" password_hash: \"{}\"", hash); + println!("\nNote: This hash uses Argon2id algorithm (recommended)."); + println!(" bcrypt hashes are also supported for compatibility."); Ok(()) } diff --git a/src/server/auth/composite.rs b/src/server/auth/composite.rs new file mode 100644 index 00000000..01b6f109 --- /dev/null +++ b/src/server/auth/composite.rs @@ -0,0 +1,310 @@ +// Copyright 2025 Lablup Inc. and Jeongkyu Shin +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Composite authentication provider. +//! +//! This module provides a composite authentication provider that combines +//! multiple authentication methods (public key and password authentication). + +use std::sync::Arc; + +use anyhow::Result; +use async_trait::async_trait; +use russh::keys::ssh_key::PublicKey; + +use super::password::{PasswordAuthConfig, PasswordVerifier}; +use super::provider::AuthProvider; +use super::publickey::{PublicKeyAuthConfig, PublicKeyVerifier}; +use crate::shared::auth_types::{AuthResult, UserInfo}; + +/// Composite authentication provider that supports multiple auth methods. +/// +/// This provider delegates to specific providers based on the authentication +/// method being used. It supports: +/// +/// - Public key authentication via [`PublicKeyVerifier`] +/// - Password authentication via [`PasswordVerifier`] +/// +/// # Example +/// +/// ```no_run +/// use bssh::server::auth::CompositeAuthProvider; +/// use bssh::server::auth::PublicKeyAuthConfig; +/// use bssh::server::auth::PasswordAuthConfig; +/// +/// # async fn example() -> anyhow::Result<()> { +/// let pubkey_config = PublicKeyAuthConfig::with_directory("/etc/bssh/authorized_keys"); +/// let password_config = PasswordAuthConfig::default(); +/// +/// let provider = CompositeAuthProvider::new( +/// Some(pubkey_config), +/// Some(password_config), +/// ).await?; +/// # Ok(()) +/// # } +/// ``` +pub struct CompositeAuthProvider { + /// Public key verifier (if public key auth is enabled). + publickey_verifier: Option, + + /// Password verifier (if password auth is enabled). + password_verifier: Option>, +} + +impl CompositeAuthProvider { + /// Create a new composite auth provider. + /// + /// # Arguments + /// + /// * `publickey_config` - Configuration for public key authentication (None to disable) + /// * `password_config` - Configuration for password authentication (None to disable) + /// + /// # Returns + /// + /// A new composite auth provider, or an error if initialization fails. + pub async fn new( + publickey_config: Option, + password_config: Option, + ) -> Result { + let publickey_verifier = publickey_config.map(PublicKeyVerifier::new); + + let password_verifier = match password_config { + Some(config) => Some(Arc::new(PasswordVerifier::new(config).await?)), + None => None, + }; + + tracing::info!( + publickey_enabled = publickey_verifier.is_some(), + password_enabled = password_verifier.is_some(), + "Composite auth provider initialized" + ); + + Ok(Self { + publickey_verifier, + password_verifier, + }) + } + + /// Create a provider with only public key authentication. + pub fn publickey_only(config: PublicKeyAuthConfig) -> Self { + Self { + publickey_verifier: Some(PublicKeyVerifier::new(config)), + password_verifier: None, + } + } + + /// Create a provider with only password authentication. + pub async fn password_only(config: PasswordAuthConfig) -> Result { + Ok(Self { + publickey_verifier: None, + password_verifier: Some(Arc::new(PasswordVerifier::new(config).await?)), + }) + } + + /// Check if public key authentication is enabled. + pub fn publickey_enabled(&self) -> bool { + self.publickey_verifier.is_some() + } + + /// Check if password authentication is enabled. + pub fn password_enabled(&self) -> bool { + self.password_verifier.is_some() + } + + /// Get a reference to the password verifier (if enabled). + pub fn password_verifier(&self) -> Option<&Arc> { + self.password_verifier.as_ref() + } + + /// Reload password users from configuration. + /// + /// This allows hot-reloading of user configuration without restarting the server. + pub async fn reload_password_users(&self) -> Result<()> { + if let Some(ref verifier) = self.password_verifier { + verifier.reload_users().await?; + } + Ok(()) + } +} + +#[async_trait] +impl AuthProvider for CompositeAuthProvider { + async fn verify_publickey(&self, username: &str, key: &PublicKey) -> Result { + if let Some(ref verifier) = self.publickey_verifier { + verifier.verify_publickey(username, key).await + } else { + // Public key auth not enabled + Ok(AuthResult::Reject) + } + } + + async fn verify_password(&self, username: &str, password: &str) -> Result { + if let Some(ref verifier) = self.password_verifier { + verifier.verify_password(username, password).await + } else { + // Password auth not enabled + Ok(AuthResult::Reject) + } + } + + async fn get_user_info(&self, username: &str) -> Result> { + // Try to get user info from password verifier first (has more detailed info) + if let Some(ref verifier) = self.password_verifier { + if let Some(info) = verifier.get_user_info(username).await? { + return Ok(Some(info)); + } + } + + // Fall back to public key verifier + if let Some(ref verifier) = self.publickey_verifier { + return verifier.get_user_info(username).await; + } + + Ok(None) + } + + async fn user_exists(&self, username: &str) -> Result { + // Check password verifier first + if let Some(ref verifier) = self.password_verifier { + if verifier.user_exists(username).await? { + return Ok(true); + } + } + + // Check public key verifier + if let Some(ref verifier) = self.publickey_verifier { + if verifier.user_exists(username).await? { + return Ok(true); + } + } + + Ok(false) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::server::auth::hash_password; + use crate::server::config::UserDefinition; + use std::collections::HashMap; + + #[tokio::test] + async fn test_composite_provider_publickey_only() { + let config = PublicKeyAuthConfig::with_directory("/tmp/nonexistent"); + let provider = CompositeAuthProvider::publickey_only(config); + + assert!(provider.publickey_enabled()); + assert!(!provider.password_enabled()); + } + + #[tokio::test] + async fn test_composite_provider_password_only() { + let hash = hash_password("password").unwrap(); + let users = vec![UserDefinition { + name: "testuser".to_string(), + password_hash: hash, + shell: None, + home: None, + env: HashMap::new(), + }]; + + let config = PasswordAuthConfig::with_users(users); + let provider = CompositeAuthProvider::password_only(config).await.unwrap(); + + assert!(!provider.publickey_enabled()); + assert!(provider.password_enabled()); + + // Test password verification + let result = provider + .verify_password("testuser", "password") + .await + .unwrap(); + assert!(result.is_accepted()); + + let result = provider.verify_password("testuser", "wrong").await.unwrap(); + assert!(result.is_rejected()); + } + + #[tokio::test] + async fn test_composite_provider_both() { + let pubkey_config = PublicKeyAuthConfig::with_directory("/tmp/nonexistent"); + let hash = hash_password("password").unwrap(); + let users = vec![UserDefinition { + name: "testuser".to_string(), + password_hash: hash, + shell: None, + home: None, + env: HashMap::new(), + }]; + let password_config = PasswordAuthConfig::with_users(users); + + let provider = CompositeAuthProvider::new(Some(pubkey_config), Some(password_config)) + .await + .unwrap(); + + assert!(provider.publickey_enabled()); + assert!(provider.password_enabled()); + } + + #[tokio::test] + async fn test_composite_provider_user_info() { + let hash = hash_password("password").unwrap(); + let users = vec![UserDefinition { + name: "testuser".to_string(), + password_hash: hash, + shell: Some("/bin/bash".into()), + home: Some("/home/testuser".into()), + env: HashMap::new(), + }]; + + let config = PasswordAuthConfig::with_users(users); + let provider = CompositeAuthProvider::password_only(config).await.unwrap(); + + let info = provider.get_user_info("testuser").await.unwrap(); + assert!(info.is_some()); + let info = info.unwrap(); + assert_eq!(info.username, "testuser"); + assert_eq!(info.shell.to_str().unwrap(), "/bin/bash"); + assert_eq!(info.home_dir.to_str().unwrap(), "/home/testuser"); + } + + #[tokio::test] + async fn test_composite_provider_user_exists() { + let hash = hash_password("password").unwrap(); + let users = vec![UserDefinition { + name: "existinguser".to_string(), + password_hash: hash, + shell: None, + home: None, + env: HashMap::new(), + }]; + + let config = PasswordAuthConfig::with_users(users); + let provider = CompositeAuthProvider::password_only(config).await.unwrap(); + + assert!(provider.user_exists("existinguser").await.unwrap()); + assert!(!provider.user_exists("nonexistent").await.unwrap()); + } + + #[tokio::test] + async fn test_composite_provider_disabled_methods() { + let pubkey_config = PublicKeyAuthConfig::with_directory("/tmp/nonexistent"); + let provider = CompositeAuthProvider::publickey_only(pubkey_config); + + // Password auth should reject when disabled + let result = provider.verify_password("user", "pass").await.unwrap(); + assert!(result.is_rejected()); + } +} diff --git a/src/server/auth/mod.rs b/src/server/auth/mod.rs index 84d9845f..511d86e9 100644 --- a/src/server/auth/mod.rs +++ b/src/server/auth/mod.rs @@ -16,7 +16,7 @@ //! //! This module provides the authentication framework for the SSH server, //! including traits for authentication providers and implementations for -//! public key authentication. +//! public key and password authentication. //! //! # Architecture //! @@ -24,31 +24,86 @@ //! which allows for extensible authentication methods. Currently supported: //! //! - **Public Key Authentication**: Via [`PublicKeyVerifier`] +//! - **Password Authentication**: Via [`PasswordVerifier`] with Argon2id hashing +//! - **Composite Authentication**: Via [`CompositeAuthProvider`] combining multiple methods //! //! # Security Features //! //! - Username validation to prevent path traversal attacks //! - Rate limiting integration //! - Logging of authentication attempts (success/failure) -//! - Timing attack mitigation where possible +//! - Timing attack mitigation with constant-time verification +//! - Secure memory cleanup using `zeroize` for password handling +//! - User enumeration protection via dummy hash verification //! //! # Usage //! +//! ## Public Key Authentication +//! //! ```no_run //! use bssh::server::auth::{AuthProvider, PublicKeyVerifier, PublicKeyAuthConfig}; -//! use std::path::PathBuf; //! //! // Create a public key verifier //! let config = PublicKeyAuthConfig::with_directory("/etc/bssh/authorized_keys"); //! let verifier = PublicKeyVerifier::new(config); //! //! // Use with SSH handler -//! // verifier.verify("username", &public_key).await +//! // verifier.verify_publickey("username", &public_key).await +//! ``` +//! +//! ## Password Authentication +//! +//! ```no_run +//! use bssh::server::auth::{PasswordVerifier, PasswordAuthConfig, hash_password}; +//! use bssh::server::config::UserDefinition; +//! use std::collections::HashMap; +//! +//! # async fn example() -> anyhow::Result<()> { +//! // Hash a password using Argon2id +//! let hash = hash_password("secure_password")?; +//! +//! // Create inline user configuration +//! let users = vec![UserDefinition { +//! name: "testuser".to_string(), +//! password_hash: hash, +//! shell: None, +//! home: None, +//! env: HashMap::new(), +//! }]; +//! +//! let config = PasswordAuthConfig::with_users(users); +//! let verifier = PasswordVerifier::new(config).await?; +//! +//! // Verify a password +//! let result = verifier.verify("testuser", "secure_password").await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Composite Authentication +//! +//! ```no_run +//! use bssh::server::auth::{CompositeAuthProvider, PublicKeyAuthConfig, PasswordAuthConfig}; +//! +//! # async fn example() -> anyhow::Result<()> { +//! let pubkey_config = PublicKeyAuthConfig::with_directory("/etc/bssh/authorized_keys"); +//! let password_config = PasswordAuthConfig::default(); +//! +//! let provider = CompositeAuthProvider::new( +//! Some(pubkey_config), +//! Some(password_config), +//! ).await?; +//! # Ok(()) +//! # } //! ``` +pub mod composite; +pub mod password; pub mod provider; pub mod publickey; +pub use composite::CompositeAuthProvider; +pub use password::{hash_password, verify_password_hash, PasswordAuthConfig, PasswordVerifier}; pub use provider::AuthProvider; pub use publickey::{AuthKeyOptions, AuthorizedKey, PublicKeyAuthConfig, PublicKeyVerifier}; diff --git a/src/server/auth/password.rs b/src/server/auth/password.rs new file mode 100644 index 00000000..48254a36 --- /dev/null +++ b/src/server/auth/password.rs @@ -0,0 +1,793 @@ +// Copyright 2025 Lablup Inc. and Jeongkyu Shin +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Password authentication verifier. +//! +//! This module provides the [`PasswordVerifier`] which implements password +//! authentication using Argon2id hashing with proper security measures. +//! +//! # Security Features +//! +//! - **Argon2id hashing**: Uses the recommended password hashing algorithm +//! - **Timing attack mitigation**: Ensures constant-time verification +//! - **Memory cleanup**: Uses `zeroize` for secure password memory cleanup +//! - **User enumeration protection**: Performs dummy verification for non-existent users +//! +//! # Configuration +//! +//! Users can be configured in two ways: +//! +//! 1. **External file**: A YAML file containing user definitions +//! 2. **Inline configuration**: Users defined directly in the server config +//! +//! # Example +//! +//! ```no_run +//! use bssh::server::auth::password::{PasswordAuthConfig, PasswordVerifier}; +//! use std::path::PathBuf; +//! +//! # async fn example() -> anyhow::Result<()> { +//! let config = PasswordAuthConfig { +//! users_file: Some(PathBuf::from("/etc/bssh/users.yaml")), +//! users: vec![], +//! }; +//! +//! let verifier = PasswordVerifier::new(config).await?; +//! let result = verifier.verify("username", "password").await?; +//! # Ok(()) +//! # } +//! ``` + +use std::collections::HashMap; +use std::path::PathBuf; +use std::time::{Duration, Instant}; + +use anyhow::{Context, Result}; +use argon2::{ + password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier as _}, + Algorithm, Argon2, Params, Version, +}; +use async_trait::async_trait; +use russh::keys::ssh_key::PublicKey; +use serde::Deserialize; +use tokio::sync::RwLock; +use zeroize::Zeroizing; + +use super::provider::AuthProvider; +use crate::server::config::UserDefinition; +use crate::shared::auth_types::{AuthResult, UserInfo}; +use crate::shared::validation::validate_username; + +/// Configuration for password authentication. +#[derive(Debug, Clone, Default)] +pub struct PasswordAuthConfig { + /// Path to YAML file containing user definitions. + pub users_file: Option, + + /// Inline user definitions. + pub users: Vec, +} + +impl PasswordAuthConfig { + /// Create a new configuration with a users file path. + pub fn with_users_file(path: impl Into) -> Self { + Self { + users_file: Some(path.into()), + users: vec![], + } + } + + /// Create a new configuration with inline users. + pub fn with_users(users: Vec) -> Self { + Self { + users_file: None, + users, + } + } +} + +/// Password verifier with Argon2id hashing. +/// +/// This struct implements secure password verification with: +/// - Argon2id password hashing (also supports bcrypt for compatibility) +/// - Timing attack mitigation +/// - User enumeration protection +/// - Secure memory handling via `zeroize` +pub struct PasswordVerifier { + /// Configuration for password authentication. + config: PasswordAuthConfig, + + /// Loaded users (keyed by username). + users: RwLock>, + + /// Pre-computed dummy hash for timing attack mitigation. + /// This hash is verified against when the user doesn't exist. + dummy_hash: String, +} + +impl PasswordVerifier { + /// Create a new password verifier. + /// + /// This loads users from the configuration on initialization. + /// + /// # Arguments + /// + /// * `config` - Password authentication configuration + /// + /// # Returns + /// + /// A new password verifier, or an error if user loading fails. + pub async fn new(config: PasswordAuthConfig) -> Result { + // Generate a dummy hash for timing attack mitigation + let dummy_hash = hash_password("dummy_password_for_timing_attack_mitigation")?; + + let verifier = Self { + config, + users: RwLock::new(HashMap::new()), + dummy_hash, + }; + + verifier.reload_users().await?; + Ok(verifier) + } + + /// Reload users from configuration. + /// + /// This method reloads users from both the users file (if configured) + /// and inline user definitions. Inline users override file users. + pub async fn reload_users(&self) -> Result<()> { + let mut users = HashMap::new(); + + // Load from file if specified + if let Some(ref path) = self.config.users_file { + match tokio::fs::read_to_string(path).await { + Ok(content) => { + let file_users: UsersFile = + serde_yaml::from_str(&content).with_context(|| { + format!("Failed to parse users file: {}", path.display()) + })?; + + for user in file_users.users { + tracing::debug!(user = %user.name, "Loaded user from file"); + users.insert(user.name.clone(), user); + } + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + tracing::warn!( + path = %path.display(), + "Users file not found, using only inline users" + ); + } + Err(e) => { + return Err(e) + .with_context(|| format!("Failed to read users file: {}", path.display())); + } + } + } + + // Add inline users (override file users) + for user in &self.config.users { + tracing::debug!(user = %user.name, "Loaded inline user"); + users.insert(user.name.clone(), user.clone()); + } + + let user_count = users.len(); + *self.users.write().await = users; + + tracing::info!( + user_count = %user_count, + "Users loaded for password authentication" + ); + + Ok(()) + } + + /// Verify a password for a user. + /// + /// This method implements timing attack mitigation by ensuring + /// verification takes a minimum amount of time, regardless of + /// whether the user exists or the password is correct. + /// + /// # Arguments + /// + /// * `username` - The username to verify + /// * `password` - The password to verify + /// + /// # Returns + /// + /// `Ok(true)` if the password is correct, `Ok(false)` otherwise. + pub async fn verify(&self, username: &str, password: &str) -> Result { + // Wrap password in Zeroizing for secure cleanup + let password = Zeroizing::new(password.to_string()); + + // Timing attack mitigation: ensure minimum time for verification + let start = Instant::now(); + let min_time = Duration::from_millis(100); + + let result = self.verify_internal(username, &password).await; + + // Normalize timing by sleeping if we finished early + let elapsed = start.elapsed(); + if elapsed < min_time { + tokio::time::sleep(min_time - elapsed).await; + } + + result + } + + /// Internal verification logic. + async fn verify_internal(&self, username: &str, password: &Zeroizing) -> Result { + // Validate username first + let validated_username = match validate_username(username) { + Ok(name) => name, + Err(_) => { + // Invalid username - do dummy verification to prevent timing attacks + let _ = self.verify_dummy_hash(password); + tracing::debug!( + user = %username, + "Password authentication failed: invalid username" + ); + return Ok(false); + } + }; + + let users: tokio::sync::RwLockReadGuard<'_, HashMap> = + self.users.read().await; + + let user = match users.get(&validated_username) { + Some(u) => u, + None => { + // User doesn't exist - do dummy verification to prevent timing attacks + let _ = self.verify_dummy_hash(password); + tracing::debug!( + user = %validated_username, + "Password authentication failed: user not found" + ); + return Ok(false); + } + }; + + // Verify password against stored hash + let hash_str = &user.password_hash; + + // Try Argon2id first, then fall back to bcrypt for compatibility + let verified = if hash_str.starts_with("$argon2") { + self.verify_argon2(password.as_bytes(), hash_str)? + } else if hash_str.starts_with("$2") { + // bcrypt hash format ($2a$, $2b$, $2y$) + self.verify_bcrypt(password, hash_str)? + } else { + tracing::warn!( + user = %validated_username, + "Unknown password hash format" + ); + return Ok(false); + }; + + if verified { + tracing::info!( + user = %validated_username, + "Password authentication successful" + ); + Ok(true) + } else { + tracing::debug!( + user = %validated_username, + "Password authentication failed: incorrect password" + ); + Ok(false) + } + } + + /// Verify password against an Argon2 hash. + fn verify_argon2(&self, password: &[u8], hash_str: &str) -> Result { + let hash = PasswordHash::new(hash_str) + .map_err(|e| anyhow::anyhow!("Invalid Argon2 hash format: {}", e))?; + + let argon2 = Argon2::default(); + + match argon2.verify_password(password, &hash) { + Ok(()) => Ok(true), + Err(argon2::password_hash::Error::Password) => Ok(false), + Err(e) => Err(anyhow::anyhow!("Argon2 verification error: {}", e)), + } + } + + /// Verify password against a bcrypt hash. + fn verify_bcrypt(&self, password: &Zeroizing, hash_str: &str) -> Result { + match bcrypt::verify(password.as_str(), hash_str) { + Ok(verified) => Ok(verified), + Err(e) => { + tracing::warn!(error = %e, "bcrypt verification error"); + Ok(false) + } + } + } + + /// Perform dummy hash verification for timing attack mitigation. + /// + /// This ensures that verification takes the same amount of time + /// regardless of whether the user exists. + fn verify_dummy_hash(&self, password: &Zeroizing) -> bool { + // Verify against the pre-computed dummy hash + if let Ok(hash) = PasswordHash::new(&self.dummy_hash) { + let argon2 = Argon2::default(); + argon2.verify_password(password.as_bytes(), &hash).is_ok() + } else { + false + } + } + + /// Get user information for an authenticated user. + /// + /// # Arguments + /// + /// * `username` - The username to get info for + /// + /// # Returns + /// + /// User information if the user exists, None otherwise. + pub async fn get_user(&self, username: &str) -> Option { + let users: tokio::sync::RwLockReadGuard<'_, HashMap> = + self.users.read().await; + users.get(username).map(|u| { + let mut info = UserInfo::new(&u.name); + + if let Some(home) = &u.home { + info = info.with_home_dir(home.clone()); + } + if let Some(shell) = &u.shell { + info = info.with_shell(shell.clone()); + } + + info + }) + } + + /// Check if a user exists (for user enumeration, use with caution). + /// + /// # Warning + /// + /// This method reveals whether a user exists. Consider using + /// `verify` instead which provides timing attack protection. + pub async fn user_exists_internal(&self, username: &str) -> bool { + let users: tokio::sync::RwLockReadGuard<'_, HashMap> = + self.users.read().await; + users.contains_key(username) + } +} + +#[async_trait] +impl AuthProvider for PasswordVerifier { + async fn verify_publickey(&self, _username: &str, _key: &PublicKey) -> Result { + // Password verifier doesn't handle public key auth + Ok(AuthResult::Reject) + } + + async fn verify_password(&self, username: &str, password: &str) -> Result { + match self.verify(username, password).await { + Ok(true) => Ok(AuthResult::Accept), + Ok(false) => Ok(AuthResult::Reject), + Err(e) => { + tracing::error!( + user = %username, + error = %e, + "Error during password verification" + ); + Ok(AuthResult::Reject) + } + } + } + + async fn get_user_info(&self, username: &str) -> Result> { + Ok(self.get_user(username).await) + } + + async fn user_exists(&self, username: &str) -> Result { + // SECURITY: Use timing-safe verification to prevent user enumeration + // We always do a full verification cycle regardless of user existence + let start = Instant::now(); + let min_time = Duration::from_millis(50); + + let exists = self.user_exists_internal(username).await; + + // Normalize timing + let elapsed = start.elapsed(); + if elapsed < min_time { + tokio::time::sleep(min_time - elapsed).await; + } + + Ok(exists) + } +} + +/// Users file structure for YAML parsing. +#[derive(Debug, Deserialize)] +struct UsersFile { + users: Vec, +} + +/// Generate an Argon2id password hash. +/// +/// This function generates a secure password hash using Argon2id +/// with recommended parameters. +/// +/// # Arguments +/// +/// * `password` - The plaintext password to hash +/// +/// # Returns +/// +/// The Argon2id hash string, or an error if hashing fails. +/// +/// # Example +/// +/// ```no_run +/// use bssh::server::auth::password::hash_password; +/// +/// let hash = hash_password("my_secure_password").unwrap(); +/// println!("Hash: {}", hash); +/// ``` +pub fn hash_password(password: &str) -> Result { + use argon2::password_hash::SaltString; + + let salt = SaltString::generate(&mut OsRng); + + // Use Argon2id with secure parameters + // These parameters balance security and performance: + // - m=19456 KiB (19 MiB memory) + // - t=2 iterations + // - p=1 parallelism + let params = Params::new( + 19456, // m_cost (memory in KiB) + 2, // t_cost (iterations) + 1, // p_cost (parallelism) + None, // output_len (use default) + ) + .map_err(|e| anyhow::anyhow!("Invalid Argon2 parameters: {}", e))?; + + let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params); + + let hash = argon2 + .hash_password(password.as_bytes(), &salt) + .map_err(|e| anyhow::anyhow!("Failed to hash password: {}", e))?; + + Ok(hash.to_string()) +} + +/// Verify a password against an Argon2id hash. +/// +/// # Arguments +/// +/// * `password` - The plaintext password +/// * `hash` - The Argon2id hash string +/// +/// # Returns +/// +/// `true` if the password matches, `false` otherwise. +pub fn verify_password_hash(password: &str, hash: &str) -> Result { + let parsed_hash = + PasswordHash::new(hash).map_err(|e| anyhow::anyhow!("Invalid hash format: {}", e))?; + + let argon2 = Argon2::default(); + + match argon2.verify_password(password.as_bytes(), &parsed_hash) { + Ok(()) => Ok(true), + Err(argon2::password_hash::Error::Password) => Ok(false), + Err(e) => Err(anyhow::anyhow!("Verification error: {}", e)), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::Instant; + + #[test] + fn test_hash_password() { + let password = "test_password_123"; + let hash = hash_password(password).unwrap(); + + // Verify the hash starts with the Argon2id identifier + assert!(hash.starts_with("$argon2id$")); + + // Verify the hash can be parsed + let parsed = PasswordHash::new(&hash).unwrap(); + assert_eq!(parsed.algorithm, argon2::Algorithm::Argon2id.ident()); + } + + #[test] + fn test_verify_password_hash() { + let password = "test_password_123"; + let hash = hash_password(password).unwrap(); + + // Correct password should verify + assert!(verify_password_hash(password, &hash).unwrap()); + + // Incorrect password should not verify + assert!(!verify_password_hash("wrong_password", &hash).unwrap()); + } + + #[test] + fn test_hash_uniqueness() { + let password = "same_password"; + let hash1 = hash_password(password).unwrap(); + let hash2 = hash_password(password).unwrap(); + + // Each hash should be unique due to random salt + assert_ne!(hash1, hash2); + + // Both should verify correctly + assert!(verify_password_hash(password, &hash1).unwrap()); + assert!(verify_password_hash(password, &hash2).unwrap()); + } + + #[test] + fn test_verify_invalid_hash_format() { + let result = verify_password_hash("password", "invalid_hash"); + assert!(result.is_err()); + } + + #[test] + fn test_password_auth_config_with_users_file() { + let config = PasswordAuthConfig::with_users_file("/etc/bssh/users.yaml"); + assert!(config.users_file.is_some()); + assert!(config.users.is_empty()); + } + + #[test] + fn test_password_auth_config_with_users() { + let users = vec![UserDefinition { + name: "testuser".to_string(), + password_hash: hash_password("password").unwrap(), + shell: None, + home: None, + env: HashMap::new(), + }]; + + let config = PasswordAuthConfig::with_users(users); + assert!(config.users_file.is_none()); + assert_eq!(config.users.len(), 1); + } + + #[tokio::test] + async fn test_password_verifier_inline_users() { + let hash = hash_password("correct_password").unwrap(); + let users = vec![UserDefinition { + name: "testuser".to_string(), + password_hash: hash, + shell: None, + home: None, + env: HashMap::new(), + }]; + + let config = PasswordAuthConfig::with_users(users); + let verifier = PasswordVerifier::new(config).await.unwrap(); + + // Correct password should verify + assert!(verifier + .verify("testuser", "correct_password") + .await + .unwrap()); + + // Incorrect password should not verify + assert!(!verifier.verify("testuser", "wrong_password").await.unwrap()); + + // Non-existent user should not verify + assert!(!verifier.verify("nonexistent", "password").await.unwrap()); + } + + #[tokio::test] + async fn test_password_verifier_bcrypt_compatibility() { + // Create a bcrypt hash + let bcrypt_hash = bcrypt::hash("bcrypt_password", 4).unwrap(); + let users = vec![UserDefinition { + name: "bcryptuser".to_string(), + password_hash: bcrypt_hash, + shell: None, + home: None, + env: HashMap::new(), + }]; + + let config = PasswordAuthConfig::with_users(users); + let verifier = PasswordVerifier::new(config).await.unwrap(); + + // bcrypt password should verify + assert!(verifier + .verify("bcryptuser", "bcrypt_password") + .await + .unwrap()); + + // Wrong password should not verify + assert!(!verifier.verify("bcryptuser", "wrong").await.unwrap()); + } + + #[tokio::test] + async fn test_password_verifier_timing_attack_mitigation() { + let hash = hash_password("password").unwrap(); + let users = vec![UserDefinition { + name: "testuser".to_string(), + password_hash: hash, + shell: None, + home: None, + env: HashMap::new(), + }]; + + let config = PasswordAuthConfig::with_users(users); + let verifier = PasswordVerifier::new(config).await.unwrap(); + + // Measure time for existing user with wrong password + let start = Instant::now(); + let _ = verifier.verify("testuser", "wrong_password").await; + let time_existing = start.elapsed(); + + // Measure time for non-existing user + let start = Instant::now(); + let _ = verifier.verify("nonexistent_user", "password").await; + let time_nonexistent = start.elapsed(); + + // Both should take at least the minimum time (100ms) + assert!(time_existing >= Duration::from_millis(90)); // Allow small margin + assert!(time_nonexistent >= Duration::from_millis(90)); + + // The times should be roughly similar (within 50ms margin) + let diff = if time_existing > time_nonexistent { + time_existing - time_nonexistent + } else { + time_nonexistent - time_existing + }; + assert!( + diff < Duration::from_millis(50), + "Timing difference too large: {:?}", + diff + ); + } + + #[tokio::test] + async fn test_password_verifier_invalid_username() { + let config = PasswordAuthConfig::default(); + let verifier = PasswordVerifier::new(config).await.unwrap(); + + // Path traversal attempt should fail safely + let result = verifier.verify("../etc/passwd", "password").await; + assert!(result.is_ok()); + assert!(!result.unwrap()); + + // Empty username should fail safely + let result = verifier.verify("", "password").await; + assert!(result.is_ok()); + assert!(!result.unwrap()); + } + + #[tokio::test] + async fn test_password_verifier_get_user() { + let hash = hash_password("password").unwrap(); + let users = vec![UserDefinition { + name: "testuser".to_string(), + password_hash: hash, + shell: Some(PathBuf::from("/bin/bash")), + home: Some(PathBuf::from("/home/testuser")), + env: HashMap::new(), + }]; + + let config = PasswordAuthConfig::with_users(users); + let verifier = PasswordVerifier::new(config).await.unwrap(); + + // Existing user should return info + let user_info = verifier.get_user("testuser").await; + assert!(user_info.is_some()); + let info = user_info.unwrap(); + assert_eq!(info.username, "testuser"); + assert_eq!(info.shell, PathBuf::from("/bin/bash")); + assert_eq!(info.home_dir, PathBuf::from("/home/testuser")); + + // Non-existing user should return None + let user_info = verifier.get_user("nonexistent").await; + assert!(user_info.is_none()); + } + + #[tokio::test] + async fn test_auth_provider_trait() { + let hash = hash_password("password").unwrap(); + let users = vec![UserDefinition { + name: "testuser".to_string(), + password_hash: hash, + shell: None, + home: None, + env: HashMap::new(), + }]; + + let config = PasswordAuthConfig::with_users(users); + let verifier = PasswordVerifier::new(config).await.unwrap(); + + // Test verify_password via AuthProvider trait + let result = verifier + .verify_password("testuser", "password") + .await + .unwrap(); + assert!(result.is_accepted()); + + let result = verifier.verify_password("testuser", "wrong").await.unwrap(); + assert!(result.is_rejected()); + + // Test get_user_info via AuthProvider trait + let info = verifier.get_user_info("testuser").await.unwrap(); + assert!(info.is_some()); + + // Test user_exists via AuthProvider trait + let exists = verifier.user_exists("testuser").await.unwrap(); + assert!(exists); + + let exists = verifier.user_exists("nonexistent").await.unwrap(); + assert!(!exists); + + // Test verify_publickey (should always reject) + let key_str = + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl"; + let key = russh::keys::parse_public_key_base64(key_str.split_whitespace().nth(1).unwrap()) + .unwrap(); + let result = verifier.verify_publickey("testuser", &key).await.unwrap(); + assert!(result.is_rejected()); + } + + #[tokio::test] + async fn test_password_verifier_reload_users() { + let hash = hash_password("password").unwrap(); + let users = vec![UserDefinition { + name: "user1".to_string(), + password_hash: hash.clone(), + shell: None, + home: None, + env: HashMap::new(), + }]; + + let config = PasswordAuthConfig::with_users(users); + let verifier = PasswordVerifier::new(config).await.unwrap(); + + // Initial user should exist + assert!(verifier.user_exists_internal("user1").await); + assert!(!verifier.user_exists_internal("user2").await); + + // Reload should work without error + let result = verifier.reload_users().await; + assert!(result.is_ok()); + } + + #[test] + fn test_empty_password() { + // Empty password should still hash correctly + let hash = hash_password("").unwrap(); + assert!(hash.starts_with("$argon2id$")); + assert!(verify_password_hash("", &hash).unwrap()); + assert!(!verify_password_hash("notempty", &hash).unwrap()); + } + + #[test] + fn test_unicode_password() { + // Unicode passwords should work correctly + let password = "p@ssw\u{00f6}rd\u{1f512}"; + let hash = hash_password(password).unwrap(); + assert!(verify_password_hash(password, &hash).unwrap()); + assert!(!verify_password_hash("password", &hash).unwrap()); + } + + #[test] + fn test_long_password() { + // Long passwords should work correctly + let password = "a".repeat(1000); + let hash = hash_password(&password).unwrap(); + assert!(verify_password_hash(&password, &hash).unwrap()); + assert!(!verify_password_hash(&"a".repeat(999), &hash).unwrap()); + } +} diff --git a/src/server/config/mod.rs b/src/server/config/mod.rs index f75bbce2..e5d36d15 100644 --- a/src/server/config/mod.rs +++ b/src/server/config/mod.rs @@ -71,7 +71,9 @@ pub use loader::{generate_config_template, load_config}; pub use types::*; // Re-export the original config types for backward compatibility -use super::auth::{AuthProvider, PublicKeyAuthConfig, PublicKeyVerifier}; +use super::auth::{ + AuthProvider, CompositeAuthProvider, PasswordAuthConfig, PublicKeyAuthConfig, PublicKeyVerifier, +}; use super::exec::ExecConfig; use serde::{Deserialize, Serialize}; use std::path::PathBuf; @@ -140,6 +142,10 @@ pub struct ServerConfig { #[serde(default)] pub publickey_auth: PublicKeyAuthConfigSerde, + /// Configuration for password authentication. + #[serde(default)] + pub password_auth: PasswordAuthConfigSerde, + /// Configuration for command execution. #[serde(default)] pub exec: ExecConfig, @@ -167,6 +173,26 @@ impl From for PublicKeyAuthConfig { } } +/// Serializable configuration for password authentication. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct PasswordAuthConfigSerde { + /// Path to YAML file containing user definitions. + pub users_file: Option, + + /// Inline user definitions. + #[serde(default)] + pub users: Vec, +} + +impl From for PasswordAuthConfig { + fn from(serde_config: PasswordAuthConfigSerde) -> Self { + PasswordAuthConfig { + users_file: serde_config.users_file, + users: serde_config.users, + } + } +} + fn default_listen_address() -> String { "0.0.0.0:2222".to_string() } @@ -205,6 +231,7 @@ impl Default for ServerConfig { allow_keyboard_interactive: false, banner: None, publickey_auth: PublicKeyAuthConfigSerde::default(), + password_auth: PasswordAuthConfigSerde::default(), exec: ExecConfig::default(), } } @@ -248,9 +275,74 @@ impl ServerConfig { } /// Create an auth provider based on the configuration. + /// + /// This creates a composite auth provider that supports: + /// - Public key authentication (if `allow_publickey_auth` is true) + /// - Password authentication (if `allow_password_auth` is true) + /// + /// The provider is created synchronously using a blocking runtime. + /// For async creation, use `create_auth_provider_async`. pub fn create_auth_provider(&self) -> Arc { - let config: PublicKeyAuthConfig = self.publickey_auth.clone().into(); - Arc::new(PublicKeyVerifier::new(config)) + // If neither method is enabled, return a public key only provider (will reject all) + if !self.allow_publickey_auth && !self.allow_password_auth { + let config: PublicKeyAuthConfig = self.publickey_auth.clone().into(); + return Arc::new(PublicKeyVerifier::new(config)); + } + + // If only public key auth is enabled, use simple provider + if self.allow_publickey_auth && !self.allow_password_auth { + let config: PublicKeyAuthConfig = self.publickey_auth.clone().into(); + return Arc::new(PublicKeyVerifier::new(config)); + } + + // If password auth is enabled, we need to create a composite provider + // Use blocking call since this is called during server startup + let publickey_config = if self.allow_publickey_auth { + Some(self.publickey_auth.clone().into()) + } else { + None + }; + + let password_config = if self.allow_password_auth { + Some(self.password_auth.clone().into()) + } else { + None + }; + + // Create the composite provider using blocking runtime + // This is safe because this is only called during server initialization + match tokio::runtime::Handle::try_current() { + Ok(handle) => { + match tokio::task::block_in_place(|| { + handle.block_on(CompositeAuthProvider::new( + publickey_config, + password_config, + )) + }) { + Ok(provider) => Arc::new(provider), + Err(e) => { + tracing::error!(error = %e, "Failed to create composite auth provider, falling back to publickey only"); + let config: PublicKeyAuthConfig = self.publickey_auth.clone().into(); + Arc::new(PublicKeyVerifier::new(config)) + } + } + } + Err(_) => { + // No async runtime, create a new one + let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + match rt.block_on(CompositeAuthProvider::new( + publickey_config, + password_config, + )) { + Ok(provider) => Arc::new(provider), + Err(e) => { + tracing::error!(error = %e, "Failed to create composite auth provider, falling back to publickey only"); + let config: PublicKeyAuthConfig = self.publickey_auth.clone().into(); + Arc::new(PublicKeyVerifier::new(config)) + } + } + } + } } } @@ -341,6 +433,18 @@ impl ServerConfigBuilder { self } + /// Set the password users file path. + pub fn password_users_file(mut self, path: impl Into) -> Self { + self.config.password_auth.users_file = Some(path.into()); + self + } + + /// Set inline password users. + pub fn password_users(mut self, users: Vec) -> Self { + self.config.password_auth.users = users; + self + } + /// Set the exec configuration. pub fn exec(mut self, exec_config: ExecConfig) -> Self { self.config.exec = exec_config; @@ -405,6 +509,10 @@ impl ServerFileConfig { authorized_keys_dir: self.auth.publickey.authorized_keys_dir, authorized_keys_pattern: self.auth.publickey.authorized_keys_pattern, }, + password_auth: PasswordAuthConfigSerde { + users_file: self.auth.password.users_file, + users: self.auth.password.users, + }, exec: ExecConfig { default_shell: self.shell.default, timeout_secs: self.shell.command_timeout, diff --git a/src/server/handler.rs b/src/server/handler.rs index 09ed068d..e87d63cd 100644 --- a/src/server/handler.rs +++ b/src/server/handler.rs @@ -26,6 +26,7 @@ use russh::keys::ssh_key; use russh::server::{Auth, Msg, Session}; use russh::{Channel, ChannelId, MethodKind, MethodSet, Pty}; use tokio::sync::RwLock; +use zeroize::Zeroizing; use super::auth::AuthProvider; use super::config::ServerConfig; @@ -392,11 +393,12 @@ impl russh::server::Handler for SshHandler { /// Handle password authentication. /// - /// Placeholder implementation - will be implemented in a future issue. + /// Verifies the password against configured users using the auth provider. + /// Implements rate limiting and tracks failed authentication attempts. fn auth_password( &mut self, user: &str, - _password: &str, + password: &str, ) -> impl std::future::Future> + Send { tracing::debug!( user = %user, @@ -414,27 +416,132 @@ impl russh::server::Handler for SshHandler { let mut methods = self.allowed_methods(); methods.remove(MethodKind::Password); + // Clone what we need for the async block + let auth_provider = Arc::clone(&self.auth_provider); + let rate_limiter = self.rate_limiter.clone(); + let peer_addr = self.peer_addr; + let user = user.to_string(); + // Use Zeroizing to ensure password is securely cleared from memory when dropped + let password = Zeroizing::new(password.to_string()); + let allow_password = self.config.allow_password_auth; + + // Get mutable reference to session_info for authentication update + let session_info = &mut self.session_info; + async move { + // Check if password auth is enabled + if !allow_password { + tracing::debug!( + user = %user, + "Password authentication disabled" + ); + let proceed = if methods.is_empty() { + None + } else { + Some(methods) + }; + return Ok(Auth::Reject { + proceed_with_methods: proceed, + partial_success: false, + }); + } + if exceeded { - tracing::warn!("Max authentication attempts exceeded"); + tracing::warn!( + user = %user, + peer = ?peer_addr, + "Max authentication attempts exceeded" + ); return Ok(Auth::Reject { proceed_with_methods: None, partial_success: false, }); } - // Placeholder - reject but allow other methods - // Will be implemented in #127 - let proceed = if methods.is_empty() { - None - } else { - Some(methods) - }; + // Check rate limiting based on peer address + let rate_key = peer_addr + .map(|addr| addr.ip().to_string()) + .unwrap_or_else(|| "unknown".to_string()); - Ok(Auth::Reject { - proceed_with_methods: proceed, - partial_success: false, - }) + if rate_limiter.is_rate_limited(&rate_key).await { + tracing::warn!( + user = %user, + peer = ?peer_addr, + "Rate limited password authentication attempt" + ); + return Ok(Auth::Reject { + proceed_with_methods: None, + partial_success: false, + }); + } + + // Try to acquire a rate limit token + if rate_limiter.try_acquire(&rate_key).await.is_err() { + tracing::warn!( + user = %user, + peer = ?peer_addr, + "Rate limit exceeded for password authentication" + ); + return Ok(Auth::Reject { + proceed_with_methods: None, + partial_success: false, + }); + } + + // Verify password using auth provider + match auth_provider.verify_password(&user, &password).await { + Ok(result) if result.is_accepted() => { + tracing::info!( + user = %user, + peer = ?peer_addr, + "Password authentication successful" + ); + + // Mark session as authenticated + if let Some(ref mut info) = session_info { + info.authenticate(&user); + } + + Ok(Auth::Accept) + } + Ok(_) => { + tracing::debug!( + user = %user, + peer = ?peer_addr, + "Password authentication rejected" + ); + + let proceed = if methods.is_empty() { + None + } else { + Some(methods) + }; + + Ok(Auth::Reject { + proceed_with_methods: proceed, + partial_success: false, + }) + } + Err(e) => { + tracing::error!( + user = %user, + peer = ?peer_addr, + error = %e, + "Error during password verification" + ); + + let proceed = if methods.is_empty() { + None + } else { + Some(methods) + }; + + Ok(Auth::Reject { + proceed_with_methods: proceed, + partial_success: false, + }) + } + } } }