Update code2, and change secret protocol for code 1
This commit is contained in:
+1
-4
@@ -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",
|
||||
|
||||
@@ -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":"<your CODE1_SECRET value>"}'
|
||||
```
|
||||
|
||||
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": "<your CODE1_SECRET value>"
|
||||
}
|
||||
```
|
||||
|
||||
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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Assembly data will be added here.
|
||||
+9
-46
@@ -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<SocketAddr>,
|
||||
@@ -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::<Sha256>::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<Vec<u8>> {
|
||||
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<u8> {
|
||||
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",
|
||||
|
||||
+1
-1
@@ -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.";
|
||||
|
||||
|
||||
+52
-5
@@ -12,10 +12,24 @@ pub mod code2;
|
||||
|
||||
pub mod history;
|
||||
|
||||
#[derive(Clone, serde::Deserialize, serde::Serialize)]
|
||||
struct SubmissionResponse {
|
||||
message: String,
|
||||
download: Option<DownloadInfo>,
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Deserialize, serde::Serialize)]
|
||||
struct DownloadInfo {
|
||||
href: String,
|
||||
filename: String,
|
||||
title: String,
|
||||
description: String,
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn submit_code(code: String) -> Result<String, ServerFnError> {
|
||||
async fn submit_code(code: String) -> Result<Option<SubmissionResponse>, 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<String, ServerFnError> {
|
||||
.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! {
|
||||
<a
|
||||
class="download-card"
|
||||
href=download.href
|
||||
download=download.filename
|
||||
>
|
||||
<span class="download-title">{download.title}</span>
|
||||
<span class="download-description">{download.description}</span>
|
||||
</a>
|
||||
}
|
||||
});
|
||||
|
||||
view! {
|
||||
<h1>"Submission result"</h1>
|
||||
<p class="intro">{response.message}</p>
|
||||
{download}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn SubmitPage() -> impl IntoView {
|
||||
let params = use_params_map();
|
||||
@@ -90,9 +134,12 @@ pub fn SubmitPage() -> impl IntoView {
|
||||
<Suspense fallback=|| view! { <p class="loading">"Checking code..."</p> }>
|
||||
{move || Suspend::new(async move {
|
||||
match submission.await {
|
||||
Ok(message) => view! {
|
||||
Ok(Some(response)) => {
|
||||
view! { <SubmissionResult response/> }.into_any()
|
||||
},
|
||||
Ok(None) => view! {
|
||||
<h1>"Submission result"</h1>
|
||||
<p class="intro">{message}</p>
|
||||
<p class="intro">"This code is not recognized."</p>
|
||||
}.into_any(),
|
||||
Err(error) => view! {
|
||||
<p class="error-message" role="alert">
|
||||
|
||||
+6
-8
@@ -6,7 +6,7 @@ pub(crate) const TEXTBEE_DEVICE_ID: &str = "TEXTBEE_DEVICE_ID";
|
||||
pub(crate) const RECIPIENT_SELF: &str = "RECIPIENT_SELF";
|
||||
pub(crate) const RECIPIENT_1: &str = "RECIPIENT_1";
|
||||
pub(crate) const RECIPIENT_2: &str = "RECIPIENT_2";
|
||||
pub(crate) const CODE1_HMAC_SECRET: &str = "CODE1_HMAC_SECRET";
|
||||
pub(crate) const CODE1_SECRET: &str = "CODE1_SECRET";
|
||||
|
||||
static RUNTIME_SECRETS: OnceLock<RuntimeSecrets> = OnceLock::new();
|
||||
|
||||
@@ -17,7 +17,7 @@ pub(crate) struct RuntimeSecrets {
|
||||
pub(crate) recipient_self: String,
|
||||
pub(crate) recipient_1: String,
|
||||
pub(crate) recipient_2: String,
|
||||
pub(crate) code1_hmac_secret: String,
|
||||
pub(crate) code1_secret: String,
|
||||
}
|
||||
|
||||
pub fn init() -> Result<(), String> {
|
||||
@@ -53,12 +53,10 @@ impl RuntimeSecrets {
|
||||
let recipient_self = required_recipient(RECIPIENT_SELF)?;
|
||||
let recipient_1 = required_recipient(RECIPIENT_1)?;
|
||||
let recipient_2 = required_recipient(RECIPIENT_2)?;
|
||||
let code1_hmac_secret = required(CODE1_HMAC_SECRET)?;
|
||||
let code1_secret = required(CODE1_SECRET)?;
|
||||
|
||||
if code1_hmac_secret.len() < 32 {
|
||||
return Err(format!(
|
||||
"{CODE1_HMAC_SECRET} must contain at least 32 bytes"
|
||||
));
|
||||
if code1_secret.len() < 32 {
|
||||
return Err(format!("{CODE1_SECRET} must contain at least 32 bytes"));
|
||||
}
|
||||
|
||||
let textbee_device_id = env::var(TEXTBEE_DEVICE_ID)
|
||||
@@ -72,7 +70,7 @@ impl RuntimeSecrets {
|
||||
recipient_self,
|
||||
recipient_1,
|
||||
recipient_2,
|
||||
code1_hmac_secret,
|
||||
code1_secret,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,6 +193,34 @@ h1 {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.download-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin: 24px 0;
|
||||
padding: 18px 20px;
|
||||
border: 1px solid rgba(124, 179, 255, 0.35);
|
||||
border-radius: 14px;
|
||||
background: rgba(31, 111, 235, 0.12);
|
||||
color: #e6edf7;
|
||||
text-decoration: none;
|
||||
transition: background-color 150ms ease, border-color 150ms ease;
|
||||
}
|
||||
|
||||
.download-card:hover {
|
||||
border-color: rgba(124, 179, 255, 0.7);
|
||||
background: rgba(31, 111, 235, 0.2);
|
||||
}
|
||||
|
||||
.download-title {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.download-description {
|
||||
color: #a9b6c9;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.page-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user