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
}>
{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! {
diff --git a/src/secrets.rs b/src/secrets.rs
index 1ad7523..ad7de8a 100644
--- a/src/secrets.rs
+++ b/src/secrets.rs
@@ -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 = 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,
})
}
}
diff --git a/style/main.scss b/style/main.scss
index ef73ec0..cc6d621 100644
--- a/style/main.scss
+++ b/style/main.scss
@@ -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;