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 54f9924..ce0640b 100644 Binary files a/db.sqlite and b/db.sqlite differ diff --git a/db.sqlite-journal b/db.sqlite-journal new file mode 100644 index 0000000..55c0fd2 Binary files /dev/null and b/db.sqlite-journal differ diff --git a/migrations/002_create_admin_flag.sql b/migrations/002_create_admin_flag.sql new file mode 100644 index 0000000..ec70f0e --- /dev/null +++ b/migrations/002_create_admin_flag.sql @@ -0,0 +1,2 @@ +ALTER TABLE users +ADD COLUMN is_admin BOOLEAN DEFAULT FALSE; diff --git a/migrations/003_create_ban_flag.sql b/migrations/003_create_ban_flag.sql new file mode 100644 index 0000000..2459b6a --- /dev/null +++ b/migrations/003_create_ban_flag.sql @@ -0,0 +1,5 @@ +ALTER TABLE users +ADD COLUMN is_banned BOOLEAN DEFAULT FALSE; + +ALTER TABLE users +ADD COLUMN ban_reason VARCHAR(255); diff --git a/src/client/mod.rs b/src/client/mod.rs index 283d75c..a6fe033 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -4,6 +4,7 @@ pub(crate) mod handlers { Aes256Gcm, Key, Nonce, }; + use crate::db::users::{check_for_account, create_user, hash_password, verify_password}; use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; use log::{debug, error, info}; use serde::Deserialize; @@ -12,8 +13,6 @@ pub(crate) mod handlers { use tokio::net::TcpStream; use tokio::sync::broadcast; use x25519_dalek::{EphemeralSecret, PublicKey}; - - use crate::db::users::create_user; /* Specifications of the packet 32 bytes - Command name @@ -86,8 +85,90 @@ pub(crate) mod handlers { let username = Arc::new(String::from_utf8(decrypted)?); let username_read = Arc::clone(&username); // Clone the Arc for read task let username_write = Arc::clone(&username); // Clone the Arc for write task + info!("Username received: {}", username); - create_user(&username, "1234").await?; + // Check if the user already exists in the database + if check_for_account(&username).await? { + info!("User {} already exists", username); + // Send a message to the client + let message = format!("User {} is 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(); + // 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;