···22 import { Store } from "/src/utils/store.ts";
3344 // For testing (to easily go to home or signup page). You only need to uncomment the right line, run it once, and comment it out again right after
55- // await Store.reset();
55+ await Store.reset();
66 // await Store.set("user_id", "adummyuserid");
7788+ // await alert(appLocalDataDirPath); // ~/.local/share/dev.azom.privacypin
89 if (await Store.isLoggedIn()) {
910 window.location.href = "/src/home-page/home.html";
1011 } else {
···1111serde = { version = "1.0.228", features = ["derive"] }
1212serde_json = "1.0.145"
1313tokio = { version = "1.48.0", features = ["full"] }
1414-tower-http = {version="0.6.6", features=["cors"]}
1414+tower-http = {version="0.6.6", features=["cors", "trace"]}
1515+tracing = "0.1.44"
1616+tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
+99
server/src/auth.rs
···11+use axum::{body::Body, extract::State, http::Request, middleware::Next};
22+use base64::{Engine, prelude::BASE64_STANDARD};
33+use ed25519_dalek::Signature;
44+55+use crate::{
66+ ReqBail, SrvErr,
77+ types::{AppState, AuthData},
88+};
99+1010+pub async fn auth_test(
1111+ State(state): State<AppState>,
1212+ req: Request<Body>,
1313+ next: Next,
1414+) -> Result<axum::response::Response, SrvErr> {
1515+ let endpoint = req.uri().path().to_owned();
1616+ if endpoint != "/create-account" {
1717+ // CURSED STUFF BEGIN
1818+ let (parts, body) = req.into_parts();
1919+ let body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap();
2020+ let new_body = Body::from(body_bytes.clone());
2121+ let mut req = Request::from_parts(parts, new_body);
2222+ // CURSED STUFF END
2323+2424+ let auth_header = req
2525+ .headers()
2626+ .get("x-auth")
2727+ .and_then(|v| v.to_str().ok())
2828+ .ok_or(SrvErr!("missing x-auth header"))?;
2929+3030+ let decoded_auth = BASE64_STANDARD
3131+ .decode(auth_header)
3232+ .map_err(|e| SrvErr!("invalid base64 in x-auth header", e))?;
3333+3434+ let auth_str = String::from_utf8(decoded_auth)
3535+ .map_err(|e| SrvErr!("invalid utf8 in x-auth header", e))?;
3636+3737+ let auth_data: AuthData = serde_json::from_str(&auth_str)
3838+ .map_err(|e| SrvErr!("failed to parse x-auth JSON", e))?;
3939+4040+ let users = state.users.lock().await;
4141+ let user_id = auth_data.user_id;
4242+ let user = users
4343+ .iter()
4444+ .find(|u| u.id == user_id)
4545+ .ok_or(SrvErr!("User not found"))?;
4646+ let verifying_key = user.pub_key.clone();
4747+4848+ // NOTE (key chaining):
4949+ // Do NOT drop the `users` lock until after both steps are complete:
5050+ // 1) verify the request using the current stored key
5151+ // 2) update the stored key to the next key from this request
5252+ //
5353+ // If we unlock in between, a replay/duplicate of the same request can race:
5454+ //
5555+ // - Request A reads pk_i and starts verifying
5656+ // - Attacker replays A (same signature) while A is still verifying
5757+ // - Replay gets verified against pk_i and proceeds to update pk -> pk_attacker
5858+ // - Request A finishes and updates pk again, but the replay has already
5959+ // been accepted and may have advanced the key to an attacker-chosen value
6060+ //
6161+ // Keeping the lock across "verify + update" makes the transition atomic.
6262+ drop(users);
6363+6464+ ////////////////////////////////////
6565+ //////////////////////////////////// unsure
6666+ ////////////////////////////////////
6767+6868+ let sig_vec = BASE64_STANDARD
6969+ .decode(&auth_data.signature)
7070+ .map_err(|e| SrvErr!("base64 decode fail", e))?;
7171+ let sig_bytes: [u8; 64] = sig_vec
7272+ .try_into()
7373+ .map_err(|e| SrvErr!("invalid signature length", e))?;
7474+ let signature = Signature::from_bytes(&sig_bytes);
7575+7676+ if let Err(err) = verifying_key.verify_strict(&body_bytes, &signature) {
7777+ panic!("Signature verification failed: {err}");
7878+ }
7979+8080+ ////////////////////////////////////
8181+ ////////////////////////////////////
8282+ ////////////////////////////////////
8383+8484+ // TODO: Make the endpoints as enums at some point
8585+ if endpoint == "/generate-signup-key" {
8686+ let admin_id = state.admin_id.lock().await;
8787+ if admin_id.as_ref() != Some(&user_id) {
8888+ ReqBail!("not allowed: admin only");
8989+ }
9090+ }
9191+9292+ req.extensions_mut().insert(user_id); // pass user_id to the actual request handler, whatever it is, in handlers.rs
9393+ *req.body_mut() = Body::from(body_bytes);
9494+9595+ return Ok(next.run(req).await);
9696+ }
9797+9898+ return Ok(next.run(req).await);
9999+}
+17-23
server/src/handlers.rs
···33use ed25519_dalek::VerifyingKey;
44use nanoid::nanoid;
5566-use crate::types::*;
77-88-macro_rules! my_err {
99- ($msg:expr) => {
1010- Err(MyErr($msg))
1111- };
1212-}
66+use crate::{ReqBail, SrvErr, types::*};
137148pub async fn create_user(
159 State(state): State<AppState>, // TODO: some time ago, I change this (and all other handlers) to State(state): State<Arc<AppState>> no idea if I actually need it
1610 Json(payload): Json<CreateUserRequest>,
1717-) -> Result<Json<CreateAccountResponse>, MyErr> {
1111+) -> Result<Json<CreateAccountResponse>, SrvErr> {
1812 let key_used = { state.signup_keys.lock().await.remove(&payload.signup_key) };
19132014 if !key_used {
2121- return my_err!("Signup key was not there");
1515+ ReqBail!("Signup key was not there");
2216 }
23172418 // todo check
2519 let pub_key_bytes = match BASE64_STANDARD.decode(&payload.pub_key_b64) {
2620 Ok(b) => b,
2727- Err(_) => return my_err!("Invalid base64 public key"),
2121+ Err(_) => ReqBail!("Invalid base64 public key"),
2822 };
29233024 // todo check
3125 let pub_key = match VerifyingKey::from_bytes(
3226 &pub_key_bytes
3327 .try_into()
3434- .map_err(|_| MyErr("Invalid pubkey length".into()))?,
2828+ .map_err(|_| SrvErr("Invalid pubkey length".into()))?,
3529 ) {
3630 Ok(pk) => pk,
3737- Err(_) => return my_err!("Invalid public key bytes"),
3131+ Err(_) => ReqBail!("Invalid public key bytes"),
3832 };
39334034 let user_id = nanoid!(5);
···5448 return Ok(Json(CreateAccountResponse { user_id, is_admin }));
5549}
56505757-pub async fn generate_signup_key(State(state): State<AppState>) -> Result<String, MyErr> {
5151+pub async fn generate_signup_key(State(state): State<AppState>) -> Result<String, SrvErr> {
5852 let new_signup_key = nanoid!(5);
5953 let mut signup_keys = state.signup_keys.lock().await;
6054···6862 State(state): State<AppState>,
6963 Extension(user_id): Extension<String>,
7064 accepter_id: String,
7171-) -> Result<(), MyErr> {
6565+) -> Result<(), SrvErr> {
7266 if accepter_id == user_id {
7373- return my_err!("Cannot friend yourself");
6767+ ReqBail!("Cannot friend yourself");
7468 }
75697670 let mut friend_requests = state.friend_requests.lock().await;
7771 let link = Link::new(accepter_id, user_id);
7872 if friend_requests.contains(&link) {
7979- return my_err!("Friend request already exists");
7373+ ReqBail!("Friend request already exists");
8074 }
8175 friend_requests.insert(link);
8276 return Ok(());
···8680 State(state): State<AppState>,
8781 Extension(user_id): Extension<String>,
8882 sender_id: String,
8989-) -> Result<(), MyErr> {
8383+) -> Result<(), SrvErr> {
9084 let link = Link::new(user_id, sender_id);
91859286 let friend_request_accepted = { state.friend_requests.lock().await.remove(&link) };
93879488 if !friend_request_accepted {
9595- return my_err!("Friend request not found");
8989+ ReqBail!("Friend request not found");
9690 }
97919892 let mut pings_state = state.positions.lock().await;
···109103 State(state): State<AppState>,
110104 Extension(user_id): Extension<String>,
111105 friend_id: String,
112112-) -> Result<PlainBool, MyErr> {
106106+) -> Result<PlainBool, SrvErr> {
113107 let link = Link::new(friend_id, user_id);
114108 let links = state.links.lock().await;
115109 let accepted = links.contains(&link);
···120114 State(state): State<AppState>,
121115 Extension(user_id): Extension<String>,
122116 Json(pings): Json<Vec<PingPayload>>,
123123-) -> Result<(), MyErr> {
117117+) -> Result<(), SrvErr> {
124118 let links = state.links.lock().await;
125119 for ping in &pings {
126120 let link = Link::new(user_id.clone(), ping.receiver_id.clone());
127121 if !links.contains(&link) {
128128- return my_err!("Ping receiver is not linked to sender");
122122+ ReqBail!("Ping receiver is not linked to sender");
129123 }
130124 }
131125 drop(links);
···147141 State(state): State<AppState>,
148142 Extension(user_id): Extension<String>,
149143 sender_id: String,
150150-) -> Result<EncryptedPingVec, MyErr> {
144144+) -> Result<EncryptedPingVec, SrvErr> {
151145 let link = Link::new(user_id, sender_id);
152146 let links = state.links.lock().await;
153147154148 if !links.contains(&link) {
155155- return my_err!("No link exists between these users");
149149+ ReqBail!("No link exists between these users");
156150 }
157151 drop(links);
158152
+208
server/src/log.rs
···11+use std::{
22+ sync::atomic::{AtomicU64, Ordering},
33+ time::Instant,
44+};
55+66+use axum::{
77+ body::{Body, Bytes, to_bytes},
88+ http::{HeaderMap, Method, Request},
99+ middleware::Next,
1010+ response::Response,
1111+};
1212+use base64::{Engine, prelude::BASE64_STANDARD};
1313+use serde_json::Value;
1414+1515+static REQ_SEQ: AtomicU64 = AtomicU64::new(1);
1616+1717+fn format_x_auth(headers: &HeaderMap) -> Option<String> {
1818+ let v = headers.get("x-auth")?;
1919+ let s = v.to_str().ok()?.to_string();
2020+2121+ const MAX: usize = 120;
2222+ if s.len() > MAX {
2323+ Some(format!("{}… (len={})", &s[..MAX], s.len()))
2424+ } else {
2525+ Some(s)
2626+ }
2727+}
2828+2929+fn status_emoji(status: axum::http::StatusCode) -> &'static str {
3030+ if status.is_success() {
3131+ "✅"
3232+ } else if status.is_redirection() {
3333+ "↪"
3434+ } else if status.is_client_error() {
3535+ "⚠"
3636+ } else if status.is_server_error() {
3737+ "❌"
3838+ } else {
3939+ "ℹ"
4040+ }
4141+}
4242+4343+/// Convert JSON into a "key: value" style display.
4444+/// - Objects: `key: value` (nested objects/arrays are indented)
4545+/// - Arrays: `- item` (nested indented)
4646+/// If not JSON: UTF-8 text, else base64.
4747+fn body_as_kv(bytes: &Bytes) -> String {
4848+ if bytes.is_empty() {
4949+ return "<empty>".to_string();
5050+ }
5151+5252+ if let Ok(v) = serde_json::from_slice::<Value>(bytes) {
5353+ let mut out = String::new();
5454+ write_value(&mut out, &v, 0);
5555+ return out.trim_end().to_string();
5656+ }
5757+5858+ match std::str::from_utf8(bytes) {
5959+ Ok(s) => s.to_string(),
6060+ Err(_) => format!("<non-utf8; base64>\n{}", BASE64_STANDARD.encode(bytes)),
6161+ }
6262+}
6363+6464+fn write_value(out: &mut String, v: &Value, indent: usize) {
6565+ match v {
6666+ Value::Object(map) => {
6767+ for (k, val) in map {
6868+ write_key_value(out, k, val, indent);
6969+ }
7070+ }
7171+ Value::Array(arr) => {
7272+ for item in arr {
7373+ write_array_item(out, item, indent);
7474+ }
7575+ }
7676+ _ => {
7777+ // Root primitive
7878+ out.push_str(&indent_str(indent));
7979+ out.push_str(&format_primitive(v));
8080+ out.push('\n');
8181+ }
8282+ }
8383+}
8484+8585+fn write_key_value(out: &mut String, key: &str, val: &Value, indent: usize) {
8686+ let pad = indent_str(indent);
8787+8888+ match val {
8989+ Value::Object(_) | Value::Array(_) => {
9090+ out.push_str(&pad);
9191+ out.push_str(key);
9292+ out.push_str(":\n");
9393+ write_value(out, val, indent + 2);
9494+ }
9595+ _ => {
9696+ out.push_str(&pad);
9797+ out.push_str(key);
9898+ out.push_str(": ");
9999+ out.push_str(&format_primitive(val));
100100+ out.push('\n');
101101+ }
102102+ }
103103+}
104104+105105+fn write_array_item(out: &mut String, item: &Value, indent: usize) {
106106+ let pad = indent_str(indent);
107107+108108+ match item {
109109+ Value::Object(_) | Value::Array(_) => {
110110+ out.push_str(&pad);
111111+ out.push_str("-\n");
112112+ write_value(out, item, indent + 2);
113113+ }
114114+ _ => {
115115+ out.push_str(&pad);
116116+ out.push_str("- ");
117117+ out.push_str(&format_primitive(item));
118118+ out.push('\n');
119119+ }
120120+ }
121121+}
122122+123123+fn format_primitive(v: &Value) -> String {
124124+ match v {
125125+ Value::String(s) => s.clone(),
126126+ Value::Number(n) => n.to_string(),
127127+ Value::Bool(b) => b.to_string(),
128128+ Value::Null => "null".to_string(),
129129+ // Shouldn’t happen here (we route these elsewhere), but safe fallback
130130+ Value::Object(_) | Value::Array(_) => "<complex>".to_string(),
131131+ }
132132+}
133133+134134+fn indent_str(spaces: usize) -> String {
135135+ " ".repeat(spaces)
136136+}
137137+138138+fn indent_block(s: &str, spaces: usize) -> String {
139139+ let pad = " ".repeat(spaces);
140140+ s.lines()
141141+ .map(|line| format!("{pad}{line}\n"))
142142+ .collect::<String>()
143143+ .trim_end_matches('\n')
144144+ .to_string()
145145+}
146146+147147+pub async fn log_req_res_bodies(req: Request<Body>, next: Next) -> Response {
148148+ // Avoid noisy CORS preflight logs
149149+ if req.method() == Method::OPTIONS {
150150+ return next.run(req).await;
151151+ }
152152+153153+ let id = REQ_SEQ.fetch_add(1, Ordering::Relaxed);
154154+ let start = Instant::now();
155155+156156+ let method = req.method().clone();
157157+ let uri = req.uri().clone();
158158+ let req_headers = req.headers().clone();
159159+160160+ // Read + restore request body
161161+ let (req_parts, req_body) = req.into_parts();
162162+ let req_bytes = to_bytes(req_body, usize::MAX).await.unwrap();
163163+ let req = Request::from_parts(req_parts, Body::from(req_bytes.clone()));
164164+165165+ // Run handler
166166+ let res = next.run(req).await;
167167+168168+ let status = res.status();
169169+ let res_headers = res.headers().clone();
170170+171171+ // Read + restore response body
172172+ let (res_parts, res_body) = res.into_parts();
173173+ let res_bytes = to_bytes(res_body, usize::MAX).await.unwrap();
174174+ let res = Response::from_parts(res_parts, Body::from(res_bytes.clone()));
175175+176176+ let ms = start.elapsed().as_millis();
177177+ let sep = "════════════════════════════════════════════════════════════════";
178178+179179+ let mut out = String::new();
180180+ out.push('\n');
181181+ out.push_str(sep);
182182+ out.push('\n');
183183+ out.push_str(&format!(
184184+ "🚦 #{id} {method} {uri} {} {status} ⏱ {ms}ms\n",
185185+ status_emoji(status)
186186+ ));
187187+188188+ out.push('\n');
189189+190190+ out.push_str("📥 Request\n");
191191+ if let Some(xauth) = format_x_auth(&req_headers) {
192192+ out.push_str(&format!(" 🔐 x-auth: {xauth}\n"));
193193+ }
194194+ out.push_str(&indent_block(&body_as_kv(&req_bytes), 2));
195195+ out.push('\n');
196196+197197+ out.push_str("📤 Response\n");
198198+ out.push_str(&indent_block(&body_as_kv(&res_bytes), 2));
199199+ out.push('\n');
200200+201201+ out.push_str(sep);
202202+ out.push('\n');
203203+204204+ tracing::info!("{out}");
205205+206206+ let _ = res_headers;
207207+ res
208208+}
+19-85
server/src/main.rs
···11use std::{collections::HashMap, sync::Arc};
2233-use axum::{
44- Router,
55- body::{Body, to_bytes},
66- extract::{Request, State},
77- middleware::Next,
88- response::IntoResponse,
99- routing::post,
1010-};
1111-use base64::{Engine, prelude::BASE64_STANDARD};
1212-use ed25519_dalek::Signature;
33+use axum::{Router, routing::post};
134use nanoid::nanoid;
1414-use serde::Deserialize;
155use std::collections::HashSet;
166use tokio::sync::Mutex;
1717-use tower_http::cors::{Any, CorsLayer};
77+use tower_http::cors::CorsLayer;
88+use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
1891010+mod auth;
1911mod handlers;
1212+mod log;
2013mod types;
21142215use handlers::*;
2316use types::*;
1717+1818+use crate::auth::auth_test;
1919+use crate::log::log_req_res_bodies;
24202521#[tokio::main]
2622async fn main() {
2323+ tracing_subscriber::registry()
2424+ .with(
2525+ tracing_subscriber::EnvFilter::try_from_default_env()
2626+ .unwrap_or_else(|_| "info,tower_http=info,axum=info".into()),
2727+ )
2828+ .with(tracing_subscriber::fmt::layer())
2929+ .init();
3030+2731 // TODO: should this be inside an Arc?
2832 let state = AppState {
2933 users: Arc::new(Mutex::new(Vec::new())),
···5660 .route("/get-pings", post(get_pings))
5761 .with_state(state.clone())
5862 .layer(CorsLayer::permissive())
5959- .layer(axum::middleware::from_fn_with_state(state, auth_test));
6363+ .layer(axum::middleware::from_fn_with_state(state, auth_test))
6464+ .layer(axum::middleware::from_fn(log_req_res_bodies));
60656166 let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
6267 axum::serve(listener, app).await.unwrap();
6368}
64696565-async fn auth_test(State(state): State<AppState>, req: Request, next: Next) -> impl IntoResponse {
6666- let endpoint = req.uri().path().to_owned();
6767- if endpoint != "/create-account" {
6868- // CURSED STUFF BEGIN
6969- let (parts, body) = req.into_parts();
7070- let body_bytes = to_bytes(body, usize::MAX).await.unwrap();
7171- let new_body = Body::from(body_bytes.clone());
7272- let mut req = Request::from_parts(parts, new_body);
7373- // CURSED STUFF END
7474-7575- let auth_header = req
7676- .headers()
7777- .get("x-auth")
7878- .and_then(|v| v.to_str().ok())
7979- .unwrap_or_else(|| panic!("missing x-auth header"));
8080-8181- let decoded_auth = BASE64_STANDARD
8282- .decode(auth_header)
8383- .unwrap_or_else(|_| panic!("invalid base64 in x-auth header"));
8484-8585- let auth_str = String::from_utf8(decoded_auth)
8686- .unwrap_or_else(|_| panic!("invalid utf8 in x-auth header"));
8787-8888- let auth_data: Auth = serde_json::from_str(&auth_str)
8989- .unwrap_or_else(|e| panic!("failed to parse x-auth JSON: {e}"));
9090-9191- let users = state.users.lock().await;
9292- let user_id = auth_data.user_id;
9393- let user = users
9494- .iter()
9595- .find(|u| u.id == user_id)
9696- .unwrap_or_else(|| panic!("User not found"));
9797- let verifying_key = user.pub_key.clone();
9898- drop(users);
9999-100100- ////////////////////////////////////
101101- //////////////////////////////////// unsure
102102- ////////////////////////////////////
103103-104104- let sig_vec = BASE64_STANDARD.decode(&auth_data.signature).unwrap();
105105- let sig_bytes: [u8; 64] = sig_vec.try_into().expect("invalid signature length");
106106- let signature = Signature::from_bytes(&sig_bytes);
107107-108108- if let Err(err) = verifying_key.verify_strict(&body_bytes, &signature) {
109109- panic!("Signature verification failed: {err}");
110110- }
111111-112112- ////////////////////////////////////
113113- ////////////////////////////////////
114114- ////////////////////////////////////
115115-116116- // TODO: Maybe make the endpoints an enum
117117- if endpoint == "/generate-signup-key" {
118118- let admin_id = state.admin_id.lock().await;
119119- if admin_id.as_ref() != Some(&user_id) {
120120- todo!("not allowed")
121121- }
122122- }
123123-124124- req.extensions_mut().insert(user_id);
125125-126126- return next.run(req).await;
127127- }
128128-129129- return next.run(req).await;
130130-}
131131-132132-#[derive(Debug, Deserialize, Clone)]
133133-struct Auth {
134134- user_id: String,
135135- signature: String,
136136-}
7070+// TODO: potential security risk of returning error messages directly to the user. nice for debugging tho :p