From 1efe2ad5c262c0b8fa36914c2b807a0bfb27f06c Mon Sep 17 00:00:00 2001 From: RafayAhmad7548 Date: Thu, 10 Jul 2025 08:08:36 +0500 Subject: [PATCH] some more config optinos and password auth with bcrypt hashing --- Cargo.lock | 60 ++++++++++++++++++++++++++++++++--- Cargo.toml | 1 + src/config.rs | 30 ++++++++++++++++-- src/main.rs | 88 ++++++++++++++++++++++++++++++++------------------- 4 files changed, 138 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e07aea6..d28e23a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -155,6 +155,19 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +[[package]] +name = "bcrypt" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92758ad6077e4c76a6cadbce5005f666df70d4f13b19976b1a8062eef880040f" +dependencies = [ + "base64", + "blowfish", + "getrandom 0.3.3", + "subtle", + "zeroize", +] + [[package]] name = "bcrypt-pbkdf" version = "0.10.0" @@ -325,7 +338,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom", + "getrandom 0.2.16", "once_cell", "tiny-keccak", ] @@ -668,6 +681,7 @@ dependencies = [ name = "flux-sftp" version = "0.1.0" dependencies = [ + "bcrypt", "chrono", "regex", "russh", @@ -813,10 +827,22 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + [[package]] name = "ghash" version = "0.5.1" @@ -1202,7 +1228,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] @@ -1550,6 +1576,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.8.5" @@ -1577,7 +1609,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.16", ] [[package]] @@ -1676,7 +1708,7 @@ dependencies = [ "flate2", "futures", "generic-array", - "getrandom", + "getrandom 0.2.16", "hex-literal", "hmac", "home", @@ -2518,6 +2550,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasite" version = "0.1.0" @@ -2917,6 +2958,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + [[package]] name = "writeable" version = "0.6.1" diff --git a/Cargo.toml b/Cargo.toml index 901eeb0..13147be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,4 @@ tokio = { version = "1.45.1", features = ["full"] } sqlx = { version = "0.8.6", features = [ "runtime-tokio", "postgres", "sqlite", "mysql" ] } toml = "0.8.23" serde = "1.0.219" +bcrypt = "0.17.0" diff --git a/src/config.rs b/src/config.rs index 38c0088..811d9c7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -13,9 +13,17 @@ pub(crate) struct GeneralConfig { pub(crate) jail_dir: String } +#[derive(Serialize, Deserialize, Clone)] +pub(crate) struct DBConfig { + #[serde(flatten)] + pub(crate) driver: DriverConfig, + #[serde(flatten)] + pub(crate) common: CommonConfig +} + #[derive(Serialize, Deserialize, Clone)] #[serde(tag = "driver")] -pub(crate) enum DBConfig { +pub(crate) enum DriverConfig { #[serde(rename = "sqlite")] Sqlite { path: String @@ -38,6 +46,14 @@ pub(crate) enum DBConfig { } } +#[derive(Serialize, Deserialize, Clone)] +pub(crate) struct CommonConfig { + pub(crate) table: String, + pub(crate) username_field: String, + pub(crate) public_key_field: Option, + pub(crate) password_field: Option +} + impl Default for Config { fn default() -> Self { @@ -47,8 +63,16 @@ impl Default for Config { port: 2222, jail_dir: String::from("/srv/sftp") }, - database: DBConfig::Sqlite { - path: String::from("/var/lib/flux-sftp/auth.db") + database: DBConfig { + driver: DriverConfig::Sqlite { + path: String::from("/var/lib/flux-sftp/auth.db") + }, + common: CommonConfig { + table: String::from("users"), + username_field: String::from("username"), + public_key_field: Some(String::from("public_key")), + password_field: None + } } } } diff --git a/src/main.rs b/src/main.rs index 1953a9c..85d5d45 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,22 @@ mod sftp; mod config; -use std::{io::ErrorKind, net::SocketAddr, sync::Arc, time::Duration}; -use config::{Config, DBConfig}; +use std::{fmt::format, io::ErrorKind, net::SocketAddr, sync::Arc, time::Duration}; +use bcrypt::{hash, DEFAULT_COST}; +use config::{Config, DriverConfig}; use russh::{keys::ssh_key::{rand_core::OsRng, PublicKey}, server::{Auth, Handler as SshHandler, Msg, Server, Session}, Channel, ChannelId}; use sftp::SftpSession; use sqlx::{mysql::MySqlPoolOptions, postgres::PgPoolOptions, sqlite::SqlitePoolOptions, MySql, Pool, Postgres, Row, Sqlite}; use tokio::fs; -macro_rules! fetch_pub_key { - ($pool:ident, $query:literal, $user:ident) => { +macro_rules! fetch_col { + ($col:ident, $pool:ident, $query:expr, $user:ident) => { { - let row_res = sqlx::query($query) + let row_res = sqlx::query(&$query) .bind($user) .fetch_one($pool).await; match row_res { - Ok(row) => Some(row.get("public_key")), + Ok(row) => Some(row.get($col as &str)), Err(_) => None } } @@ -47,35 +48,53 @@ struct SshSession { impl SshHandler for SshSession { type Error = russh::Error; + async fn auth_password( + &mut self, + user: &str, + password: &str, + ) -> Result { + if let Some(password_field) = &self.config.database.common.password_field { + self.user = Some(user.to_string()); + let offered_hash = match hash(password, DEFAULT_COST) { + Ok(hash) => hash, + Err(_) => return Ok(Auth::reject()) + }; + + let query = format!("SELECT {} FROM {} WHERE {} = ?", password_field, self.config.database.common.table, self.config.database.common.username_field); + let stored_password: String = match &*self.pool { + DBPool::Sqlite(pool) => fetch_col!(password_field, pool, query, user), + DBPool::Postgres(pool) => fetch_col!(password_field, pool, query.replace("?", "$1"), user), + DBPool::Mysql(pool) => fetch_col!(password_field, pool, query, user) + }.unwrap_or_default(); + + if offered_hash == stored_password { Ok(Auth::Accept) } else { Ok(Auth::reject()) } + } + else { + Ok(Auth::reject()) + } + } + async fn auth_publickey_offered( &mut self, user: &str, public_key: &PublicKey, ) -> Result { - self.user = Some(user.to_string()); + if let Some(public_key_field) = &self.config.database.common.public_key_field { + self.user = Some(user.to_string()); + let offered_key = public_key.to_string(); - let offered_key = public_key.to_string(); - - let stored_key_opt: Option = match &*self.pool { - DBPool::Sqlite(pool) => fetch_pub_key!(pool, "SELECT * FROM users WHERE username = ?", user), - DBPool::Postgres(pool) => fetch_pub_key!(pool, "SELECT * FROM users WHERE username = $1", user), - DBPool::Mysql(pool) => fetch_pub_key!(pool, "SELECT * FROM users WHERE username = ?", user) - }; - - if let Some(stored_key) = stored_key_opt { - if stored_key == offered_key { - Ok(Auth::Accept) - } - else { - println!("invalid key"); - Ok(Auth::reject()) - } + let query = format!("SELECT {} FROM {} WHERE {} = ?", public_key_field, self.config.database.common.table, self.config.database.common.username_field); + let stored_key: String = match &*self.pool { + DBPool::Sqlite(pool) => fetch_col!(public_key_field, pool, query, user), + DBPool::Postgres(pool) => fetch_col!(public_key_field, pool, query.replace("?", "$1"), user), + DBPool::Mysql(pool) => fetch_col!(public_key_field, pool, query, user) + }.unwrap_or_default(); + + if offered_key == stored_key { Ok(Auth::Accept) } else { Ok(Auth::reject()) } } else { - println!("user not found"); Ok(Auth::reject()) } - } async fn auth_publickey( @@ -132,6 +151,9 @@ enum DBPool { #[tokio::main] async fn main() -> Result<(), sqlx::Error> { + // let config = Config::default(); + // let toml = toml::to_string(&config).unwrap(); + // println!("{}", toml); const CONFIG_PATH: &str = "/etc/flux-sftp/config.toml"; let config: Arc; @@ -155,16 +177,16 @@ async fn main() -> Result<(), sqlx::Error> { } } - let url = match &config.database { - DBConfig::Sqlite { path } => format!("sqlite:{}", path), - DBConfig::Postgres { host, port, user, password, dbname } => format!("postgres://{}:{}@{}:{}/{}", user, password, host, port, dbname), - DBConfig::Mysql { host, port, user, password, dbname } => format!("mysql://{}:{}@{}:{}/{}", user, password, host, port, dbname), + let url = match &config.database.driver { + DriverConfig::Sqlite { path } => format!("sqlite:{}", path), + DriverConfig::Postgres { host, port, user, password, dbname } => format!("postgres://{}:{}@{}:{}/{}", user, password, host, port, dbname), + DriverConfig::Mysql { host, port, user, password, dbname } => format!("mysql://{}:{}@{}:{}/{}", user, password, host, port, dbname), }; - let pool = match &config.database { - DBConfig::Sqlite { .. } => DBPool::Sqlite(SqlitePoolOptions::new().max_connections(3).connect(&url).await?), - DBConfig::Postgres { .. } => DBPool::Postgres(PgPoolOptions::new().max_connections(3).connect(&url).await?), - DBConfig::Mysql { .. } => DBPool::Mysql(MySqlPoolOptions::new().max_connections(3).connect(&url).await?) + let pool = match &config.database.driver { + DriverConfig::Sqlite { .. } => DBPool::Sqlite(SqlitePoolOptions::new().max_connections(3).connect(&url).await?), + DriverConfig::Postgres { .. } => DBPool::Postgres(PgPoolOptions::new().max_connections(3).connect(&url).await?), + DriverConfig::Mysql { .. } => DBPool::Mysql(MySqlPoolOptions::new().max_connections(3).connect(&url).await?) }; let mut server = SftpServer { pool: Arc::new(pool), config: config.clone() };