From b98a89073876b57b628c00b272506f9a7b54a96d Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 21 Apr 2025 17:46:22 +0200 Subject: [PATCH] ``` Add user authentication with password hashing - Integrated Argon2 for password hashing and verification - Added bcrypt and thiserror dependencies - Updated database schema with admin and ban flags - Implemented user account creation and login logic - Enhanced error handling for database operations ``` --- Cargo.lock | 66 ++++++++++++++- Cargo.toml | 5 +- db.sqlite | Bin 28672 -> 28672 bytes db.sqlite-journal | Bin 0 -> 4616 bytes migrations/002_create_admin_flag.sql | 2 + migrations/003_create_ban_flag.sql | 5 ++ src/client/mod.rs | 87 ++++++++++++++++++- src/db/mod.rs | 121 ++++++++++++++++++++++++++- src/main.rs | 2 +- 9 files changed, 278 insertions(+), 10 deletions(-) create mode 100644 db.sqlite-journal create mode 100644 migrations/002_create_admin_flag.sql create mode 100644 migrations/003_create_ban_flag.sql diff --git a/Cargo.lock b/Cargo.lock index 58ccc92..48022a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,6 +131,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "atoi" version = "2.0.0" @@ -179,6 +191,19 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bcrypt" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92758ad6077e4c76a6cadbce5005f666df70d4f13b19976b1a8062eef880040f" +dependencies = [ + "base64 0.22.1", + "blowfish", + "getrandom 0.3.1", + "subtle", + "zeroize", +] + [[package]] name = "bitflags" version = "2.6.0" @@ -188,6 +213,15 @@ dependencies = [ "serde", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -197,6 +231,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -250,7 +294,9 @@ name = "chatserver" version = "0.1.0" dependencies = [ "aes-gcm", + "argon2", "base64 0.21.7", + "bcrypt", "chrono", "colog", "crossterm", @@ -261,6 +307,7 @@ dependencies = [ "ratatui", "serde", "sqlx", + "thiserror", "tokio", "toml", "x25519-dalek", @@ -1379,6 +1426,17 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" @@ -2122,18 +2180,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 8a8fe4c..f4e8af5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,4 +18,7 @@ toml = "0.8.19" ratatui = "0.29.0" crossterm = "0.28.1" chrono = "0.4.39" -sqlx = { version = "0.8", features = [ "sqlite", "runtime-tokio", "tls-native-tls" ] } \ No newline at end of file +sqlx = { version = "0.8", features = [ "sqlite", "runtime-tokio", "tls-native-tls" ] } +bcrypt = "0.17.0" +argon2 = "0.5.3" +thiserror = "2.0.12" diff --git a/db.sqlite b/db.sqlite index 54f9924fee6e4c0dd94f35165341aaf8827c53c4..ce0640b8b4906cc876ee5a35ab39450545b5107c 100644 GIT binary patch delta 889 zcmZp8z}WDBae}m<00RR9I}pPF+e95>NdX4Essdi#I0iQE2nN1x{_WfmdF(o%MPr=FG-^bO_Pr=32%`wy`M8VC`C)gD% zpOl!Fmzsi00Z=x+C^fM-KTjddG054&F-XJ6)KpV*@)n*|Ec`zhnKmc!E@I+h=DWkd zKabyr@6KjHfpdKIam?(D+QytYEX>J3-6g3CKywt*auU;x42(>54NP>63>6Gbt&B~q zjEsY)&3C?@Bkm`%mr3)+p0df4=BdYd?n?P&;THIgWzn=t6OYf4`)kwb=c%#y)y1Zh z(F|v7$`Y8^8TCQtGeOOV_!4G=Gr|fZODhA58Iy0uCEoT?)s5_ENek!pVEZ>q_HdoR zM=_!HHE!);iqZcLv+#V)bTeD9CgSV){frF9<}^*7C!fZ}%>RLb{}2B+{tuf46&~>` zu`)9G^p^nJFq|wuY7}xj@3i)J&(u)<~zo)=))TJ0r}|)wC?hDNWn0Dl4PZAl)Om zT%{_{C?YG(Aj{ZT-!rwq(YeUj)jcK0$<(4aQ{ON%wV=YiBBb0a)70H4Fq?&yLA=o) z7}eRSDv9-YZ8OP@$T9LNOD{IcNly3AF%9zZPAYR$@kz}Hbj_&BO$sb6at-qLbM{Mh zEzNh!woD3gD|K^@D$-7O4+sr2@JcoSCP{s9@!Z6s%>3lUE~ zflrZjF3u%~Kx|M|XN<{_+Xxb_gDnto!yZ)g)=pRySOS7 z@dd0CqdUGg+$B`G5vr8w=J){BDGO9<><6mU4~TW@2i9Q)RygH# zVK)!}0T2KI5C8!X009sH0T2Lze8iB4xja<9v_xRN4aag z_i<~}mv}+&=L1d2tl?77i-h-XzA&u7VAHzD~BEX}}+v33X%s`oqpR8=ak6nMN`9)|=(?pw@6LsQh zX{@h%)=W=z^(_%R encrypted, + Err(e) => { + error!("Encryption error: {}", e); + return Ok(()); + } + }; + let message = format!("{}\n", BASE64.encode(&encrypted)); + writer.write_all(message.as_bytes()).await?; + + // Read the password from the client + line.clear(); + reader.read_line(&mut line).await?; + let decoded = BASE64.decode(line.trim().as_bytes())?; + let decrypted = cipher_reader + .decrypt(&nonce_reader, decoded.as_ref()) + .unwrap(); + // verifiy password + let password = String::from_utf8(decrypted)?; + if verify_password(&password, &username).await.is_ok() { + info!("Password verified successfully"); + // Send a success message to the client + let message = format!("Welcome back, {}!", username); + let encrypted = match cipher_writer.encrypt(&nonce_writer, message.as_bytes()) { + Ok(encrypted) => encrypted, + Err(e) => { + error!("Encryption error: {}", e); + return Ok(()); + } + }; + let message = format!("{}\n", BASE64.encode(&encrypted)); + writer.write_all(message.as_bytes()).await?; + } else { + info!("Password verification failed"); + // Send an error message to the client + let message = format!("Invalid password for user {}", username); + let encrypted = match cipher_writer.encrypt(&nonce_writer, message.as_bytes()) { + Ok(encrypted) => encrypted, + Err(e) => { + error!("Encryption error: {}", e); + return Ok(()); + } + }; + let message = format!("{}\n", BASE64.encode(&encrypted)); + writer.write_all(message.as_bytes()).await?; + return Ok(()); + } + } else { + // User does not exist, create a new account + // Send a message to the client + let message = format!("User {} is not registered, input your password", username); + let encrypted = match cipher_writer.encrypt(&nonce_writer, message.as_bytes()) { + Ok(encrypted) => encrypted, + Err(e) => { + error!("Encryption error: {}", e); + return Ok(()); + } + }; + let message = format!("{}\n", BASE64.encode(&encrypted)); + writer.write_all(message.as_bytes()).await?; + // Read the password from the client + line.clear(); + reader.read_line(&mut line).await?; + let decoded = BASE64.decode(line.trim().as_bytes())?; + let decrypted = cipher_reader + .decrypt(&nonce_reader, decoded.as_ref()) + .unwrap(); + let password = String::from_utf8(decrypted)?; + info!("Password received"); + // Hash the password + let password_hash = hash_password(&password).await; + let password_hash = password_hash.as_str(); + info!("Password hashed successfully"); + debug!("Hash: {}", password_hash); + // Create the user in the database + create_user(&username, password_hash).await?; + } // Read task for receiving messages from the client let read_task = tokio::spawn(async move { diff --git a/src/db/mod.rs b/src/db/mod.rs index 5e99f89..5b17473 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,5 +1,21 @@ pub(crate) mod users { - use sqlx::sqlite::SqlitePool; + use argon2::{ + password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, + Argon2, + }; + use log::info; + use sqlx::{sqlite::SqlitePool, Row}; + use thiserror::Error; + + #[derive(Error, Debug)] + pub enum DbError { + #[error("Database error: {0}")] + Database(#[from] sqlx::Error), + #[error("Password hashing error: {0}")] + Hashing(argon2::password_hash::Error), + #[error("User not found")] + UserNotFound, + } pub async fn connect_to_db() -> Result { let pool = SqlitePool::connect("sqlite:./db.sqlite").await?; @@ -12,6 +28,46 @@ pub(crate) mod users { Ok(pool) } + pub async fn get_user_by_username( + username: &str, + ) -> Result, sqlx::Error> { + let pool = create_db_pool().await?; + + let user = sqlx::query( + r#" + SELECT id, username + FROM users + WHERE username = ? + "#, + ) + .bind(username) + .fetch_optional(&pool) + .await?; + + Ok(user.map(|row| (row.get(0), row.get(1)))) + } + + pub async fn check_for_account(username: &str) -> Result { + // Fixed error type + let pool = create_db_pool().await?; + + let exists = sqlx::query( + r#" + SELECT EXISTS( + SELECT 1 + FROM users + WHERE username = ? + ) + "#, + ) + .bind(username) + .fetch_one(&pool) + .await? + .get::(0); + + Ok(exists == 1) + } + pub async fn create_user(username: &str, password_hash: &str) -> Result<(), sqlx::Error> { let pool = create_db_pool().await?; @@ -27,4 +83,67 @@ pub(crate) mod users { .await?; Ok(()) } + + pub async fn hash_password(password: &str) -> String { + let salt = SaltString::generate(&mut rand::thread_rng()); + let argon2 = Argon2::default(); + let password_hash = argon2 + .hash_password(password.as_bytes(), &salt) + .expect("Failed to hash password"); + password_hash.to_string() + } + + pub async fn verify_password( + // Use clearer argument names + username: &str, + provided_password: &str, + ) -> Result { + let pool = create_db_pool().await?; // Propagates sqlx::Error + + // Fetch the stored hash for the user + let user_row = sqlx::query( + r#" + SELECT password_hash + FROM users + WHERE username = ? + "#, + ) + .bind(username) + .fetch_optional(&pool) // Use fetch_optional to handle not found case + .await?; + + // Get the stored hash string or return error if user not found + let stored_hash_str = match user_row { + Some(row) => row.get::(0), + None => return Err(DbError::UserNotFound), + }; + + // Parse the stored hash + let parsed_hash = PasswordHash::new(&stored_hash_str).map_err(DbError::Hashing)?; // Manually map the error + + let argon2 = Argon2::default(); + + let verification_result = + argon2.verify_password(provided_password.as_bytes(), &parsed_hash); + + // Check the result and return true/false accordingly + match verification_result { + Ok(()) => { + info!("Password check successful for user: {}", username); + Ok(true) + } + Err(argon2::password_hash::Error::Password) => { + info!("Password check failed (mismatch) for user: {}", username); + Ok(false) + } + Err(e) => { + // Handle other potential argon2 errors (e.g., invalid hash format) + info!( + "Password check failed for user {} with error: {}", + username, e + ); + Err(DbError::Hashing(e)) + } + } + } } diff --git a/src/main.rs b/src/main.rs index 6202b4d..698357a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ mod client; -mod tui; // Add this new module mod db; +mod tui; use client::handlers::handle_client; use db::users::create_db_pool;