```
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 ```
This commit is contained in:
parent
ba22b22ecc
commit
b98a890738
9 changed files with 278 additions and 10 deletions
66
Cargo.lock
generated
66
Cargo.lock
generated
|
@ -131,6 +131,18 @@ dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"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]]
|
[[package]]
|
||||||
name = "atoi"
|
name = "atoi"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
|
@ -179,6 +191,19 @@ version = "1.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
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]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.6.0"
|
version = "2.6.0"
|
||||||
|
@ -188,6 +213,15 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blake2"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.10.4"
|
version = "0.10.4"
|
||||||
|
@ -197,6 +231,16 @@ dependencies = [
|
||||||
"generic-array",
|
"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]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.16.0"
|
version = "3.16.0"
|
||||||
|
@ -250,7 +294,9 @@ name = "chatserver"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
|
"argon2",
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
|
"bcrypt",
|
||||||
"chrono",
|
"chrono",
|
||||||
"colog",
|
"colog",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
|
@ -261,6 +307,7 @@ dependencies = [
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"serde",
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
"x25519-dalek",
|
"x25519-dalek",
|
||||||
|
@ -1379,6 +1426,17 @@ dependencies = [
|
||||||
"windows-targets 0.52.6",
|
"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]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
|
@ -2122,18 +2180,18 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.11"
|
version = "2.0.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
|
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "2.0.11"
|
version = "2.0.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
|
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
|
@ -18,4 +18,7 @@ toml = "0.8.19"
|
||||||
ratatui = "0.29.0"
|
ratatui = "0.29.0"
|
||||||
crossterm = "0.28.1"
|
crossterm = "0.28.1"
|
||||||
chrono = "0.4.39"
|
chrono = "0.4.39"
|
||||||
sqlx = { version = "0.8", features = [ "sqlite", "runtime-tokio", "tls-native-tls" ] }
|
sqlx = { version = "0.8", features = [ "sqlite", "runtime-tokio", "tls-native-tls" ] }
|
||||||
|
bcrypt = "0.17.0"
|
||||||
|
argon2 = "0.5.3"
|
||||||
|
thiserror = "2.0.12"
|
||||||
|
|
BIN
db.sqlite
BIN
db.sqlite
Binary file not shown.
BIN
db.sqlite-journal
Normal file
BIN
db.sqlite-journal
Normal file
Binary file not shown.
2
migrations/002_create_admin_flag.sql
Normal file
2
migrations/002_create_admin_flag.sql
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN is_admin BOOLEAN DEFAULT FALSE;
|
5
migrations/003_create_ban_flag.sql
Normal file
5
migrations/003_create_ban_flag.sql
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN is_banned BOOLEAN DEFAULT FALSE;
|
||||||
|
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN ban_reason VARCHAR(255);
|
|
@ -4,6 +4,7 @@ pub(crate) mod handlers {
|
||||||
Aes256Gcm, Key, Nonce,
|
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 base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
@ -12,8 +13,6 @@ pub(crate) mod handlers {
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||||
|
|
||||||
use crate::db::users::create_user;
|
|
||||||
/*
|
/*
|
||||||
Specifications of the packet
|
Specifications of the packet
|
||||||
32 bytes - Command name
|
32 bytes - Command name
|
||||||
|
@ -86,8 +85,90 @@ pub(crate) mod handlers {
|
||||||
let username = Arc::new(String::from_utf8(decrypted)?);
|
let username = Arc::new(String::from_utf8(decrypted)?);
|
||||||
let username_read = Arc::clone(&username); // Clone the Arc for read task
|
let username_read = Arc::clone(&username); // Clone the Arc for read task
|
||||||
let username_write = Arc::clone(&username); // Clone the Arc for write 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
|
// Read task for receiving messages from the client
|
||||||
let read_task = tokio::spawn(async move {
|
let read_task = tokio::spawn(async move {
|
||||||
|
|
121
src/db/mod.rs
121
src/db/mod.rs
|
@ -1,5 +1,21 @@
|
||||||
pub(crate) mod users {
|
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<SqlitePool, sqlx::Error> {
|
pub async fn connect_to_db() -> Result<SqlitePool, sqlx::Error> {
|
||||||
let pool = SqlitePool::connect("sqlite:./db.sqlite").await?;
|
let pool = SqlitePool::connect("sqlite:./db.sqlite").await?;
|
||||||
|
@ -12,6 +28,46 @@ pub(crate) mod users {
|
||||||
Ok(pool)
|
Ok(pool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_user_by_username(
|
||||||
|
username: &str,
|
||||||
|
) -> Result<Option<(i64, String)>, 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<bool, sqlx::Error> {
|
||||||
|
// 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::<i64, _>(0);
|
||||||
|
|
||||||
|
Ok(exists == 1)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn create_user(username: &str, password_hash: &str) -> Result<(), sqlx::Error> {
|
pub async fn create_user(username: &str, password_hash: &str) -> Result<(), sqlx::Error> {
|
||||||
let pool = create_db_pool().await?;
|
let pool = create_db_pool().await?;
|
||||||
|
|
||||||
|
@ -27,4 +83,67 @@ pub(crate) mod users {
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
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<bool, DbError> {
|
||||||
|
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::<String, _>(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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
mod client;
|
mod client;
|
||||||
mod tui; // Add this new module
|
|
||||||
mod db;
|
mod db;
|
||||||
|
mod tui;
|
||||||
|
|
||||||
use client::handlers::handle_client;
|
use client::handlers::handle_client;
|
||||||
use db::users::create_db_pool;
|
use db::users::create_db_pool;
|
||||||
|
|
Loading…
Add table
Reference in a new issue