feat: implement argon2id and expect passwords to be pre-hashed

This commit is contained in:
Radical 2025-04-30 21:36:22 +02:00
parent 3461218025
commit 87edb9dd12
4 changed files with 86 additions and 49 deletions

View file

@ -5,6 +5,7 @@ edition = "2024"
[dependencies] [dependencies]
actix-web = "4.10" actix-web = "4.10"
argon2 = { version = "0.5.3", features = ["std"] }
futures = "0.3" futures = "0.3"
regex = "1.11" regex = "1.11"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }

View file

@ -1,4 +1,5 @@
use actix_web::{error, post, web, Error, HttpResponse}; use actix_web::{error, post, web, Error, HttpResponse};
use argon2::{Argon2, PasswordHash, PasswordVerifier};
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use futures::StreamExt; use futures::StreamExt;
@ -40,15 +41,22 @@ pub async fn res(mut payload: web::Payload, data: web::Data<Data>) -> Result<Htt
// FIXME: This regex doesnt seem to be working // FIXME: This regex doesnt seem to be working
let username_regex = Regex::new(r"[a-zA-Z0-9.-_]").unwrap(); let username_regex = Regex::new(r"[a-zA-Z0-9.-_]").unwrap();
// 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(&login_information.password) {
return Ok(HttpResponse::Forbidden().json(r#"{ "password_hashed": false }"#));
}
if email_regex.is_match(&login_information.username) { if email_regex.is_match(&login_information.username) {
if let Ok(password) = sqlx::query_scalar("SELECT password FROM users WHERE email = $1").bind(login_information.username).fetch_one(&data.pool).await { if let Ok(password) = sqlx::query_scalar("SELECT password FROM users WHERE email = $1").bind(login_information.username).fetch_one(&data.pool).await {
return Ok(login(login_information.password, password)) return Ok(login(data.argon2.clone(), login_information.password, password))
} }
return Ok(HttpResponse::Unauthorized().finish()) return Ok(HttpResponse::Unauthorized().finish())
} else if username_regex.is_match(&login_information.username) { } else if username_regex.is_match(&login_information.username) {
if let Ok(password) = sqlx::query_scalar("SELECT password FROM users WHERE username = $1").bind(login_information.username).fetch_one(&data.pool).await { if let Ok(password) = sqlx::query_scalar("SELECT password FROM users WHERE username = $1").bind(login_information.username).fetch_one(&data.pool).await {
return Ok(login(login_information.password, password)) return Ok(login(data.argon2.clone(), login_information.password, password))
} }
return Ok(HttpResponse::Unauthorized().finish()) return Ok(HttpResponse::Unauthorized().finish())
@ -57,14 +65,18 @@ pub async fn res(mut payload: web::Payload, data: web::Data<Data>) -> Result<Htt
Ok(HttpResponse::Unauthorized().finish()) Ok(HttpResponse::Unauthorized().finish())
} }
fn login(request_password: String, database_password: String) -> HttpResponse { fn login(argon2: Argon2, request_password: String, database_password: String) -> HttpResponse {
if request_password == database_password { if let Ok(parsed_hash) = PasswordHash::new(&database_password) {
return HttpResponse::Ok().json(Response { if argon2.verify_password(request_password.as_bytes(), &parsed_hash).is_ok() {
access_token: "bogus".to_string(), return HttpResponse::Ok().json(Response {
expires_in: 0, access_token: "bogus".to_string(),
refresh_token: "bogus".to_string(), expires_in: 0,
}) refresh_token: "bogus".to_string(),
})
}
return HttpResponse::Unauthorized().finish()
} }
HttpResponse::Unauthorized().finish() HttpResponse::InternalServerError().finish()
} }

View file

@ -3,6 +3,7 @@ use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use futures::StreamExt; use futures::StreamExt;
use uuid::Uuid; use uuid::Uuid;
use argon2::{password_hash::{rand_core::OsRng, SaltString}, PasswordHasher};
use crate::Data; use crate::Data;
@ -21,6 +22,7 @@ struct ResponseError {
gorb_id_available: bool, gorb_id_available: bool,
email_valid: bool, email_valid: bool,
email_available: bool, email_available: bool,
password_hashed: bool,
password_minimum_length: bool, password_minimum_length: bool,
password_special_characters: bool, password_special_characters: bool,
password_letters: bool, password_letters: bool,
@ -35,6 +37,7 @@ impl Default for ResponseError {
gorb_id_available: true, gorb_id_available: true,
email_valid: true, email_valid: true,
email_available: true, email_available: true,
password_hashed: true,
password_minimum_length: true, password_minimum_length: true,
password_special_characters: true, password_special_characters: true,
password_letters: true, password_letters: true,
@ -64,7 +67,6 @@ pub async fn res(mut payload: web::Payload, data: web::Data<Data>) -> Result<Htt
} }
body.extend_from_slice(&chunk); body.extend_from_slice(&chunk);
} }
let account_information = serde_json::from_slice::<AccountInformation>(&body)?; let account_information = serde_json::from_slice::<AccountInformation>(&body)?;
let uuid = Uuid::now_v7(); let uuid = Uuid::now_v7();
@ -92,41 +94,59 @@ pub async fn res(mut payload: web::Payload, data: web::Data<Data>) -> Result<Htt
)) ))
} }
// TODO: Check security of this implementation // Password is expected to be hashed using SHA3-384
Ok(match sqlx::query(&format!("INSERT INTO users VALUES ( '{}', $1, NULL, $2, $3, false )", uuid)) let password_regex = Regex::new(r"/[0-9a-f]{96}/i").unwrap();
.bind(account_information.identifier)
// FIXME: Password has no security currently, either from a client or server perspective if !password_regex.is_match(&account_information.password) {
.bind(account_information.password) return Ok(HttpResponse::Forbidden().json(
.bind(account_information.email) ResponseError {
.execute(&data.pool) password_hashed: false,
.await { ..Default::default()
Ok(_out) => { }
HttpResponse::Ok().json( ))
Response { }
access_token: "bogus".to_string(),
user_id: "bogus".to_string(), let salt = SaltString::generate(&mut OsRng);
expires_in: 1,
refresh_token: "bogus".to_string(), 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();
Ok(HttpResponse::InternalServerError().finish())
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()
}
}
},
})
} }

View file

@ -1,5 +1,6 @@
use actix_web::{App, HttpServer, web}; use actix_web::{App, HttpServer, web};
use sqlx::{Executor, PgPool, Pool, Postgres}; use argon2::Argon2;
use sqlx::{PgPool, Pool, Postgres};
use std::time::SystemTime; use std::time::SystemTime;
mod config; mod config;
use config::{Config, ConfigBuilder}; use config::{Config, ConfigBuilder};
@ -11,6 +12,7 @@ type Error = Box<dyn std::error::Error>;
struct Data { struct Data {
pub pool: Pool<Postgres>, pub pool: Pool<Postgres>,
pub config: Config, pub config: Config,
pub argon2: Argon2<'static>,
pub start_time: SystemTime, pub start_time: SystemTime,
} }
@ -46,6 +48,8 @@ async fn main() -> Result<(), Error> {
let data = Data { let data = Data {
pool, pool,
config, 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(), start_time: SystemTime::now(),
}; };