From 87edb9dd122438bf3f25b03f0798343dde66b684 Mon Sep 17 00:00:00 2001 From: Radical Date: Wed, 30 Apr 2025 21:36:22 +0200 Subject: [PATCH] feat: implement argon2id and expect passwords to be pre-hashed --- Cargo.toml | 1 + src/api/v1/login.rs | 34 ++++++++++----- src/api/v1/register.rs | 94 +++++++++++++++++++++++++----------------- src/main.rs | 6 ++- 4 files changed, 86 insertions(+), 49 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 52ac0fa..8151ec8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] actix-web = "4.10" +argon2 = { version = "0.5.3", features = ["std"] } futures = "0.3" regex = "1.11" serde = { version = "1.0", features = ["derive"] } diff --git a/src/api/v1/login.rs b/src/api/v1/login.rs index 49e7305..257f2bf 100644 --- a/src/api/v1/login.rs +++ b/src/api/v1/login.rs @@ -1,4 +1,5 @@ use actix_web::{error, post, web, Error, HttpResponse}; +use argon2::{Argon2, PasswordHash, PasswordVerifier}; use regex::Regex; use serde::{Deserialize, Serialize}; use futures::StreamExt; @@ -40,15 +41,22 @@ pub async fn res(mut payload: web::Payload, data: web::Data) -> Result) -> Result HttpResponse { - if request_password == database_password { - return HttpResponse::Ok().json(Response { - access_token: "bogus".to_string(), - expires_in: 0, - refresh_token: "bogus".to_string(), - }) +fn login(argon2: Argon2, request_password: String, database_password: String) -> HttpResponse { + if let Ok(parsed_hash) = PasswordHash::new(&database_password) { + if argon2.verify_password(request_password.as_bytes(), &parsed_hash).is_ok() { + return HttpResponse::Ok().json(Response { + access_token: "bogus".to_string(), + expires_in: 0, + refresh_token: "bogus".to_string(), + }) + } + + return HttpResponse::Unauthorized().finish() } - - HttpResponse::Unauthorized().finish() + + HttpResponse::InternalServerError().finish() } diff --git a/src/api/v1/register.rs b/src/api/v1/register.rs index 877ad43..ea95ce9 100644 --- a/src/api/v1/register.rs +++ b/src/api/v1/register.rs @@ -3,6 +3,7 @@ use regex::Regex; use serde::{Deserialize, Serialize}; use futures::StreamExt; use uuid::Uuid; +use argon2::{password_hash::{rand_core::OsRng, SaltString}, PasswordHasher}; use crate::Data; @@ -21,6 +22,7 @@ struct ResponseError { gorb_id_available: bool, email_valid: bool, email_available: bool, + password_hashed: bool, password_minimum_length: bool, password_special_characters: bool, password_letters: bool, @@ -35,6 +37,7 @@ impl Default for ResponseError { gorb_id_available: true, email_valid: true, email_available: true, + password_hashed: true, password_minimum_length: true, password_special_characters: true, password_letters: true, @@ -64,7 +67,6 @@ pub async fn res(mut payload: web::Payload, data: web::Data) -> Result(&body)?; let uuid = Uuid::now_v7(); @@ -92,41 +94,59 @@ pub async fn res(mut payload: web::Payload, data: web::Data) -> Result { - HttpResponse::Ok().json( - Response { - access_token: "bogus".to_string(), - user_id: "bogus".to_string(), - expires_in: 1, - refresh_token: "bogus".to_string(), + // Password is expected to be hashed using SHA3-384 + let password_regex = Regex::new(r"/[0-9a-f]{96}/i").unwrap(); + + if !password_regex.is_match(&account_information.password) { + return Ok(HttpResponse::Forbidden().json( + ResponseError { + password_hashed: false, + ..Default::default() + } + )) + } + + let salt = SaltString::generate(&mut OsRng); + + if let Ok(hashed_password) = data.argon2.hash_password(account_information.password.as_bytes(), &salt) { + // TODO: Check security of this implementation + return Ok(match sqlx::query(&format!("INSERT INTO users VALUES ( '{}', $1, NULL, $2, $3, false )", uuid)) + .bind(account_information.identifier) + // FIXME: Password has no security currently, either from a client or server perspective + .bind(hashed_password.to_string()) + .bind(account_information.email) + .execute(&data.pool) + .await { + Ok(_out) => { + HttpResponse::Ok().json( + Response { + access_token: "bogus".to_string(), + user_id: "bogus".to_string(), + expires_in: 1, + refresh_token: "bogus".to_string(), + } + ) + }, + Err(error) => { + let err_msg = error.as_database_error().unwrap().message(); + + match err_msg { + err_msg if err_msg.contains("unique") && err_msg.contains("username_key") => HttpResponse::Forbidden().json(ResponseError { + gorb_id_available: false, + ..Default::default() + }), + err_msg if err_msg.contains("unique") && err_msg.contains("email_key") => HttpResponse::Forbidden().json(ResponseError { + email_available: false, + ..Default::default() + }), + _ => { + eprintln!("{}", err_msg); + HttpResponse::InternalServerError().finish() + } } - ) - }, - Err(error) => { - let err_msg = error.as_database_error().unwrap().message(); - - match err_msg { - err_msg if err_msg.contains("unique") && err_msg.contains("username_key") => HttpResponse::Forbidden().json(ResponseError { - gorb_id_available: false, - ..Default::default() - }), - err_msg if err_msg.contains("unique") && err_msg.contains("email_key") => HttpResponse::Forbidden().json(ResponseError { - email_available: false, - ..Default::default() - }), - _ => { - eprintln!("{}", err_msg); - HttpResponse::InternalServerError().finish() - } - } - }, - }) + }, + }) + } + + Ok(HttpResponse::InternalServerError().finish()) } diff --git a/src/main.rs b/src/main.rs index e0a82ac..5077e7e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ use actix_web::{App, HttpServer, web}; -use sqlx::{Executor, PgPool, Pool, Postgres}; +use argon2::Argon2; +use sqlx::{PgPool, Pool, Postgres}; use std::time::SystemTime; mod config; use config::{Config, ConfigBuilder}; @@ -11,6 +12,7 @@ type Error = Box; struct Data { pub pool: Pool, pub config: Config, + pub argon2: Argon2<'static>, pub start_time: SystemTime, } @@ -46,6 +48,8 @@ async fn main() -> Result<(), Error> { let data = Data { pool, config, + // TODO: Possibly implement "pepper" into this (thinking it could generate one if it doesnt exist and store it on disk) + argon2: Argon2::default(), start_time: SystemTime::now(), };