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;
#[derive(Debug, Clone, Default)]
pub struct PasswordAuthConfig {
pub users_file: Option<PathBuf>,
pub users: Vec<UserDefinition>,
}
impl PasswordAuthConfig {
pub fn with_users_file(path: impl Into<PathBuf>) -> Self {
Self {
users_file: Some(path.into()),
users: vec![],
}
}
pub fn with_users(users: Vec<UserDefinition>) -> Self {
Self {
users_file: None,
users,
}
}
}
pub struct PasswordVerifier {
config: PasswordAuthConfig,
users: RwLock<HashMap<String, UserDefinition>>,
dummy_hash: String,
}
impl PasswordVerifier {
pub async fn new(config: PasswordAuthConfig) -> Result<Self> {
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)
}
pub async fn reload_users(&self) -> Result<()> {
let mut users = HashMap::new();
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()));
}
}
}
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(())
}
pub async fn verify(&self, username: &str, password: &str) -> Result<bool> {
let password = Zeroizing::new(password.to_string());
let start = Instant::now();
let min_time = Duration::from_millis(100);
let result = self.verify_internal(username, &password).await;
let elapsed = start.elapsed();
if elapsed < min_time {
tokio::time::sleep(min_time - elapsed).await;
}
result
}
async fn verify_internal(&self, username: &str, password: &Zeroizing<String>) -> Result<bool> {
let validated_username = match validate_username(username) {
Ok(name) => name,
Err(_) => {
let _ = self.verify_dummy_hash(password);
tracing::debug!(
user = %username,
"Password authentication failed: invalid username"
);
return Ok(false);
}
};
let users: tokio::sync::RwLockReadGuard<'_, HashMap<String, UserDefinition>> =
self.users.read().await;
let user = match users.get(&validated_username) {
Some(u) => u,
None => {
let _ = self.verify_dummy_hash(password);
tracing::debug!(
user = %validated_username,
"Password authentication failed: user not found"
);
return Ok(false);
}
};
let hash_str = &user.password_hash;
let verified = if hash_str.starts_with("$argon2") {
self.verify_argon2(password.as_bytes(), hash_str)?
} else if hash_str.starts_with("$2") {
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)
}
}
fn verify_argon2(&self, password: &[u8], hash_str: &str) -> Result<bool> {
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)),
}
}
fn verify_bcrypt(&self, password: &Zeroizing<String>, hash_str: &str) -> Result<bool> {
match bcrypt::verify(password.as_str(), hash_str) {
Ok(verified) => Ok(verified),
Err(e) => {
tracing::warn!(error = %e, "bcrypt verification error");
Ok(false)
}
}
}
fn verify_dummy_hash(&self, password: &Zeroizing<String>) -> bool {
if let Ok(hash) = PasswordHash::new(&self.dummy_hash) {
let argon2 = Argon2::default();
argon2.verify_password(password.as_bytes(), &hash).is_ok()
} else {
false
}
}
pub async fn get_user(&self, username: &str) -> Option<UserInfo> {
let users: tokio::sync::RwLockReadGuard<'_, HashMap<String, UserDefinition>> =
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
})
}
pub async fn user_exists_internal(&self, username: &str) -> bool {
let users: tokio::sync::RwLockReadGuard<'_, HashMap<String, UserDefinition>> =
self.users.read().await;
users.contains_key(username)
}
}
#[async_trait]
impl AuthProvider for PasswordVerifier {
async fn verify_publickey(&self, _username: &str, _key: &PublicKey) -> Result<AuthResult> {
Ok(AuthResult::Reject)
}
async fn verify_password(&self, username: &str, password: &str) -> Result<AuthResult> {
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<Option<UserInfo>> {
Ok(self.get_user(username).await)
}
async fn user_exists(&self, username: &str) -> Result<bool> {
let start = Instant::now();
let min_time = Duration::from_millis(50);
let exists = self.user_exists_internal(username).await;
let elapsed = start.elapsed();
if elapsed < min_time {
tokio::time::sleep(min_time - elapsed).await;
}
Ok(exists)
}
}
#[derive(Debug, Deserialize)]
struct UsersFile {
users: Vec<UserDefinition>,
}
pub fn hash_password(password: &str) -> Result<String> {
use argon2::password_hash::SaltString;
let salt = SaltString::generate(&mut OsRng);
let params = Params::new(
19456, 2, 1, None, )
.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())
}
pub fn verify_password_hash(password: &str, hash: &str) -> Result<bool> {
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();
assert!(hash.starts_with("$argon2id$"));
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();
assert!(verify_password_hash(password, &hash).unwrap());
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();
assert_ne!(hash1, hash2);
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();
assert!(verifier
.verify("testuser", "correct_password")
.await
.unwrap());
assert!(!verifier.verify("testuser", "wrong_password").await.unwrap());
assert!(!verifier.verify("nonexistent", "password").await.unwrap());
}
#[tokio::test]
async fn test_password_verifier_bcrypt_compatibility() {
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();
assert!(verifier
.verify("bcryptuser", "bcrypt_password")
.await
.unwrap());
assert!(!verifier.verify("bcryptuser", "wrong").await.unwrap());
}
#[tokio::test]
#[ignore = "Timing-based test is flaky in CI; run locally with: cargo test test_password_verifier_timing_attack_mitigation --lib -- --ignored"]
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();
let start = Instant::now();
let _ = verifier.verify("testuser", "wrong_password").await;
let time_existing = start.elapsed();
let start = Instant::now();
let _ = verifier.verify("nonexistent_user", "password").await;
let time_nonexistent = start.elapsed();
assert!(time_existing >= Duration::from_millis(90)); assert!(time_nonexistent >= Duration::from_millis(90));
let diff = time_existing.abs_diff(time_nonexistent);
assert!(
diff < Duration::from_millis(200),
"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();
let result = verifier.verify("../etc/passwd", "password").await;
assert!(result.is_ok());
assert!(!result.unwrap());
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();
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"));
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();
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());
let info = verifier.get_user_info("testuser").await.unwrap();
assert!(info.is_some());
let exists = verifier.user_exists("testuser").await.unwrap();
assert!(exists);
let exists = verifier.user_exists("nonexistent").await.unwrap();
assert!(!exists);
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();
assert!(verifier.user_exists_internal("user1").await);
assert!(!verifier.user_exists_internal("user2").await);
let result = verifier.reload_users().await;
assert!(result.is_ok());
}
#[test]
fn test_empty_password() {
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() {
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() {
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());
}
}