From 1ff23a3004175e9aa066e6f45102129346e7646f Mon Sep 17 00:00:00 2001 From: Mitchell M Date: Mon, 12 Oct 2026 23:20:30 -0500 Subject: [PATCH] Update code2, and change secret protocol for code 1 --- Cargo.toml | 5 +--- README.md | 56 ++++++++++++------------------------------ dev.env | 2 +- public/challenge3.txt | 1 + src/codes/code1.rs | 55 +++++++---------------------------------- src/codes/code2.rs | 2 +- src/codes/mod.rs | 57 +++++++++++++++++++++++++++++++++++++++---- src/secrets.rs | 14 +++++------ style/main.scss | 28 +++++++++++++++++++++ 9 files changed, 114 insertions(+), 106 deletions(-) create mode 100644 public/challenge3.txt diff --git a/Cargo.toml b/Cargo.toml index 93fdc37..18f716a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,12 +12,11 @@ leptos_router = { version = "0.8.0" } axum = { version = "0.8.0", optional = true } console_error_panic_hook = { version = "0.1", optional = true } dotenvy = { version = "0.15", optional = true } -hmac = { version = "0.12", optional = true } leptos_axum = { version = "0.8.0", optional = true } leptos_meta = { version = "0.8.0" } reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"], optional = true } +serde = { version = "1", features = ["derive"] } serde_json = { version = "1", optional = true } -sha2 = { version = "0.10", optional = true } tokio = { version = "1", features = ["rt-multi-thread"], optional = true } wasm-bindgen = { version = "0.2.106", optional = true } @@ -30,12 +29,10 @@ hydrate = [ ssr = [ "dep:axum", "dep:dotenvy", - "dep:hmac", "dep:tokio", "dep:leptos_axum", "dep:reqwest", "dep:serde_json", - "dep:sha2", "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", diff --git a/README.md b/README.md index 0ba5bb6..bb736cb 100644 --- a/README.md +++ b/README.md @@ -65,69 +65,43 @@ the notification instead while offline). The result page displays step when ready. The robot submits code 1 with `POST /event/buzz` and a JSON body containing -`delta_ms` (a signed integer) and `signature` (hex-encoded HMAC-SHA256). The -signature is the HMAC of the UTF-8 decimal representation of `delta_ms`, using -`CODE1_HMAC_SECRET` as the key. Set `CODE1_HMAC_SECRET` to a unique random -secret of at least 32 bytes in `.env`; for example: +`delta_ms` (a signed integer) and `secret` (the value of `CODE1_SECRET`). +Set `CODE1_SECRET` to a unique random secret of at least 32 bytes in +`.env`. For example, with the server running locally: -```json -{"delta_ms":-42,"signature":"<64 hex characters>"} +```sh +curl.exe -X POST http://127.0.0.1:3000/event/buzz -H "Content-Type: application/json" -d '{"delta_ms":-42,"secret":""}' ``` +The integer `delta_ms` value is passed through to the congratulations message; +a valid request returns HTTP `202`. + ### Test code1 with Postman Create a `POST` request to `http://127.0.0.1:3000/event/buzz`, select **Body > -raw > JSON**, and use this body: - +raw > JSON**, and send: ```json { - "delta_ms": {{delta_ms}}, - "signature": "{{buzz_signature}}" + "delta_ms": -42, + "secret": "" } ``` -Add a Postman pre-request script to calculate the signature: - -```javascript -const deltaMs = pm.environment.get("delta_ms") || "-42"; -const secret = pm.environment.get("CODE1_HMAC_SECRET"); - -if (!secret) { - throw new Error("Set CODE1_HMAC_SECRET in the active Postman environment"); -} - -pm.environment.set("delta_ms", deltaMs); -pm.environment.set( - "buzz_signature", - CryptoJS.HmacSHA256(String(deltaMs), secret).toString(CryptoJS.enc.Hex) -); -``` - -Set `CODE1_HMAC_SECRET` in the active Postman environment to the same value as -in `.env`. Set `delta_ms` to a signed integer (for example `-42`) to change the -test event. A successful request returns HTTP `202`. - ### Test code1 with Node.js -With the server running and `CODE1_HMAC_SECRET` set in your shell, this sends a +With the server running and `CODE1_SECRET` set in your shell, this sends a single test event: ```javascript -const { createHmac } = require("node:crypto"); - async function main() { const deltaMs = -42; - const secret = process.env.CODE1_HMAC_SECRET; - if (!secret) throw new Error("CODE1_HMAC_SECRET is not set"); - - const signature = createHmac("sha256", secret) - .update(String(deltaMs), "utf8") - .digest("hex"); + const secret = process.env.CODE1_SECRET; + if (!secret) throw new Error("CODE1_SECRET is not set"); const response = await fetch("http://127.0.0.1:3000/event/buzz", { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify({ delta_ms: deltaMs, signature }), + body: JSON.stringify({ delta_ms: deltaMs, secret }), }); console.log(response.status, await response.text()); diff --git a/dev.env b/dev.env index 7a8c1e6..0e116d1 100644 --- a/dev.env +++ b/dev.env @@ -4,4 +4,4 @@ TEXTBEE_DEVICE_ID= RECIPIENT_SELF=+12015550123 RECIPIENT_1=+12015550124 RECIPIENT_2=+12015550125 -CODE1_HMAC_SECRET=replace-with-a-unique-random-secret +CODE1_SECRET=replace-with-a-unique-random-secret diff --git a/public/challenge3.txt b/public/challenge3.txt new file mode 100644 index 0000000..e8e6b33 --- /dev/null +++ b/public/challenge3.txt @@ -0,0 +1 @@ +Assembly data will be added here. diff --git a/src/codes/code1.rs b/src/codes/code1.rs index 1606c39..118d5ad 100644 --- a/src/codes/code1.rs +++ b/src/codes/code1.rs @@ -1,13 +1,11 @@ use axum::{extract::ConnectInfo, http::StatusCode, Json}; -use hmac::{Hmac, Mac}; use serde_json::Value; -use sha2::Sha256; use std::net::SocketAddr; pub const ID: &str = "code1"; const SELF_NOTIFICATION: &str = "Code 1 has been submitted while the server is live."; const CHALLENGE_1_FIRST: &str = "TODO: secded data"; -const CHALLENGE_1_SECOND: &str = "SECDED-13"; +const CHALLENGE_1_SECOND: &str = "SECDED-13\nMSB-LSB"; pub async fn buzz_event_handler( ConnectInfo(address): ConnectInfo, @@ -22,31 +20,21 @@ pub async fn buzz_event_handler( "delta_ms must be a signed 64-bit integer".to_string(), ) })?; - let signature = payload - .get("signature") + let secret = payload + .get("secret") .and_then(Value::as_str) .ok_or_else(|| { ( StatusCode::BAD_REQUEST, - "signature must be a hexadecimal HMAC-SHA256 string".to_string(), + "secret must be a string".to_string(), ) })?; - let signature = decode_hex_signature(signature).ok_or_else(|| { - ( + if secret != crate::secrets::runtime().code1_secret { + return Err(( StatusCode::UNAUTHORIZED, - "Invalid buzz event signature".to_string(), - ) - })?; - let mut mac = - Hmac::::new_from_slice(crate::secrets::runtime().code1_hmac_secret.as_bytes()) - .expect("HMAC accepts secrets of any length"); - mac.update(delta_ms.to_string().as_bytes()); - mac.verify_slice(&signature).map_err(|_| { - ( - StatusCode::UNAUTHORIZED, - "Invalid buzz event signature".to_string(), - ) - })?; + "Invalid buzz event secret".to_string(), + )); + } let live = crate::manage::server_live_state(); crate::codes::history::record_successful_submission(address.ip(), live, ID) @@ -62,31 +50,6 @@ pub async fn buzz_event_handler( Ok(StatusCode::ACCEPTED) } -fn decode_hex_signature(signature: &str) -> Option> { - if signature.len() != 64 { - return None; - } - - signature - .as_bytes() - .chunks_exact(2) - .map(|pair| { - let high = hex_nibble(pair[0])?; - let low = hex_nibble(pair[1])?; - Some((high << 4) | low) - }) - .collect() -} - -fn hex_nibble(byte: u8) -> Option { - match byte { - b'0'..=b'9' => Some(byte - b'0'), - b'a'..=b'f' => Some(byte - b'a' + 10), - b'A'..=b'F' => Some(byte - b'A' + 10), - _ => None, - } -} - async fn on_submit(live: bool, delta_ms: i64) -> Result<(), String> { let direction = match delta_ms.cmp(&0) { std::cmp::Ordering::Less => "short", diff --git a/src/codes/code2.rs b/src/codes/code2.rs index d38c97f..956b60f 100644 --- a/src/codes/code2.rs +++ b/src/codes/code2.rs @@ -1,6 +1,6 @@ pub const ID: &str = "code2"; pub const CODE: &str = "Qj4eAn"; -pub const NEXT_STEP_TEXT: &str = "TODO: replace this with the next step."; +pub const SUCCESS_MESSAGE: &str = "Congratulations on completing step 2."; const SELF_NOTIFICATION: &str = "Code 2 has been submitted."; diff --git a/src/codes/mod.rs b/src/codes/mod.rs index 7af6d37..2a057cb 100644 --- a/src/codes/mod.rs +++ b/src/codes/mod.rs @@ -12,10 +12,24 @@ pub mod code2; pub mod history; +#[derive(Clone, serde::Deserialize, serde::Serialize)] +struct SubmissionResponse { + message: String, + download: Option, +} + +#[derive(Clone, serde::Deserialize, serde::Serialize)] +struct DownloadInfo { + href: String, + filename: String, + title: String, + description: String, +} + #[server] -async fn submit_code(code: String) -> Result { +async fn submit_code(code: String) -> Result, ServerFnError> { if code != crate::codes::code2::CODE { - return Ok("This code is not recognized.".to_string()); + return Ok(None); } let live = crate::manage::server_live_state(); @@ -26,7 +40,15 @@ async fn submit_code(code: String) -> Result { .await .map_err(ServerFnError::new)?; - Ok(crate::codes::code2::NEXT_STEP_TEXT.to_string()) + Ok(Some(SubmissionResponse { + message: crate::codes::code2::SUCCESS_MESSAGE.to_string(), + download: Some(DownloadInfo { + href: "/challenge3.txt".to_string(), + filename: "challenge3.txt".to_string(), + title: "Download challenge 3".to_string(), + description: "TXT file".to_string(), + }), + })) } fn encode_path_segment(value: &str) -> String { @@ -78,6 +100,28 @@ pub fn HomePage() -> impl IntoView { } } +#[component] +fn SubmissionResult(response: SubmissionResponse) -> impl IntoView { + let download = response.download.map(|download| { + view! { + + {download.title} + {download.description} + + } + }); + + view! { +

"Submission result"

+

{response.message}

+ {download} + } +} + #[component] pub fn SubmitPage() -> impl IntoView { let params = use_params_map(); @@ -90,9 +134,12 @@ pub fn SubmitPage() -> impl IntoView { "Checking code..."

}> {move || Suspend::new(async move { match submission.await { - Ok(message) => view! { + Ok(Some(response)) => { + view! { }.into_any() + }, + Ok(None) => view! {

"Submission result"

-

{message}

+

"This code is not recognized."

}.into_any(), Err(error) => view! {