-
Notifications
You must be signed in to change notification settings - Fork 1
Closed
Labels
priority:highHigh priority issueHigh priority issuestatus:doneCompletedCompletedtype:enhancementNew feature or requestNew feature or request
Description
Summary
Implement password authentication verification for bssh-server. This provides an alternative authentication method when public key auth is not available.
Parent Epic
- Implement bssh-server with SFTP/SCP support #123 - bssh-server 추가 구현
- Depends on: Implement public key authentication for server #126 (public key auth - for AuthProvider trait)
Security Considerations
- Passwords must NEVER be stored in plain text
- Use secure password hashing (Argon2id recommended, bcrypt acceptable)
- Implement timing attack mitigation
- Rate limit authentication attempts
- Use
zeroizecrate for password memory cleanup
Implementation Details
1. Password Verifier
// src/server/auth/password.rs
use argon2::{Argon2, PasswordHash, PasswordVerifier as Argon2Verifier};
use zeroize::Zeroizing;
/// Configuration for password authentication
pub struct PasswordAuthConfig {
/// Path to users file (YAML format)
pub users_file: Option<PathBuf>,
/// Inline user definitions (for container environments)
pub users: Vec<UserDefinition>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct UserDefinition {
pub name: String,
/// Password hash (Argon2id or bcrypt format)
pub password_hash: String,
pub shell: Option<PathBuf>,
pub home: Option<PathBuf>,
pub env: Option<HashMap<String, String>>,
}
pub struct PasswordVerifier {
config: PasswordAuthConfig,
users: RwLock<HashMap<String, UserDefinition>>,
}
impl PasswordVerifier {
pub fn new(config: PasswordAuthConfig) -> Result<Self> {
let verifier = Self {
config,
users: RwLock::new(HashMap::new()),
};
// Load users on init
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(verifier.reload_users())
})?;
Ok(verifier)
}
/// Reload users from configuration
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 {
let content = tokio::fs::read_to_string(path).await?;
let file_users: UsersFile = serde_yaml::from_str(&content)?;
for user in file_users.users {
users.insert(user.name.clone(), user);
}
}
// Add inline users (override file users)
for user in &self.config.users {
users.insert(user.name.clone(), user.clone());
}
*self.users.write().await = users;
Ok(())
}
/// Verify password for user
pub async fn verify(&self, username: &str, password: &str) -> Result<bool> {
// Wrap password in Zeroizing for secure cleanup
let password = Zeroizing::new(password.to_string());
// Timing attack mitigation: always do work even for non-existent users
let start = std::time::Instant::now();
let min_time = Duration::from_millis(100);
let result = self.verify_internal(username, &password).await;
// Normalize timing
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 users = self.users.read().await;
let user = match users.get(username) {
Some(u) => u,
None => {
// Do dummy hash verification to prevent timing attacks
let _ = self.verify_dummy_hash(password);
return Ok(false);
}
};
// Parse and verify password hash
let hash = PasswordHash::new(&user.password_hash)
.map_err(|e| anyhow::anyhow!("Invalid password hash format: {}", e))?;
let argon2 = Argon2::default();
match argon2.verify_password(password.as_bytes(), &hash) {
Ok(()) => {
tracing::info!("Password authentication successful for {}", username);
Ok(true)
}
Err(_) => {
tracing::debug!("Password authentication failed for {}", username);
Ok(false)
}
}
}
/// Dummy hash verification for timing attack mitigation
fn verify_dummy_hash(&self, password: &Zeroizing<String>) -> bool {
// Pre-computed dummy hash to verify against
const DUMMY_HASH: &str = "$argon2id$v=19$m=19456,t=2,p=1$dummy$dummyhash";
let hash = PasswordHash::new(DUMMY_HASH).unwrap();
let argon2 = Argon2::default();
argon2.verify_password(password.as_bytes(), &hash).is_ok()
}
/// Get user info after successful auth
pub async fn get_user_info(&self, username: &str) -> Option<UserInfo> {
let users = self.users.read().await;
users.get(username).map(|u| UserInfo {
username: u.name.clone(),
home_dir: u.home.clone().unwrap_or_else(|| PathBuf::from("/tmp")),
shell: u.shell.clone().unwrap_or_else(|| PathBuf::from("/bin/sh")),
uid: None,
gid: None,
env: u.env.clone().unwrap_or_default(),
})
}
}
#[derive(Deserialize)]
struct UsersFile {
users: Vec<UserDefinition>,
}2. Password Hash Generation Utility
// src/server/auth/password.rs (additional)
/// Generate a password hash for configuration files
pub fn hash_password(password: &str) -> Result<String> {
use argon2::{
password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
Argon2,
};
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let hash = argon2
.hash_password(password.as_bytes(), &salt)
.map_err(|e| anyhow::anyhow!("Failed to hash password: {}", e))?;
Ok(hash.to_string())
}
/// CLI subcommand for password hashing
pub fn hash_password_cli() -> Result<()> {
use rpassword::prompt_password;
let password = prompt_password("Enter password to hash: ")?;
let confirm = prompt_password("Confirm password: ")?;
if password != confirm {
anyhow::bail!("Passwords do not match");
}
let hash = hash_password(&password)?;
println!("{}", hash);
Ok(())
}3. Integrate with SSH Handler
// Update src/server/handler.rs
impl Handler for SshHandler {
async fn auth_password(
&mut self,
user: &str,
password: &str,
) -> Result<Auth, Self::Error> {
// Check if password auth is enabled
if !self.config.auth.methods.contains(&AuthMethod::Password) {
return Ok(Auth::Reject { proceed_with_methods: None });
}
// Rate limit check
if let Some(addr) = &self.peer_addr {
self.config.security.rate_limiter.try_acquire(&addr.to_string()).await
.map_err(|_| {
tracing::warn!("Rate limited password auth attempt from {}", addr);
})?;
}
// Verify password
match self.config.auth_provider.verify_password(user, password).await? {
AuthResult::Accept => {
self.user = Some(user.to_string());
tracing::info!("Password auth accepted for user: {}", user);
Ok(Auth::Accept)
}
AuthResult::Reject => {
// Record failed attempt for rate limiting
if let Some(addr) = &self.peer_addr {
self.config.security.record_failed_auth(&addr.to_string()).await;
}
tracing::debug!("Password auth rejected for user: {}", user);
Ok(Auth::Reject { proceed_with_methods: None })
}
_ => Ok(Auth::Reject { proceed_with_methods: None }),
}
}
}4. Configuration
# Server config
auth:
methods:
- publickey
- password
password:
# Option 1: External file
users_file: /etc/bssh/users.yaml
# Option 2: Inline users (useful for containers)
# users:
# - name: admin
# password_hash: "$argon2id$v=19$m=19456,t=2,p=1$..."
# shell: /bin/bash
# home: /home/admin# /etc/bssh/users.yaml
users:
- name: admin
password_hash: "$argon2id$v=19$m=19456,t=2,p=1$randomsalt$hashedpassword"
shell: /bin/bash
home: /home/admin
env:
EDITOR: vim
- name: deploy
password_hash: "$argon2id$v=19$m=19456,t=2,p=1$randomsalt$hashedpassword"
shell: /bin/sh
home: /home/deployDependencies to Add
[dependencies]
argon2 = "0.5" # Password hashingReference Code
src/ssh/auth.rs- Timing attack mitigation patternssrc/shared/validation.rs- Username validation
Files to Create/Modify
| File | Action |
|---|---|
src/server/auth/password.rs |
Create - Password verifier |
src/server/handler.rs |
Modify - Integrate auth_password |
src/server/config/types.rs |
Modify - Add password config |
Cargo.toml |
Modify - Add argon2 dependency |
Testing Requirements
- Unit test: Password hash verification
- Unit test: Timing attack mitigation (verify timing consistency)
- Unit test: Invalid hash format handling
- Unit test: Non-existent user handling (timing-safe)
- Integration test: Authenticate with OpenSSH client using password
# Generate password hash
bssh-server hash-password
# Test authentication
sshpass -p 'testpassword' ssh -p 2222 testuser@localhostAcceptance Criteria
-
PasswordVerifierimplementation with Argon2id - Timing attack mitigation for all code paths
-
zeroizeused for password memory cleanup - Users file loading (YAML format)
- Inline user configuration support
- Password hashing utility for generating hashes
- Rate limiting integration with failed attempts
- Configuration support
- Tests passing
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
priority:highHigh priority issueHigh priority issuestatus:doneCompletedCompletedtype:enhancementNew feature or requestNew feature or request