feat: implement argon2id and expect passwords to be pre-hashed
This commit is contained in:
parent
3461218025
commit
87edb9dd12
4 changed files with 86 additions and 49 deletions
|
@ -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"] }
|
||||
|
|
|
@ -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<Data>) -> Result<Htt
|
|||
// FIXME: This regex doesnt seem to be working
|
||||
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 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())
|
||||
} 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 {
|
||||
return Ok(login(login_information.password, password))
|
||||
return Ok(login(data.argon2.clone(), login_information.password, password))
|
||||
}
|
||||
|
||||
return Ok(HttpResponse::Unauthorized().finish())
|
||||
|
@ -57,8 +65,9 @@ pub async fn res(mut payload: web::Payload, data: web::Data<Data>) -> Result<Htt
|
|||
Ok(HttpResponse::Unauthorized().finish())
|
||||
}
|
||||
|
||||
fn login(request_password: String, database_password: String) -> HttpResponse {
|
||||
if request_password == database_password {
|
||||
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,
|
||||
|
@ -66,5 +75,8 @@ fn login(request_password: String, database_password: String) -> HttpResponse {
|
|||
})
|
||||
}
|
||||
|
||||
HttpResponse::Unauthorized().finish()
|
||||
return HttpResponse::Unauthorized().finish()
|
||||
}
|
||||
|
||||
HttpResponse::InternalServerError().finish()
|
||||
}
|
||||
|
|
|
@ -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<Data>) -> Result<Htt
|
|||
}
|
||||
body.extend_from_slice(&chunk);
|
||||
}
|
||||
|
||||
let account_information = serde_json::from_slice::<AccountInformation>(&body)?;
|
||||
|
||||
let uuid = Uuid::now_v7();
|
||||
|
@ -92,11 +94,26 @@ pub async fn res(mut payload: web::Payload, data: web::Data<Data>) -> Result<Htt
|
|||
))
|
||||
}
|
||||
|
||||
// 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
|
||||
Ok(match sqlx::query(&format!("INSERT INTO users VALUES ( '{}', $1, NULL, $2, $3, false )", uuid))
|
||||
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(account_information.password)
|
||||
.bind(hashed_password.to_string())
|
||||
.bind(account_information.email)
|
||||
.execute(&data.pool)
|
||||
.await {
|
||||
|
@ -129,4 +146,7 @@ pub async fn res(mut payload: web::Payload, data: web::Data<Data>) -> Result<Htt
|
|||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
Ok(HttpResponse::InternalServerError().finish())
|
||||
}
|
||||
|
|
|
@ -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<dyn std::error::Error>;
|
|||
struct Data {
|
||||
pub pool: Pool<Postgres>,
|
||||
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(),
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue