Skip to content

Commit cae16e1

Browse files
committed
feat: Implement password authentication for server (#127)
- Add Argon2id password hashing with secure memory cleanup (zeroize) - Implement timing attack mitigation with constant-time verification - Support users from YAML file and inline configuration - Add bcrypt compatibility for backward compatibility - Integrate password auth with SSH handler and rate limiting - Add CompositeAuthProvider for combined pubkey/password auth - Update hash-password CLI command to use Argon2id - Add comprehensive tests for password authentication
1 parent 74e6f2e commit cae16e1

8 files changed

Lines changed: 1345 additions & 20 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ shell-words = "1.1.1"
5555
libc = "0.2"
5656
ipnetwork = "0.20"
5757
bcrypt = "0.16"
58+
argon2 = "0.5"
5859
rand = "0.8"
5960
ssh-key = { version = "0.6", features = ["std"] }
6061

src/bin/bssh_server.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ fn gen_config(output: Option<PathBuf>) -> Result<()> {
257257

258258
/// Hash a password for configuration
259259
async fn hash_password() -> Result<()> {
260+
use bssh::server::auth::hash_password as generate_hash;
260261
use rpassword::read_password;
261262

262263
print!("Enter password: ");
@@ -269,7 +270,7 @@ async fn hash_password() -> Result<()> {
269270

270271
// Warn about weak passwords (but still allow them)
271272
if password.len() < 8 {
272-
println!("\n Warning: Password is shorter than 8 characters.");
273+
println!("\n Warning: Password is shorter than 8 characters.");
273274
println!(" This is considered weak and may be easily compromised.");
274275
println!(" Consider using a longer password for better security.\n");
275276
}
@@ -282,8 +283,8 @@ async fn hash_password() -> Result<()> {
282283
anyhow::bail!("Passwords do not match");
283284
}
284285

285-
// Use bcrypt for password hashing (cost factor 12)
286-
let hash = bcrypt::hash(&password, 12).context("Failed to hash password")?;
286+
// Use Argon2id for password hashing (recommended algorithm)
287+
let hash = generate_hash(&password).context("Failed to hash password")?;
287288

288289
println!("\nPassword hash (use in configuration):");
289290
println!("{}", hash);
@@ -295,6 +296,8 @@ async fn hash_password() -> Result<()> {
295296
println!(" users:");
296297
println!(" - name: username");
297298
println!(" password_hash: \"{}\"", hash);
299+
println!("\nNote: This hash uses Argon2id algorithm (recommended).");
300+
println!(" bcrypt hashes are also supported for compatibility.");
298301

299302
Ok(())
300303
}

src/server/auth/composite.rs

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
// Copyright 2025 Lablup Inc. and Jeongkyu Shin
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//! Composite authentication provider.
16+
//!
17+
//! This module provides a composite authentication provider that combines
18+
//! multiple authentication methods (public key and password authentication).
19+
20+
use std::sync::Arc;
21+
22+
use anyhow::Result;
23+
use async_trait::async_trait;
24+
use russh::keys::ssh_key::PublicKey;
25+
26+
use super::password::{PasswordAuthConfig, PasswordVerifier};
27+
use super::provider::AuthProvider;
28+
use super::publickey::{PublicKeyAuthConfig, PublicKeyVerifier};
29+
use crate::shared::auth_types::{AuthResult, UserInfo};
30+
31+
/// Composite authentication provider that supports multiple auth methods.
32+
///
33+
/// This provider delegates to specific providers based on the authentication
34+
/// method being used. It supports:
35+
///
36+
/// - Public key authentication via [`PublicKeyVerifier`]
37+
/// - Password authentication via [`PasswordVerifier`]
38+
///
39+
/// # Example
40+
///
41+
/// ```no_run
42+
/// use bssh::server::auth::CompositeAuthProvider;
43+
/// use bssh::server::auth::PublicKeyAuthConfig;
44+
/// use bssh::server::auth::PasswordAuthConfig;
45+
///
46+
/// # async fn example() -> anyhow::Result<()> {
47+
/// let pubkey_config = PublicKeyAuthConfig::with_directory("/etc/bssh/authorized_keys");
48+
/// let password_config = PasswordAuthConfig::default();
49+
///
50+
/// let provider = CompositeAuthProvider::new(
51+
/// Some(pubkey_config),
52+
/// Some(password_config),
53+
/// ).await?;
54+
/// # Ok(())
55+
/// # }
56+
/// ```
57+
pub struct CompositeAuthProvider {
58+
/// Public key verifier (if public key auth is enabled).
59+
publickey_verifier: Option<PublicKeyVerifier>,
60+
61+
/// Password verifier (if password auth is enabled).
62+
password_verifier: Option<Arc<PasswordVerifier>>,
63+
}
64+
65+
impl CompositeAuthProvider {
66+
/// Create a new composite auth provider.
67+
///
68+
/// # Arguments
69+
///
70+
/// * `publickey_config` - Configuration for public key authentication (None to disable)
71+
/// * `password_config` - Configuration for password authentication (None to disable)
72+
///
73+
/// # Returns
74+
///
75+
/// A new composite auth provider, or an error if initialization fails.
76+
pub async fn new(
77+
publickey_config: Option<PublicKeyAuthConfig>,
78+
password_config: Option<PasswordAuthConfig>,
79+
) -> Result<Self> {
80+
let publickey_verifier = publickey_config.map(PublicKeyVerifier::new);
81+
82+
let password_verifier = match password_config {
83+
Some(config) => Some(Arc::new(PasswordVerifier::new(config).await?)),
84+
None => None,
85+
};
86+
87+
tracing::info!(
88+
publickey_enabled = publickey_verifier.is_some(),
89+
password_enabled = password_verifier.is_some(),
90+
"Composite auth provider initialized"
91+
);
92+
93+
Ok(Self {
94+
publickey_verifier,
95+
password_verifier,
96+
})
97+
}
98+
99+
/// Create a provider with only public key authentication.
100+
pub fn publickey_only(config: PublicKeyAuthConfig) -> Self {
101+
Self {
102+
publickey_verifier: Some(PublicKeyVerifier::new(config)),
103+
password_verifier: None,
104+
}
105+
}
106+
107+
/// Create a provider with only password authentication.
108+
pub async fn password_only(config: PasswordAuthConfig) -> Result<Self> {
109+
Ok(Self {
110+
publickey_verifier: None,
111+
password_verifier: Some(Arc::new(PasswordVerifier::new(config).await?)),
112+
})
113+
}
114+
115+
/// Check if public key authentication is enabled.
116+
pub fn publickey_enabled(&self) -> bool {
117+
self.publickey_verifier.is_some()
118+
}
119+
120+
/// Check if password authentication is enabled.
121+
pub fn password_enabled(&self) -> bool {
122+
self.password_verifier.is_some()
123+
}
124+
125+
/// Get a reference to the password verifier (if enabled).
126+
pub fn password_verifier(&self) -> Option<&Arc<PasswordVerifier>> {
127+
self.password_verifier.as_ref()
128+
}
129+
130+
/// Reload password users from configuration.
131+
///
132+
/// This allows hot-reloading of user configuration without restarting the server.
133+
pub async fn reload_password_users(&self) -> Result<()> {
134+
if let Some(ref verifier) = self.password_verifier {
135+
verifier.reload_users().await?;
136+
}
137+
Ok(())
138+
}
139+
}
140+
141+
#[async_trait]
142+
impl AuthProvider for CompositeAuthProvider {
143+
async fn verify_publickey(&self, username: &str, key: &PublicKey) -> Result<AuthResult> {
144+
if let Some(ref verifier) = self.publickey_verifier {
145+
verifier.verify_publickey(username, key).await
146+
} else {
147+
// Public key auth not enabled
148+
Ok(AuthResult::Reject)
149+
}
150+
}
151+
152+
async fn verify_password(&self, username: &str, password: &str) -> Result<AuthResult> {
153+
if let Some(ref verifier) = self.password_verifier {
154+
verifier.verify_password(username, password).await
155+
} else {
156+
// Password auth not enabled
157+
Ok(AuthResult::Reject)
158+
}
159+
}
160+
161+
async fn get_user_info(&self, username: &str) -> Result<Option<UserInfo>> {
162+
// Try to get user info from password verifier first (has more detailed info)
163+
if let Some(ref verifier) = self.password_verifier {
164+
if let Some(info) = verifier.get_user_info(username).await? {
165+
return Ok(Some(info));
166+
}
167+
}
168+
169+
// Fall back to public key verifier
170+
if let Some(ref verifier) = self.publickey_verifier {
171+
return verifier.get_user_info(username).await;
172+
}
173+
174+
Ok(None)
175+
}
176+
177+
async fn user_exists(&self, username: &str) -> Result<bool> {
178+
// Check password verifier first
179+
if let Some(ref verifier) = self.password_verifier {
180+
if verifier.user_exists(username).await? {
181+
return Ok(true);
182+
}
183+
}
184+
185+
// Check public key verifier
186+
if let Some(ref verifier) = self.publickey_verifier {
187+
if verifier.user_exists(username).await? {
188+
return Ok(true);
189+
}
190+
}
191+
192+
Ok(false)
193+
}
194+
}
195+
196+
#[cfg(test)]
197+
mod tests {
198+
use super::*;
199+
use crate::server::auth::hash_password;
200+
use crate::server::config::UserDefinition;
201+
use std::collections::HashMap;
202+
203+
#[tokio::test]
204+
async fn test_composite_provider_publickey_only() {
205+
let config = PublicKeyAuthConfig::with_directory("/tmp/nonexistent");
206+
let provider = CompositeAuthProvider::publickey_only(config);
207+
208+
assert!(provider.publickey_enabled());
209+
assert!(!provider.password_enabled());
210+
}
211+
212+
#[tokio::test]
213+
async fn test_composite_provider_password_only() {
214+
let hash = hash_password("password").unwrap();
215+
let users = vec![UserDefinition {
216+
name: "testuser".to_string(),
217+
password_hash: hash,
218+
shell: None,
219+
home: None,
220+
env: HashMap::new(),
221+
}];
222+
223+
let config = PasswordAuthConfig::with_users(users);
224+
let provider = CompositeAuthProvider::password_only(config).await.unwrap();
225+
226+
assert!(!provider.publickey_enabled());
227+
assert!(provider.password_enabled());
228+
229+
// Test password verification
230+
let result = provider
231+
.verify_password("testuser", "password")
232+
.await
233+
.unwrap();
234+
assert!(result.is_accepted());
235+
236+
let result = provider.verify_password("testuser", "wrong").await.unwrap();
237+
assert!(result.is_rejected());
238+
}
239+
240+
#[tokio::test]
241+
async fn test_composite_provider_both() {
242+
let pubkey_config = PublicKeyAuthConfig::with_directory("/tmp/nonexistent");
243+
let hash = hash_password("password").unwrap();
244+
let users = vec![UserDefinition {
245+
name: "testuser".to_string(),
246+
password_hash: hash,
247+
shell: None,
248+
home: None,
249+
env: HashMap::new(),
250+
}];
251+
let password_config = PasswordAuthConfig::with_users(users);
252+
253+
let provider = CompositeAuthProvider::new(Some(pubkey_config), Some(password_config))
254+
.await
255+
.unwrap();
256+
257+
assert!(provider.publickey_enabled());
258+
assert!(provider.password_enabled());
259+
}
260+
261+
#[tokio::test]
262+
async fn test_composite_provider_user_info() {
263+
let hash = hash_password("password").unwrap();
264+
let users = vec![UserDefinition {
265+
name: "testuser".to_string(),
266+
password_hash: hash,
267+
shell: Some("/bin/bash".into()),
268+
home: Some("/home/testuser".into()),
269+
env: HashMap::new(),
270+
}];
271+
272+
let config = PasswordAuthConfig::with_users(users);
273+
let provider = CompositeAuthProvider::password_only(config).await.unwrap();
274+
275+
let info = provider.get_user_info("testuser").await.unwrap();
276+
assert!(info.is_some());
277+
let info = info.unwrap();
278+
assert_eq!(info.username, "testuser");
279+
assert_eq!(info.shell.to_str().unwrap(), "/bin/bash");
280+
assert_eq!(info.home_dir.to_str().unwrap(), "/home/testuser");
281+
}
282+
283+
#[tokio::test]
284+
async fn test_composite_provider_user_exists() {
285+
let hash = hash_password("password").unwrap();
286+
let users = vec![UserDefinition {
287+
name: "existinguser".to_string(),
288+
password_hash: hash,
289+
shell: None,
290+
home: None,
291+
env: HashMap::new(),
292+
}];
293+
294+
let config = PasswordAuthConfig::with_users(users);
295+
let provider = CompositeAuthProvider::password_only(config).await.unwrap();
296+
297+
assert!(provider.user_exists("existinguser").await.unwrap());
298+
assert!(!provider.user_exists("nonexistent").await.unwrap());
299+
}
300+
301+
#[tokio::test]
302+
async fn test_composite_provider_disabled_methods() {
303+
let pubkey_config = PublicKeyAuthConfig::with_directory("/tmp/nonexistent");
304+
let provider = CompositeAuthProvider::publickey_only(pubkey_config);
305+
306+
// Password auth should reject when disabled
307+
let result = provider.verify_password("user", "pass").await.unwrap();
308+
assert!(result.is_rejected());
309+
}
310+
}

src/server/auth/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,13 @@
4646
//! // verifier.verify("username", &public_key).await
4747
//! ```
4848
49+
pub mod composite;
50+
pub mod password;
4951
pub mod provider;
5052
pub mod publickey;
5153

54+
pub use composite::CompositeAuthProvider;
55+
pub use password::{hash_password, verify_password_hash, PasswordAuthConfig, PasswordVerifier};
5256
pub use provider::AuthProvider;
5357
pub use publickey::{AuthKeyOptions, AuthorizedKey, PublicKeyAuthConfig, PublicKeyVerifier};
5458

0 commit comments

Comments
 (0)