diff --git a/README.md b/README.md index 64a6b4c..4cc8df2 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ And it's ready to take contestants, the hackathon can finally start! ## Security - We use [`Argon2`] for password-hashing. +- Puzzle solutions are also stored as Argon2 hashes (not raw plaintext). - State saving is encrypted with [`chacha20poly1305`] (based on [this great guide]) > [!note] diff --git a/src/backend/endpoints.rs b/src/backend/endpoints.rs index 2e7c9f2..11c9e5a 100644 --- a/src/backend/endpoints.rs +++ b/src/backend/endpoints.rs @@ -4,11 +4,23 @@ use dioxus::prelude::*; #[cfg(feature = "server")] use { super::logic::*, + chacha20poly1305::aead::{OsRng, rand_core::RngCore}, dioxus::fullstack::{Cookie, TypedHeader}, uuid::Uuid, zeroize::Zeroize, }; +#[cfg(feature = "server")] +fn hash_puzzle_solution(raw_solution: &str) -> Result { + let mut salt = [0u8; 32]; + OsRng.fill_bytes(&mut salt); + let hash = argon2::hash_encoded(raw_solution.as_bytes(), &salt, &ARGON2CONF) + .inspect_err(|e| error!("nem sikerült feladatmegoldást hasítani: {e}")) + .or_internal_server_error("nem sikerült feladatmegoldást hasítani"); + salt.zeroize(); + hash +} + #[get("/api/event_title")] pub async fn event_title() -> Result { Ok(EVENT_TITLE.clone()?) @@ -157,7 +169,7 @@ pub async fn set_passwd(init_password: String, mut password: String) -> Result Result { // submitting as admin @@ -175,6 +187,12 @@ pub async fn set_solution( .or_forbidden("legalább egy feladat már be van állítva")?; drop(puzzles_lock); + for puzzle in puzzle_solutions.values_mut() { + let solution_hash = hash_puzzle_solution(&puzzle.solution)?; + puzzle.solution.zeroize(); + puzzle.solution = solution_hash; + } + PUZZLES.write().await.extend(puzzle_solutions); #[cfg(feature = "server_state_save")] @@ -191,7 +209,7 @@ pub async fn set_solution( #[post("/api/submit", cookies: TypedHeader)] pub async fn submit_solution( puzzle_id: PuzzleId, - solution: PuzzleSolution, + mut solution: PuzzleSolution, ) -> Result { check_admin_pwd()?; let uuid = extract_sid_cookie(cookies).await?; @@ -202,14 +220,17 @@ pub async fn submit_solution( .or_not_found("nincs ezzel az azonosítóval csapat")? .clone(); // PERF: rather clone than lock - PUZZLES - .read() - .await - .get(&puzzle_id) - .or_not_found("nincs ezzel az azonosítóval feladat")? - .solution - .eq(&solution) - .or_forbidden("érvénytelen megoldás ehhez a feladathoz")?; + let is_solution_valid = { + let puzzles_lock = PUZZLES.read().await; + let puzzle = puzzles_lock + .get(&puzzle_id) + .or_not_found("nincs ezzel az azonosítóval feladat")?; + argon2::verify_encoded(&puzzle.solution, solution.as_bytes()) + .inspect_err(|e| error!("nem sikerült ellenőrizni a feladatmegoldást: {e}")) + .or_internal_server_error("nem sikerült ellenőrizni a feladatmegoldást")? + }; + solution.zeroize(); + is_solution_valid.or_forbidden("érvénytelen megoldás ehhez a feladathoz")?; let mut teams_lock = TEAMS.write().await; diff --git a/src/backend/models.rs b/src/backend/models.rs index 9cbe9d6..d02b96a 100644 --- a/src/backend/models.rs +++ b/src/backend/models.rs @@ -5,7 +5,8 @@ use std::collections::{HashMap, HashSet}; #[derive(Clone, PartialOrd, Ord, PartialEq, Eq, serde::Deserialize, serde::Serialize)] #[serde(crate = "dioxus::fullstack::serde")] pub struct Puzzle { - pub solution: PuzzleSolution, + /// argon2-encoded solution hash (not the raw solution) + pub solution: PuzzleSolutionHash, /// how much it's worth pub value: PuzzleValue, } @@ -14,6 +15,7 @@ pub type PuzzleId = String; /// how much points you get for solving a puzzle pub type PuzzleValue = u32; pub type PuzzleSolution = String; +pub type PuzzleSolutionHash = String; /// all the known puzzles with their values pub type PuzzlesExisting = HashMap; /// all the puzzles with their values and solutions