From ad11ac6c18fd4d5f0d2cbc21b3193b1b09179bc2 Mon Sep 17 00:00:00 2001 From: James2Tulloch <146088090+James2Tulloch@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:10:10 +0000 Subject: [PATCH] Password hashing --- rust_crud_api/Cargo.toml | 4 ++- rust_crud_api/src/lib.rs | 61 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/rust_crud_api/Cargo.toml b/rust_crud_api/Cargo.toml index 1714ef2..ef75e3b 100644 --- a/rust_crud_api/Cargo.toml +++ b/rust_crud_api/Cargo.toml @@ -12,4 +12,6 @@ pyo3 = { version = "0.18", features = ["extension-module"] } postgres = "0.19" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -jsonwebtoken = "8.2.0" +jsonwebtoken = "8.2.0" +argon2 = "0.4" +rand = "0.8" diff --git a/rust_crud_api/src/lib.rs b/rust_crud_api/src/lib.rs index 0d446d7..fbe7ef2 100644 --- a/rust_crud_api/src/lib.rs +++ b/rust_crud_api/src/lib.rs @@ -67,6 +67,37 @@ fn verify_jwt(token: String, secret: String) -> PyResult<PyObject> { Ok(dict.to_object(py)) } +// Pasword Hashing with Argon2 + +#[pyfunction] +fn hash_password(password: &str) -> PyResult<String> { + use argon2::{Argon2, PasswordHasher}; + use argon2::password_hash::{SaltString, rand_core::OsRng}; + + // Generate a random salt using OsRng. + let salt = SaltString::generate(&mut OsRng); + // Create a default Argon2 instance. + let argon2 = Argon2::default(); + // Hash the password using the salt. + let password_hash = argon2.hash_password(password.as_bytes(), &salt) + .map_err(|e| PyRuntimeError::new_err(e.to_string()))? + .to_string(); + Ok(password_hash) +} + +#[pyfunction] +fn verify_password(hash: &str, password: &str) -> PyResult<bool> { + use argon2::{Argon2, PasswordVerifier}; + use argon2::password_hash::PasswordHash; + + let argon2 = Argon2::default(); + let parsed_hash = PasswordHash::new(hash) + .map_err(|e| PyRuntimeError::new_err(e.to_string()))?; + argon2.verify_password(password.as_bytes(), &parsed_hash) + .map_err(|e| PyRuntimeError::new_err(e.to_string())) + .map(|_| true) +} + /// A helper function to convert postgres::Error into a Python RuntimeError. fn pg_err(e: postgres::Error) -> PyErr { PyRuntimeError::new_err(e.to_string()) @@ -81,7 +112,8 @@ fn init_db(db_url: &str) -> PyResult<()> { CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, name VARCHAR NOT NULL, - email VARCHAR NOT NULL + email VARCHAR NOT NULL, + password_hash VARCHAR NOT NULL ); CREATE TABLE IF NOT EXISTS groups ( id SERIAL PRIMARY KEY, @@ -99,11 +131,12 @@ fn init_db(db_url: &str) -> PyResult<()> { /// Create a new user by inserting into the database. #[pyfunction] -fn create_user(db_url: &str, name: &str, email: &str) -> PyResult<()> { +fn create_user(db_url: &str, name: &str, email: &str, password: &str) -> PyResult<()> { + let password_hash = hash_password(password)?; let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; client.execute( - "INSERT INTO users (name, email) VALUES ($1, $2)", - &[&name, &email] + "INSERT INTO users (name, email, password_hash) VALUES ($1, $2, $3)", + &[&name, &email, &password_hash] ).map_err(pg_err)?; Ok(()) } @@ -173,6 +206,24 @@ fn delete_user(db_url: &str, user_id: i32) -> PyResult<bool> { Ok(deleted > 0) } +///Verify credentials +///Retrieve a stored password hash for the email and compare. +#[pyfunction] +fn verify_user(db_url: &str, email: &str, password: & str) -> PyResult<bool> { + let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; + let row_opt = client.query_opt( + "SELECT password_hash FROM users WHERE email = $1", + &[&email] + ).map_err(pg_err)?; + if let Some(row) = row_opt { + let stored_hash: String = row.get(0); + verify_password(&stored_hash, password) + } else { + Ok(false) + } +} + + /// Create a group #[pyfunction] fn create_group(db_url: &str, name: &str) -> PyResult<()> { @@ -270,6 +321,8 @@ fn rust_crud_api(_py: Python, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(get_group_members, m)?)?; m.add_function(wrap_pyfunction!(generate_jwt, m)?)?; m.add_function(wrap_pyfunction!(verify_jwt, m)?)?; + m.add_function(wrap_pyfunction!(hash_password, m)?)?; + m.add_function(wrap_pyfunction!(verify_password, m)?)?; Ok(()) } -- GitLab