Self-hosted, federated location sharing app and server that prioritizes user privacy and security
end-to-end-encryption location-sharing privacy self-hosted federated

very basic auth should be working

azom.dev a7a0ec13 3b0d8e01

verified
+18 -17
app/bun.lock
··· 1 1 { 2 2 "lockfileVersion": 1, 3 + "configVersion": 0, 3 4 "workspaces": { 4 5 "": { 5 6 "name": "privacypin", 6 7 "dependencies": { 7 - "@tauri-apps/api": "^2", 8 - "@tauri-apps/plugin-opener": "^2", 8 + "@tauri-apps/api": "^2.9.0", 9 + "@tauri-apps/plugin-opener": "^2.5.2", 9 10 "@tauri-apps/plugin-store": "^2.4.1", 10 11 "alpinejs": "^3.15.1", 11 12 }, 12 13 "devDependencies": { 13 - "@tauri-apps/cli": "^2", 14 + "@tauri-apps/cli": "^2.9.4", 14 15 "@types/alpinejs": "^3.13.11", 15 - "typescript": "~5.6.2", 16 - "vite": "^6.0.3", 16 + "typescript": "~5.6.3", 17 + "vite": "^6.4.1", 17 18 }, 18 19 }, 19 20 }, ··· 116 117 117 118 "@tauri-apps/api": ["@tauri-apps/api@2.9.0", "", {}, "sha512-qD5tMjh7utwBk9/5PrTA/aGr3i5QaJ/Mlt7p8NilQ45WgbifUNPyKWsA63iQ8YfQq6R8ajMapU+/Q8nMcPRLNw=="], 118 119 119 - "@tauri-apps/cli": ["@tauri-apps/cli@2.9.3", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.9.3", "@tauri-apps/cli-darwin-x64": "2.9.3", "@tauri-apps/cli-linux-arm-gnueabihf": "2.9.3", "@tauri-apps/cli-linux-arm64-gnu": "2.9.3", "@tauri-apps/cli-linux-arm64-musl": "2.9.3", "@tauri-apps/cli-linux-riscv64-gnu": "2.9.3", "@tauri-apps/cli-linux-x64-gnu": "2.9.3", "@tauri-apps/cli-linux-x64-musl": "2.9.3", "@tauri-apps/cli-win32-arm64-msvc": "2.9.3", "@tauri-apps/cli-win32-ia32-msvc": "2.9.3", "@tauri-apps/cli-win32-x64-msvc": "2.9.3" }, "bin": { "tauri": "tauri.js" } }, "sha512-BQ7iLUXTQcyG1PpzLWeVSmBCedYDpnA/6Cm/kRFGtqjTf/eVUlyYO5S2ee07tLum3nWwDBWTGFZeruO8yEukfA=="], 120 + "@tauri-apps/cli": ["@tauri-apps/cli@2.9.4", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.9.4", "@tauri-apps/cli-darwin-x64": "2.9.4", "@tauri-apps/cli-linux-arm-gnueabihf": "2.9.4", "@tauri-apps/cli-linux-arm64-gnu": "2.9.4", "@tauri-apps/cli-linux-arm64-musl": "2.9.4", "@tauri-apps/cli-linux-riscv64-gnu": "2.9.4", "@tauri-apps/cli-linux-x64-gnu": "2.9.4", "@tauri-apps/cli-linux-x64-musl": "2.9.4", "@tauri-apps/cli-win32-arm64-msvc": "2.9.4", "@tauri-apps/cli-win32-ia32-msvc": "2.9.4", "@tauri-apps/cli-win32-x64-msvc": "2.9.4" }, "bin": { "tauri": "tauri.js" } }, "sha512-pvylWC9QckrOS9ATWXIXcgu7g2hKK5xTL5ZQyZU/U0n9l88SEFGcWgLQNa8WZmd+wWIOWhkxOFcOl3i6ubDNNw=="], 120 121 121 - "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.9.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W8FQXZXQmQ0Fmj9UJXNrm2mLdIaLLriKVY7o/FzmizyIKTPIvHjfZALTNybbpTQRbJvKoGHLrW1DNzAWVDWJYg=="], 122 + "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9rHkMVtbMhe0AliVbrGpzMahOBg3rwV46JYRELxR9SN6iu1dvPOaMaiC4cP6M/aD1424ziXnnMdYU06RAH8oIw=="], 122 123 123 - "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.9.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-zDwu40rlshijt3TU6aRvzPUyVpapsx1sNfOlreDMTaMelQLHl6YoQzSRpLHYwrHrhimxyX2uDqnKIiuGel0Lhg=="], 124 + "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-VT9ymNuT06f5TLjCZW2hfSxbVtZDhORk7CDUDYiq5TiSYQdxkl8MVBy0CCFFcOk4QAkUmqmVUA9r3YZ/N/vPRQ=="], 124 125 125 - "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.9.3", "", { "os": "linux", "cpu": "arm" }, "sha512-+Oc2OfcTRwYtW93VJqd/HOk77buORwC9IToj/qsEvM7bTMq6Kda4alpZprzwrCHYANSw+zD8PgjJdljTpe4p+g=="], 126 + "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.9.4", "", { "os": "linux", "cpu": "arm" }, "sha512-tTWkEPig+2z3Rk0zqZYfjUYcgD+aSm72wdrIhdYobxbQZOBw0zfn50YtWv+av7bm0SHvv75f0l7JuwgZM1HFow=="], 126 127 127 - "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.9.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-59GqU/J1n9wFyAtleoQOaU0oVIo+kwQynEw4meFDoKRXszKGor6lTsbsS3r0QKLSPbc0o/yYGJhqqCtkYjb/eg=="], 128 + "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-ql6vJ611qoqRYHxkKPnb2vHa27U+YRKRmIpLMMBeZnfFtZ938eao7402AQCH1mO2+/8ioUhbpy9R/ZcLTXVmkg=="], 128 129 129 - "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.9.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-fzvG+jEn5/iYGNH6Z2IRMheYFC4pJdXa19BR9fFm6Bdn2cuajRLDKdUcEME/DCtwqclphXtFZTrT4oezY5vI/A=="], 130 + "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-vg7yNn7ICTi6hRrcA/6ff2UpZQP7un3xe3SEld5QM0prgridbKAiXGaCKr3BnUBx/rGXegQlD/wiLcWdiiraSw=="], 130 131 131 - "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.9.3", "", { "os": "linux", "cpu": "none" }, "sha512-qV8DZXI/fZwawk6T3Th1g6smiNC2KeQTk7XFgKvqZ6btC01z3UTsQmNGvI602zwm3Ld1TBZb4+rEWu2QmQimmw=="], 132 + "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.9.4", "", { "os": "linux", "cpu": "none" }, "sha512-l8L+3VxNk6yv5T/Z/gv5ysngmIpsai40B9p6NQQyqYqxImqYX37pqREoEBl1YwG7szGnDibpWhidPrWKR59OJA=="], 132 133 133 - "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.9.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tquyEONCNRfqEBWEe4eAHnxFN5yY5lFkCuD4w79XLIovUxVftQ684+xLp7zkhntkt4y20SMj2AgJa/+MOlx4Kg=="], 134 + "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-PepPhCXc/xVvE3foykNho46OmCyx47E/aG676vKTVp+mqin5d+IBqDL6wDKiGNT5OTTxKEyNlCQ81Xs2BQhhqA=="], 134 135 135 - "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.9.3", "", { "os": "linux", "cpu": "x64" }, "sha512-v2cBIB/6ji8DL+aiL5QUykU3ZO8OoJGyx50/qv2HQVzkf85KdaYSis3D/oVRemN/pcDz+vyCnnL3XnzFnDl4JQ=="], 136 + "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-zcd1QVffh5tZs1u1SCKUV/V7RRynebgYUNWHuV0FsIF1MjnULUChEXhAhug7usCDq4GZReMJOoXa6rukEozWIw=="], 136 137 137 - "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.9.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZGvBy7nvrHPbE0HeKp/ioaiw8bNgAHxWnb7JRZ4/G0A+oFj0SeSFxl9k5uU6FKnM7bHM23Gd1oeaDex9g5Fceg=="], 138 + "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-/7ZhnP6PY04bEob23q8MH/EoDISdmR1wuNm0k9d5HV7TDMd2GGCDa8dPXA4vJuglJKXIfXqxFmZ4L+J+MO42+w=="], 138 139 139 - "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.9.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-UsgIwOnpCoY9NK9/65QiwgmWVIE80LE7SwRYVblGtmlY9RYfsYvpbItwsovA/AcHMTiO+OCvS/q9yLeqS3m6Sg=="], 140 + "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.9.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-1LmAfaC4Cq+3O1Ir1ksdhczhdtFSTIV51tbAGtbV/mr348O+M52A/xwCCXQank0OcdBxy5BctqkMtuZnQvA8uQ=="], 140 141 141 - "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.9.3", "", { "os": "win32", "cpu": "x64" }, "sha512-fmw7NrrHE5m49idCvJAx9T9bsupjdJ0a3p3DPCNCZRGANU6R1tA1L+KTlVuUtdAldX2NqU/9UPo2SCslYKgJHQ=="], 142 + "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-EdYd4c9wGvtPB95kqtEyY+bUR+k4kRw3IA30mAQ1jPH6z57AftT8q84qwv0RDp6kkEqOBKxeInKfqi4BESYuqg=="], 142 143 143 144 "@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew=="], 144 145
+2 -1
app/index.html
··· 2 2 import { Store } from "/src/utils/store.ts"; 3 3 4 4 // 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 5 - // await Store.reset(); 5 + await Store.reset(); 6 6 // await Store.set("user_id", "adummyuserid"); 7 7 8 + // await alert(appLocalDataDirPath); // ~/.local/share/dev.azom.privacypin 8 9 if (await Store.isLoggedIn()) { 9 10 window.location.href = "/src/home-page/home.html"; 10 11 } else {
+5 -5
app/package.json
··· 10 10 "tauri": "WEBKIT_DISABLE_DMABUF_RENDERER=1 tauri" 11 11 }, 12 12 "dependencies": { 13 - "@tauri-apps/api": "^2", 14 - "@tauri-apps/plugin-opener": "^2", 13 + "@tauri-apps/api": "^2.9.0", 14 + "@tauri-apps/plugin-opener": "^2.5.2", 15 15 "@tauri-apps/plugin-store": "^2.4.1", 16 16 "alpinejs": "^3.15.1" 17 17 }, 18 18 "devDependencies": { 19 - "@tauri-apps/cli": "^2", 19 + "@tauri-apps/cli": "^2.9.4", 20 20 "@types/alpinejs": "^3.13.11", 21 - "typescript": "~5.6.2", 22 - "vite": "^6.0.3" 21 + "typescript": "~5.6.3", 22 + "vite": "^6.4.1" 23 23 } 24 24 }
app/src/add-friend-page/add-friend.css

This is a binary file and will not be displayed.

app/src/add-friend-page/add-friend.html

This is a binary file and will not be displayed.

app/src/add-friend-page/add-friend.ts

This is a binary file and will not be displayed.

+4 -5
app/src/signup-page/signup.ts
··· 1 1 import Alpine from "alpinejs"; 2 2 import { createAccount } from "../utils/api.ts"; 3 - import { Store } from "../utils/store.ts"; 4 3 5 4 Alpine.data("signupPageState", () => ({ 6 5 serverAddress: "", ··· 9 8 10 9 async signup() { 11 10 this.isDoingStuff = true; 12 - await new Promise((resolve) => setTimeout(resolve, 2000)); // temp 11 + await new Promise((resolve) => setTimeout(resolve, 1000)); // temp 13 12 try { 14 - const res = await createAccount(this.serverAddress, this.signupKey); 15 - Store.set("is_admin", res.is_admin); 16 - Store.set("user_id", res.user_id); 13 + await createAccount(this.serverAddress, this.signupKey); 17 14 window.location.href = "/src/home-page/home.html"; 18 15 } catch (e) { 16 + const err = e instanceof Error ? e.message : e; 17 + alert(`Sign-up failed: ${err}`); 19 18 this.isDoingStuff = false; 20 19 } 21 20 },
+53
app/src/types.d.ts
··· 1 + // THIS FILE IS TEMPORARY UNTIL WE CAN HAVE A TYPESCRIPT VERSION THAT INCLUDE THE BASE64 <-> UINT8ARRAY CONVERSION STUFF 2 + 3 + declare global { 4 + interface Uint8Array { 5 + /** 6 + * Converts this `Uint8Array` to a Base64 or Base64URL encoded string. 7 + * 8 + * @param options Optional configuration: 9 + * - `alphabet`: Selects between `"base64"` (default) and `"base64url"` alphabets. 10 + * - `omitPadding`: If true, omits the trailing `=` padding characters. 11 + * 12 + * @returns The Base64-encoded representation of the byte array. 13 + * 14 + * @example 15 + * ```ts 16 + * const bytes = new Uint8Array([72, 101, 108, 108, 111]); 17 + * console.log(bytes.toBase64()); // "SGVsbG8=" 18 + * ``` 19 + */ 20 + toBase64(options?: { alphabet?: "base64" | "base64url"; omitPadding?: boolean }): string; 21 + } 22 + 23 + interface Uint8ArrayConstructor { 24 + /** 25 + * Creates a `Uint8Array` from a Base64 or Base64URL encoded string. 26 + * 27 + * @param base64 The input string to decode. 28 + * @param options Optional configuration: 29 + * - `alphabet`: Selects between `"base64"` (default) and `"base64url"` alphabets. 30 + * - `lastChunkHandling`: Controls how to handle incomplete input: 31 + * - `"strict"` (default): Throws an error if input is not valid Base64. 32 + * - `"loose"`: Tolerates missing padding or invalid trailing characters. 33 + * - `"stop-before-partial"`: Ignores an incomplete trailing chunk. 34 + * 35 + * @returns A new `Uint8Array` containing the decoded bytes. 36 + * 37 + * @example 38 + * ```ts 39 + * const bytes = Uint8Array.fromBase64("SGVsbG8="); 40 + * console.log(new TextDecoder().decode(bytes)); // "Hello" 41 + * ``` 42 + */ 43 + fromBase64( 44 + base64: string, 45 + options?: { 46 + alphabet?: "base64" | "base64url"; 47 + lastChunkHandling?: "loose" | "strict" | "stop-before-partial"; 48 + }, 49 + ): Uint8Array; 50 + } 51 + } 52 + 53 + export {};
+69 -60
app/src/utils/api.ts
··· 1 1 import { Store } from "./store.ts"; 2 2 3 3 function bufToBase64(buf: ArrayBuffer): string { 4 - return btoa(String.fromCharCode(...new Uint8Array(buf))); 4 + return new Uint8Array(buf).toBase64(); 5 5 } 6 6 7 - function strToBytes(str: string): Uint8Array { 8 - return new TextEncoder().encode(str); 7 + /** 8 + * This function can throw an error 9 + */ 10 + export async function createAccount(server_url: string, signup_key: string): Promise<{ user_id: string; is_admin: boolean }> { 11 + const keyPair = await crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"]); 12 + const pubKeyRaw = await crypto.subtle.exportKey("raw", keyPair.publicKey); 13 + const privKeyRaw = await crypto.subtle.exportKey("pkcs8", keyPair.privateKey); 14 + const pub_key_b64 = bufToBase64(pubKeyRaw); 15 + 16 + const response = await fetch(server_url + "/create-account", { 17 + method: "POST", 18 + headers: { "Content-Type": "application/json" }, 19 + body: JSON.stringify({ signup_key, pub_key_b64 }), 20 + }); 21 + 22 + if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`); 23 + const json = await response.json(); 24 + 25 + // TODO validate data? 26 + 27 + await Store.set("server_url", server_url); 28 + await Store.set("user_id", json.user_id); 29 + await Store.set("is_admin", json.is_admin); 30 + await Store.set("priv_key", bufToBase64(privKeyRaw)); 31 + 32 + return json; 9 33 } 10 34 11 - export async function createAccount(server_url: string, signup_key: string): Promise<{ user_id: string; is_admin: boolean }> { 12 - try { 13 - await Store.set("server_url", server_url); 14 - const keyPair = await crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"]); 15 - const pubKeyRaw = await crypto.subtle.exportKey("raw", keyPair.publicKey); 16 - const privKeyRaw = await crypto.subtle.exportKey("pkcs8", keyPair.privateKey); 17 - const pub_key_b64 = bufToBase64(pubKeyRaw); 35 + export async function generateSignupKey(): Promise<string> { 36 + return await post("generate-signup-key"); 37 + } 18 38 19 - const response = await fetch(server_url + "/create-account", { 20 - method: "POST", 21 - headers: { "Content-Type": "application/json" }, 22 - body: JSON.stringify({ signup_key, pub_key_b64 }), 23 - }); 39 + export async function createFriendRequest(friend_id: string): Promise<void> { 40 + await post("create-friend-request", friend_id); 41 + } 24 42 25 - if (!response.ok) throw new Error(await response.text()); 26 - const json = await response.json(); 43 + export async function acceptFriendRequest(friend_id: string): Promise<void> { 44 + await post("accept-friend-request", friend_id); 45 + } 27 46 28 - await Store.set("user_id", json.user_id); 29 - await Store.set("priv_key", bufToBase64(privKeyRaw)); 47 + export async function isFriendRequestAccepted(friend_id: string): Promise<boolean> { 48 + const res = await post("is-friend-request-accepted", friend_id); 49 + return res === "true"; 50 + } 30 51 31 - return json; 32 - } catch (err) { 33 - alert(`${err}`); 34 - throw err; 35 - } 52 + export async function sendPings(friend_id: string, ping: string): Promise<void> { 53 + await post("send-pings", JSON.stringify({ receiver_id: friend_id, encrypted_ping: ping })); 36 54 } 37 55 38 - export async function post(endpoint: string, data: object | string | undefined): Promise<any> { 39 - try { 40 - const user_id = await Store.get("user_id"); 41 - const server_url = await Store.get("server_url"); 42 - console.log(`Exhibit B: ${server_url}`); 43 - const privKey_b64 = await Store.get("priv_key"); 44 - 45 - if (!user_id || !privKey_b64) throw new Error("Missing user credentials"); 56 + export async function getPings(friend_id: string): Promise<string[]> { 57 + const res = await post("get-pings", friend_id); 58 + return JSON.parse(res); 59 + } 46 60 47 - // Prepare request body bytes 48 - let bodyStr = ""; 49 - if (typeof data === "object") bodyStr = JSON.stringify(data); 50 - else if (typeof data === "string") bodyStr = data; 51 - const bodyBytes = strToBytes(bodyStr); 61 + /** 62 + * This function can throw an error 63 + */ 64 + async function post(endpoint: string, body: string | undefined = undefined) { 65 + const user_id = await Store.get("user_id"); 66 + const server_url = await Store.get("server_url"); 67 + const privKey_b64 = await Store.get("priv_key"); 52 68 53 - // Import private key and sign 54 - const privKeyBytes = Uint8Array.from(atob(privKey_b64), (c) => c.charCodeAt(0)); 69 + const bodyBytes = new TextEncoder().encode(body === undefined ? "" : body); 55 70 56 - const privKey = await crypto.subtle.importKey("pkcs8", privKeyBytes.buffer, { name: "Ed25519" }, false, ["sign"]); 71 + const privKeyBytes = Uint8Array.fromBase64(privKey_b64); 57 72 58 - const signature = await crypto.subtle.sign("Ed25519", privKey, bodyBytes); 59 - const signature_b64 = bufToBase64(signature); 73 + const privKey = await crypto.subtle.importKey("pkcs8", privKeyBytes.buffer, "Ed25519", false, ["sign"]); 60 74 61 - // Encode header JSON to base64 62 - const authJson = JSON.stringify({ user_id, signature: signature_b64 }); 63 - const authHeader = btoa(authJson); 75 + const signature = await crypto.subtle.sign("Ed25519", privKey, bodyBytes); 76 + const signature_b64 = bufToBase64(signature); 64 77 65 - const headers: Record<string, string> = { 66 - "x-auth": authHeader, 67 - }; 68 - if (typeof data === "object") headers["Content-Type"] = "application/json"; 78 + const headers = { 79 + "x-auth": JSON.stringify({ user_id, signature: signature_b64 }), 80 + "Content-Type": "application/json", // TODO: not always json tho, but does it matter? 81 + }; 69 82 70 - const res = await fetch(`${server_url}/${endpoint}`, { 71 - method: "POST", 72 - headers, 73 - body: bodyStr.length > 0 ? bodyStr : undefined, 74 - }); 83 + const res = await fetch(`${server_url}/${endpoint}`, { 84 + method: "POST", 85 + headers, 86 + body: bodyBytes, // TODO: do we need to send bodyBytes instead to match server side auth? 87 + }); 75 88 76 - if (!res.ok) throw new Error(await res.text()); 77 - return await res.text(); 78 - } catch (err) { 79 - alert(`${err}`); 80 - throw err; 81 - } 89 + if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`); 90 + return await res.text(); 82 91 }
+190 -60
server/Cargo.lock
··· 3 3 version = 4 4 4 5 5 [[package]] 6 + name = "aho-corasick" 7 + version = "1.1.4" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" 10 + dependencies = [ 11 + "memchr", 12 + ] 13 + 14 + [[package]] 6 15 name = "atomic-waker" 7 16 version = "1.1.2" 8 17 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 10 19 11 20 [[package]] 12 21 name = "axum" 13 - version = "0.8.6" 22 + version = "0.8.8" 14 23 source = "registry+https://github.com/rust-lang/crates.io-index" 15 - checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" 24 + checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" 16 25 dependencies = [ 17 26 "axum-core", 18 27 "bytes", ··· 43 52 44 53 [[package]] 45 54 name = "axum-core" 46 - version = "0.5.5" 55 + version = "0.5.6" 47 56 source = "registry+https://github.com/rust-lang/crates.io-index" 48 - checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" 57 + checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" 49 58 dependencies = [ 50 59 "bytes", 51 60 "futures-core", ··· 83 92 84 93 [[package]] 85 94 name = "bytes" 86 - version = "1.10.1" 95 + version = "1.11.0" 87 96 source = "registry+https://github.com/rust-lang/crates.io-index" 88 - checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 97 + checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" 89 98 90 99 [[package]] 91 100 name = "cfg-if" ··· 110 119 111 120 [[package]] 112 121 name = "crypto-common" 113 - version = "0.2.0-rc.5" 122 + version = "0.2.0-rc.8" 114 123 source = "registry+https://github.com/rust-lang/crates.io-index" 115 - checksum = "919bd05924682a5480aec713596b9e2aabed3a0a6022fab6847f85a99e5f190a" 124 + checksum = "e6165b8029cdc3e765b74d3548f85999ee799d5124877ce45c2c85ca78e4d4aa" 116 125 dependencies = [ 117 126 "hybrid-array", 118 127 ] 119 128 120 129 [[package]] 121 130 name = "curve25519-dalek" 122 - version = "5.0.0-pre.1" 131 + version = "5.0.0-pre.3" 123 132 source = "registry+https://github.com/rust-lang/crates.io-index" 124 - checksum = "6f9200d1d13637f15a6acb71e758f64624048d85b31a5fdbfd8eca1e2687d0b7" 133 + checksum = "92419e1cdc506051ffd30713ad09d0ec6a24bba9197e12989de389e35b19c77a" 125 134 dependencies = [ 126 135 "cfg-if", 127 136 "cpufeatures", ··· 146 155 147 156 [[package]] 148 157 name = "der" 149 - version = "0.8.0-rc.9" 158 + version = "0.8.0-rc.10" 150 159 source = "registry+https://github.com/rust-lang/crates.io-index" 151 - checksum = "e9d8dd2f26c86b27a2a8ea2767ec7f9df7a89516e4794e54ac01ee618dda3aa4" 160 + checksum = "02c1d73e9668ea6b6a28172aa55f3ebec38507131ce179051c8033b5c6037653" 152 161 dependencies = [ 153 162 "const-oid", 154 163 ] 155 164 156 165 [[package]] 157 166 name = "digest" 158 - version = "0.11.0-rc.4" 167 + version = "0.11.0-rc.5" 159 168 source = "registry+https://github.com/rust-lang/crates.io-index" 160 - checksum = "ea390c940e465846d64775e55e3115d5dc934acb953de6f6e6360bc232fe2bf7" 169 + checksum = "ebf9423bafb058e4142194330c52273c343f8a5beb7176d052f0e73b17dd35b9" 161 170 dependencies = [ 162 171 "block-buffer", 163 172 "crypto-common", ··· 175 184 176 185 [[package]] 177 186 name = "ed25519-dalek" 178 - version = "3.0.0-pre.1" 187 + version = "3.0.0-pre.3" 179 188 source = "registry+https://github.com/rust-lang/crates.io-index" 180 - checksum = "ad207ed88a133091f83224265eac21109930db09bedcad05d5252f2af2de20a1" 189 + checksum = "5d6d275a4ffdfc16e98fbcb5f5417214a06957c7cdc6eb2815c2dc50dce1c1dd" 181 190 dependencies = [ 182 191 "curve25519-dalek", 183 192 "ed25519", ··· 187 196 ] 188 197 189 198 [[package]] 199 + name = "errno" 200 + version = "0.3.14" 201 + source = "registry+https://github.com/rust-lang/crates.io-index" 202 + checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 203 + dependencies = [ 204 + "libc", 205 + "windows-sys 0.61.2", 206 + ] 207 + 208 + [[package]] 190 209 name = "fiat-crypto" 191 210 version = "0.3.0" 192 211 source = "registry+https://github.com/rust-lang/crates.io-index" 193 212 checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" 194 213 195 214 [[package]] 196 - name = "fnv" 197 - version = "1.0.7" 198 - source = "registry+https://github.com/rust-lang/crates.io-index" 199 - checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 200 - 201 - [[package]] 202 215 name = "form_urlencoded" 203 216 version = "1.2.2" 204 217 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 253 266 254 267 [[package]] 255 268 name = "http" 256 - version = "1.3.1" 269 + version = "1.4.0" 257 270 source = "registry+https://github.com/rust-lang/crates.io-index" 258 - checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 271 + checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" 259 272 dependencies = [ 260 273 "bytes", 261 - "fnv", 262 274 "itoa", 263 275 ] 264 276 ··· 308 320 309 321 [[package]] 310 322 name = "hyper" 311 - version = "1.7.0" 323 + version = "1.8.1" 312 324 source = "registry+https://github.com/rust-lang/crates.io-index" 313 - checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" 325 + checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" 314 326 dependencies = [ 315 327 "atomic-waker", 316 328 "bytes", ··· 329 341 330 342 [[package]] 331 343 name = "hyper-util" 332 - version = "0.1.17" 344 + version = "0.1.19" 333 345 source = "registry+https://github.com/rust-lang/crates.io-index" 334 - checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" 346 + checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" 335 347 dependencies = [ 336 348 "bytes", 337 349 "futures-core", ··· 345 357 346 358 [[package]] 347 359 name = "itoa" 348 - version = "1.0.15" 360 + version = "1.0.17" 349 361 source = "registry+https://github.com/rust-lang/crates.io-index" 350 - checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 362 + checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" 363 + 364 + [[package]] 365 + name = "lazy_static" 366 + version = "1.5.0" 367 + source = "registry+https://github.com/rust-lang/crates.io-index" 368 + checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 351 369 352 370 [[package]] 353 371 name = "libc" 354 - version = "0.2.177" 372 + version = "0.2.178" 355 373 source = "registry+https://github.com/rust-lang/crates.io-index" 356 - checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" 374 + checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" 357 375 358 376 [[package]] 359 377 name = "lock_api" ··· 366 384 367 385 [[package]] 368 386 name = "log" 369 - version = "0.4.28" 387 + version = "0.4.29" 370 388 source = "registry+https://github.com/rust-lang/crates.io-index" 371 - checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 389 + checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" 390 + 391 + [[package]] 392 + name = "matchers" 393 + version = "0.2.0" 394 + source = "registry+https://github.com/rust-lang/crates.io-index" 395 + checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" 396 + dependencies = [ 397 + "regex-automata", 398 + ] 372 399 373 400 [[package]] 374 401 name = "matchit" ··· 390 417 391 418 [[package]] 392 419 name = "mio" 393 - version = "1.1.0" 420 + version = "1.1.1" 394 421 source = "registry+https://github.com/rust-lang/crates.io-index" 395 - checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" 422 + checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" 396 423 dependencies = [ 397 424 "libc", 398 425 "wasi", ··· 406 433 checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" 407 434 dependencies = [ 408 435 "rand", 436 + ] 437 + 438 + [[package]] 439 + name = "nu-ansi-term" 440 + version = "0.50.3" 441 + source = "registry+https://github.com/rust-lang/crates.io-index" 442 + checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" 443 + dependencies = [ 444 + "windows-sys 0.61.2", 409 445 ] 410 446 411 447 [[package]] ··· 476 512 477 513 [[package]] 478 514 name = "proc-macro2" 479 - version = "1.0.103" 515 + version = "1.0.104" 480 516 source = "registry+https://github.com/rust-lang/crates.io-index" 481 - checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" 517 + checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" 482 518 dependencies = [ 483 519 "unicode-ident", 484 520 ] ··· 532 568 ] 533 569 534 570 [[package]] 571 + name = "regex-automata" 572 + version = "0.4.13" 573 + source = "registry+https://github.com/rust-lang/crates.io-index" 574 + checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" 575 + dependencies = [ 576 + "aho-corasick", 577 + "memchr", 578 + "regex-syntax", 579 + ] 580 + 581 + [[package]] 582 + name = "regex-syntax" 583 + version = "0.8.8" 584 + source = "registry+https://github.com/rust-lang/crates.io-index" 585 + checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" 586 + 587 + [[package]] 535 588 name = "rust-server" 536 589 version = "0.1.0" 537 590 dependencies = [ ··· 543 596 "serde_json", 544 597 "tokio", 545 598 "tower-http", 599 + "tracing", 600 + "tracing-subscriber", 546 601 ] 547 602 548 603 [[package]] ··· 556 611 557 612 [[package]] 558 613 name = "ryu" 559 - version = "1.0.20" 614 + version = "1.0.22" 560 615 source = "registry+https://github.com/rust-lang/crates.io-index" 561 - checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 616 + checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" 562 617 563 618 [[package]] 564 619 name = "scopeguard" ··· 604 659 605 660 [[package]] 606 661 name = "serde_json" 607 - version = "1.0.145" 662 + version = "1.0.148" 608 663 source = "registry+https://github.com/rust-lang/crates.io-index" 609 - checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" 664 + checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" 610 665 dependencies = [ 611 666 "itoa", 612 667 "memchr", 613 - "ryu", 614 668 "serde", 615 669 "serde_core", 670 + "zmij", 616 671 ] 617 672 618 673 [[package]] ··· 650 705 ] 651 706 652 707 [[package]] 708 + name = "sharded-slab" 709 + version = "0.1.7" 710 + source = "registry+https://github.com/rust-lang/crates.io-index" 711 + checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 712 + dependencies = [ 713 + "lazy_static", 714 + ] 715 + 716 + [[package]] 653 717 name = "signal-hook-registry" 654 - version = "1.4.6" 718 + version = "1.4.8" 655 719 source = "registry+https://github.com/rust-lang/crates.io-index" 656 - checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" 720 + checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" 657 721 dependencies = [ 722 + "errno", 658 723 "libc", 659 724 ] 660 725 661 726 [[package]] 662 727 name = "signature" 663 - version = "3.0.0-rc.5" 728 + version = "3.0.0-rc.6" 664 729 source = "registry+https://github.com/rust-lang/crates.io-index" 665 - checksum = "2a0251c9d6468f4ba853b6352b190fb7c1e405087779917c238445eb03993826" 730 + checksum = "597a96996ccff7dfa16f052bd995b4cecc72af22c35138738dc029f0ead6608d" 666 731 667 732 [[package]] 668 733 name = "smallvec" ··· 697 762 698 763 [[package]] 699 764 name = "syn" 700 - version = "2.0.109" 765 + version = "2.0.112" 701 766 source = "registry+https://github.com/rust-lang/crates.io-index" 702 - checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f" 767 + checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4" 703 768 dependencies = [ 704 769 "proc-macro2", 705 770 "quote", ··· 713 778 checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 714 779 715 780 [[package]] 781 + name = "thread_local" 782 + version = "1.1.9" 783 + source = "registry+https://github.com/rust-lang/crates.io-index" 784 + checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" 785 + dependencies = [ 786 + "cfg-if", 787 + ] 788 + 789 + [[package]] 716 790 name = "tokio" 717 791 version = "1.48.0" 718 792 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 758 832 759 833 [[package]] 760 834 name = "tower-http" 761 - version = "0.6.6" 835 + version = "0.6.8" 762 836 source = "registry+https://github.com/rust-lang/crates.io-index" 763 - checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" 837 + checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" 764 838 dependencies = [ 765 839 "bitflags", 766 840 "bytes", 767 841 "http", 842 + "http-body", 768 843 "pin-project-lite", 769 844 "tower-layer", 770 845 "tower-service", 846 + "tracing", 771 847 ] 772 848 773 849 [[package]] ··· 784 860 785 861 [[package]] 786 862 name = "tracing" 787 - version = "0.1.41" 863 + version = "0.1.44" 788 864 source = "registry+https://github.com/rust-lang/crates.io-index" 789 - checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 865 + checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" 790 866 dependencies = [ 791 867 "log", 792 868 "pin-project-lite", 869 + "tracing-attributes", 793 870 "tracing-core", 794 871 ] 795 872 796 873 [[package]] 874 + name = "tracing-attributes" 875 + version = "0.1.31" 876 + source = "registry+https://github.com/rust-lang/crates.io-index" 877 + checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" 878 + dependencies = [ 879 + "proc-macro2", 880 + "quote", 881 + "syn", 882 + ] 883 + 884 + [[package]] 797 885 name = "tracing-core" 798 - version = "0.1.34" 886 + version = "0.1.36" 799 887 source = "registry+https://github.com/rust-lang/crates.io-index" 800 - checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 888 + checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" 801 889 dependencies = [ 802 890 "once_cell", 891 + "valuable", 892 + ] 893 + 894 + [[package]] 895 + name = "tracing-log" 896 + version = "0.2.0" 897 + source = "registry+https://github.com/rust-lang/crates.io-index" 898 + checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 899 + dependencies = [ 900 + "log", 901 + "once_cell", 902 + "tracing-core", 903 + ] 904 + 905 + [[package]] 906 + name = "tracing-subscriber" 907 + version = "0.3.22" 908 + source = "registry+https://github.com/rust-lang/crates.io-index" 909 + checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" 910 + dependencies = [ 911 + "matchers", 912 + "nu-ansi-term", 913 + "once_cell", 914 + "regex-automata", 915 + "sharded-slab", 916 + "smallvec", 917 + "thread_local", 918 + "tracing", 919 + "tracing-core", 920 + "tracing-log", 803 921 ] 804 922 805 923 [[package]] ··· 813 931 version = "1.0.22" 814 932 source = "registry+https://github.com/rust-lang/crates.io-index" 815 933 checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 934 + 935 + [[package]] 936 + name = "valuable" 937 + version = "0.1.1" 938 + source = "registry+https://github.com/rust-lang/crates.io-index" 939 + checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 816 940 817 941 [[package]] 818 942 name = "wasi" ··· 911 1035 912 1036 [[package]] 913 1037 name = "zerocopy" 914 - version = "0.8.27" 1038 + version = "0.8.31" 915 1039 source = "registry+https://github.com/rust-lang/crates.io-index" 916 - checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" 1040 + checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" 917 1041 dependencies = [ 918 1042 "zerocopy-derive", 919 1043 ] 920 1044 921 1045 [[package]] 922 1046 name = "zerocopy-derive" 923 - version = "0.8.27" 1047 + version = "0.8.31" 924 1048 source = "registry+https://github.com/rust-lang/crates.io-index" 925 - checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" 1049 + checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" 926 1050 dependencies = [ 927 1051 "proc-macro2", 928 1052 "quote", ··· 934 1058 version = "1.8.2" 935 1059 source = "registry+https://github.com/rust-lang/crates.io-index" 936 1060 checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" 1061 + 1062 + [[package]] 1063 + name = "zmij" 1064 + version = "1.0.7" 1065 + source = "registry+https://github.com/rust-lang/crates.io-index" 1066 + checksum = "de9211a9f64b825911bdf0240f58b7a8dac217fe260fc61f080a07f61372fbd5"
+3 -1
server/Cargo.toml
··· 11 11 serde = { version = "1.0.228", features = ["derive"] } 12 12 serde_json = "1.0.145" 13 13 tokio = { version = "1.48.0", features = ["full"] } 14 - tower-http = {version="0.6.6", features=["cors"]} 14 + tower-http = {version="0.6.6", features=["cors", "trace"]} 15 + tracing = "0.1.44" 16 + tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
+99
server/src/auth.rs
··· 1 + use axum::{body::Body, extract::State, http::Request, middleware::Next}; 2 + use base64::{Engine, prelude::BASE64_STANDARD}; 3 + use ed25519_dalek::Signature; 4 + 5 + use crate::{ 6 + ReqBail, SrvErr, 7 + types::{AppState, AuthData}, 8 + }; 9 + 10 + pub async fn auth_test( 11 + State(state): State<AppState>, 12 + req: Request<Body>, 13 + next: Next, 14 + ) -> Result<axum::response::Response, SrvErr> { 15 + let endpoint = req.uri().path().to_owned(); 16 + if endpoint != "/create-account" { 17 + // CURSED STUFF BEGIN 18 + let (parts, body) = req.into_parts(); 19 + let body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap(); 20 + let new_body = Body::from(body_bytes.clone()); 21 + let mut req = Request::from_parts(parts, new_body); 22 + // CURSED STUFF END 23 + 24 + let auth_header = req 25 + .headers() 26 + .get("x-auth") 27 + .and_then(|v| v.to_str().ok()) 28 + .ok_or(SrvErr!("missing x-auth header"))?; 29 + 30 + let decoded_auth = BASE64_STANDARD 31 + .decode(auth_header) 32 + .map_err(|e| SrvErr!("invalid base64 in x-auth header", e))?; 33 + 34 + let auth_str = String::from_utf8(decoded_auth) 35 + .map_err(|e| SrvErr!("invalid utf8 in x-auth header", e))?; 36 + 37 + let auth_data: AuthData = serde_json::from_str(&auth_str) 38 + .map_err(|e| SrvErr!("failed to parse x-auth JSON", e))?; 39 + 40 + let users = state.users.lock().await; 41 + let user_id = auth_data.user_id; 42 + let user = users 43 + .iter() 44 + .find(|u| u.id == user_id) 45 + .ok_or(SrvErr!("User not found"))?; 46 + let verifying_key = user.pub_key.clone(); 47 + 48 + // NOTE (key chaining): 49 + // Do NOT drop the `users` lock until after both steps are complete: 50 + // 1) verify the request using the current stored key 51 + // 2) update the stored key to the next key from this request 52 + // 53 + // If we unlock in between, a replay/duplicate of the same request can race: 54 + // 55 + // - Request A reads pk_i and starts verifying 56 + // - Attacker replays A (same signature) while A is still verifying 57 + // - Replay gets verified against pk_i and proceeds to update pk -> pk_attacker 58 + // - Request A finishes and updates pk again, but the replay has already 59 + // been accepted and may have advanced the key to an attacker-chosen value 60 + // 61 + // Keeping the lock across "verify + update" makes the transition atomic. 62 + drop(users); 63 + 64 + //////////////////////////////////// 65 + //////////////////////////////////// unsure 66 + //////////////////////////////////// 67 + 68 + let sig_vec = BASE64_STANDARD 69 + .decode(&auth_data.signature) 70 + .map_err(|e| SrvErr!("base64 decode fail", e))?; 71 + let sig_bytes: [u8; 64] = sig_vec 72 + .try_into() 73 + .map_err(|e| SrvErr!("invalid signature length", e))?; 74 + let signature = Signature::from_bytes(&sig_bytes); 75 + 76 + if let Err(err) = verifying_key.verify_strict(&body_bytes, &signature) { 77 + panic!("Signature verification failed: {err}"); 78 + } 79 + 80 + //////////////////////////////////// 81 + //////////////////////////////////// 82 + //////////////////////////////////// 83 + 84 + // TODO: Make the endpoints as enums at some point 85 + if endpoint == "/generate-signup-key" { 86 + let admin_id = state.admin_id.lock().await; 87 + if admin_id.as_ref() != Some(&user_id) { 88 + ReqBail!("not allowed: admin only"); 89 + } 90 + } 91 + 92 + req.extensions_mut().insert(user_id); // pass user_id to the actual request handler, whatever it is, in handlers.rs 93 + *req.body_mut() = Body::from(body_bytes); 94 + 95 + return Ok(next.run(req).await); 96 + } 97 + 98 + return Ok(next.run(req).await); 99 + }
+17 -23
server/src/handlers.rs
··· 3 3 use ed25519_dalek::VerifyingKey; 4 4 use nanoid::nanoid; 5 5 6 - use crate::types::*; 7 - 8 - macro_rules! my_err { 9 - ($msg:expr) => { 10 - Err(MyErr($msg)) 11 - }; 12 - } 6 + use crate::{ReqBail, SrvErr, types::*}; 13 7 14 8 pub async fn create_user( 15 9 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 16 10 Json(payload): Json<CreateUserRequest>, 17 - ) -> Result<Json<CreateAccountResponse>, MyErr> { 11 + ) -> Result<Json<CreateAccountResponse>, SrvErr> { 18 12 let key_used = { state.signup_keys.lock().await.remove(&payload.signup_key) }; 19 13 20 14 if !key_used { 21 - return my_err!("Signup key was not there"); 15 + ReqBail!("Signup key was not there"); 22 16 } 23 17 24 18 // todo check 25 19 let pub_key_bytes = match BASE64_STANDARD.decode(&payload.pub_key_b64) { 26 20 Ok(b) => b, 27 - Err(_) => return my_err!("Invalid base64 public key"), 21 + Err(_) => ReqBail!("Invalid base64 public key"), 28 22 }; 29 23 30 24 // todo check 31 25 let pub_key = match VerifyingKey::from_bytes( 32 26 &pub_key_bytes 33 27 .try_into() 34 - .map_err(|_| MyErr("Invalid pubkey length".into()))?, 28 + .map_err(|_| SrvErr("Invalid pubkey length".into()))?, 35 29 ) { 36 30 Ok(pk) => pk, 37 - Err(_) => return my_err!("Invalid public key bytes"), 31 + Err(_) => ReqBail!("Invalid public key bytes"), 38 32 }; 39 33 40 34 let user_id = nanoid!(5); ··· 54 48 return Ok(Json(CreateAccountResponse { user_id, is_admin })); 55 49 } 56 50 57 - pub async fn generate_signup_key(State(state): State<AppState>) -> Result<String, MyErr> { 51 + pub async fn generate_signup_key(State(state): State<AppState>) -> Result<String, SrvErr> { 58 52 let new_signup_key = nanoid!(5); 59 53 let mut signup_keys = state.signup_keys.lock().await; 60 54 ··· 68 62 State(state): State<AppState>, 69 63 Extension(user_id): Extension<String>, 70 64 accepter_id: String, 71 - ) -> Result<(), MyErr> { 65 + ) -> Result<(), SrvErr> { 72 66 if accepter_id == user_id { 73 - return my_err!("Cannot friend yourself"); 67 + ReqBail!("Cannot friend yourself"); 74 68 } 75 69 76 70 let mut friend_requests = state.friend_requests.lock().await; 77 71 let link = Link::new(accepter_id, user_id); 78 72 if friend_requests.contains(&link) { 79 - return my_err!("Friend request already exists"); 73 + ReqBail!("Friend request already exists"); 80 74 } 81 75 friend_requests.insert(link); 82 76 return Ok(()); ··· 86 80 State(state): State<AppState>, 87 81 Extension(user_id): Extension<String>, 88 82 sender_id: String, 89 - ) -> Result<(), MyErr> { 83 + ) -> Result<(), SrvErr> { 90 84 let link = Link::new(user_id, sender_id); 91 85 92 86 let friend_request_accepted = { state.friend_requests.lock().await.remove(&link) }; 93 87 94 88 if !friend_request_accepted { 95 - return my_err!("Friend request not found"); 89 + ReqBail!("Friend request not found"); 96 90 } 97 91 98 92 let mut pings_state = state.positions.lock().await; ··· 109 103 State(state): State<AppState>, 110 104 Extension(user_id): Extension<String>, 111 105 friend_id: String, 112 - ) -> Result<PlainBool, MyErr> { 106 + ) -> Result<PlainBool, SrvErr> { 113 107 let link = Link::new(friend_id, user_id); 114 108 let links = state.links.lock().await; 115 109 let accepted = links.contains(&link); ··· 120 114 State(state): State<AppState>, 121 115 Extension(user_id): Extension<String>, 122 116 Json(pings): Json<Vec<PingPayload>>, 123 - ) -> Result<(), MyErr> { 117 + ) -> Result<(), SrvErr> { 124 118 let links = state.links.lock().await; 125 119 for ping in &pings { 126 120 let link = Link::new(user_id.clone(), ping.receiver_id.clone()); 127 121 if !links.contains(&link) { 128 - return my_err!("Ping receiver is not linked to sender"); 122 + ReqBail!("Ping receiver is not linked to sender"); 129 123 } 130 124 } 131 125 drop(links); ··· 147 141 State(state): State<AppState>, 148 142 Extension(user_id): Extension<String>, 149 143 sender_id: String, 150 - ) -> Result<EncryptedPingVec, MyErr> { 144 + ) -> Result<EncryptedPingVec, SrvErr> { 151 145 let link = Link::new(user_id, sender_id); 152 146 let links = state.links.lock().await; 153 147 154 148 if !links.contains(&link) { 155 - return my_err!("No link exists between these users"); 149 + ReqBail!("No link exists between these users"); 156 150 } 157 151 drop(links); 158 152
+208
server/src/log.rs
··· 1 + use std::{ 2 + sync::atomic::{AtomicU64, Ordering}, 3 + time::Instant, 4 + }; 5 + 6 + use axum::{ 7 + body::{Body, Bytes, to_bytes}, 8 + http::{HeaderMap, Method, Request}, 9 + middleware::Next, 10 + response::Response, 11 + }; 12 + use base64::{Engine, prelude::BASE64_STANDARD}; 13 + use serde_json::Value; 14 + 15 + static REQ_SEQ: AtomicU64 = AtomicU64::new(1); 16 + 17 + fn format_x_auth(headers: &HeaderMap) -> Option<String> { 18 + let v = headers.get("x-auth")?; 19 + let s = v.to_str().ok()?.to_string(); 20 + 21 + const MAX: usize = 120; 22 + if s.len() > MAX { 23 + Some(format!("{}… (len={})", &s[..MAX], s.len())) 24 + } else { 25 + Some(s) 26 + } 27 + } 28 + 29 + fn status_emoji(status: axum::http::StatusCode) -> &'static str { 30 + if status.is_success() { 31 + "✅" 32 + } else if status.is_redirection() { 33 + "↪" 34 + } else if status.is_client_error() { 35 + "⚠" 36 + } else if status.is_server_error() { 37 + "❌" 38 + } else { 39 + "ℹ" 40 + } 41 + } 42 + 43 + /// Convert JSON into a "key: value" style display. 44 + /// - Objects: `key: value` (nested objects/arrays are indented) 45 + /// - Arrays: `- item` (nested indented) 46 + /// If not JSON: UTF-8 text, else base64. 47 + fn body_as_kv(bytes: &Bytes) -> String { 48 + if bytes.is_empty() { 49 + return "<empty>".to_string(); 50 + } 51 + 52 + if let Ok(v) = serde_json::from_slice::<Value>(bytes) { 53 + let mut out = String::new(); 54 + write_value(&mut out, &v, 0); 55 + return out.trim_end().to_string(); 56 + } 57 + 58 + match std::str::from_utf8(bytes) { 59 + Ok(s) => s.to_string(), 60 + Err(_) => format!("<non-utf8; base64>\n{}", BASE64_STANDARD.encode(bytes)), 61 + } 62 + } 63 + 64 + fn write_value(out: &mut String, v: &Value, indent: usize) { 65 + match v { 66 + Value::Object(map) => { 67 + for (k, val) in map { 68 + write_key_value(out, k, val, indent); 69 + } 70 + } 71 + Value::Array(arr) => { 72 + for item in arr { 73 + write_array_item(out, item, indent); 74 + } 75 + } 76 + _ => { 77 + // Root primitive 78 + out.push_str(&indent_str(indent)); 79 + out.push_str(&format_primitive(v)); 80 + out.push('\n'); 81 + } 82 + } 83 + } 84 + 85 + fn write_key_value(out: &mut String, key: &str, val: &Value, indent: usize) { 86 + let pad = indent_str(indent); 87 + 88 + match val { 89 + Value::Object(_) | Value::Array(_) => { 90 + out.push_str(&pad); 91 + out.push_str(key); 92 + out.push_str(":\n"); 93 + write_value(out, val, indent + 2); 94 + } 95 + _ => { 96 + out.push_str(&pad); 97 + out.push_str(key); 98 + out.push_str(": "); 99 + out.push_str(&format_primitive(val)); 100 + out.push('\n'); 101 + } 102 + } 103 + } 104 + 105 + fn write_array_item(out: &mut String, item: &Value, indent: usize) { 106 + let pad = indent_str(indent); 107 + 108 + match item { 109 + Value::Object(_) | Value::Array(_) => { 110 + out.push_str(&pad); 111 + out.push_str("-\n"); 112 + write_value(out, item, indent + 2); 113 + } 114 + _ => { 115 + out.push_str(&pad); 116 + out.push_str("- "); 117 + out.push_str(&format_primitive(item)); 118 + out.push('\n'); 119 + } 120 + } 121 + } 122 + 123 + fn format_primitive(v: &Value) -> String { 124 + match v { 125 + Value::String(s) => s.clone(), 126 + Value::Number(n) => n.to_string(), 127 + Value::Bool(b) => b.to_string(), 128 + Value::Null => "null".to_string(), 129 + // Shouldn’t happen here (we route these elsewhere), but safe fallback 130 + Value::Object(_) | Value::Array(_) => "<complex>".to_string(), 131 + } 132 + } 133 + 134 + fn indent_str(spaces: usize) -> String { 135 + " ".repeat(spaces) 136 + } 137 + 138 + fn indent_block(s: &str, spaces: usize) -> String { 139 + let pad = " ".repeat(spaces); 140 + s.lines() 141 + .map(|line| format!("{pad}{line}\n")) 142 + .collect::<String>() 143 + .trim_end_matches('\n') 144 + .to_string() 145 + } 146 + 147 + pub async fn log_req_res_bodies(req: Request<Body>, next: Next) -> Response { 148 + // Avoid noisy CORS preflight logs 149 + if req.method() == Method::OPTIONS { 150 + return next.run(req).await; 151 + } 152 + 153 + let id = REQ_SEQ.fetch_add(1, Ordering::Relaxed); 154 + let start = Instant::now(); 155 + 156 + let method = req.method().clone(); 157 + let uri = req.uri().clone(); 158 + let req_headers = req.headers().clone(); 159 + 160 + // Read + restore request body 161 + let (req_parts, req_body) = req.into_parts(); 162 + let req_bytes = to_bytes(req_body, usize::MAX).await.unwrap(); 163 + let req = Request::from_parts(req_parts, Body::from(req_bytes.clone())); 164 + 165 + // Run handler 166 + let res = next.run(req).await; 167 + 168 + let status = res.status(); 169 + let res_headers = res.headers().clone(); 170 + 171 + // Read + restore response body 172 + let (res_parts, res_body) = res.into_parts(); 173 + let res_bytes = to_bytes(res_body, usize::MAX).await.unwrap(); 174 + let res = Response::from_parts(res_parts, Body::from(res_bytes.clone())); 175 + 176 + let ms = start.elapsed().as_millis(); 177 + let sep = "════════════════════════════════════════════════════════════════"; 178 + 179 + let mut out = String::new(); 180 + out.push('\n'); 181 + out.push_str(sep); 182 + out.push('\n'); 183 + out.push_str(&format!( 184 + "🚦 #{id} {method} {uri} {} {status} ⏱ {ms}ms\n", 185 + status_emoji(status) 186 + )); 187 + 188 + out.push('\n'); 189 + 190 + out.push_str("📥 Request\n"); 191 + if let Some(xauth) = format_x_auth(&req_headers) { 192 + out.push_str(&format!(" 🔐 x-auth: {xauth}\n")); 193 + } 194 + out.push_str(&indent_block(&body_as_kv(&req_bytes), 2)); 195 + out.push('\n'); 196 + 197 + out.push_str("📤 Response\n"); 198 + out.push_str(&indent_block(&body_as_kv(&res_bytes), 2)); 199 + out.push('\n'); 200 + 201 + out.push_str(sep); 202 + out.push('\n'); 203 + 204 + tracing::info!("{out}"); 205 + 206 + let _ = res_headers; 207 + res 208 + }
+19 -85
server/src/main.rs
··· 1 1 use std::{collections::HashMap, sync::Arc}; 2 2 3 - use axum::{ 4 - Router, 5 - body::{Body, to_bytes}, 6 - extract::{Request, State}, 7 - middleware::Next, 8 - response::IntoResponse, 9 - routing::post, 10 - }; 11 - use base64::{Engine, prelude::BASE64_STANDARD}; 12 - use ed25519_dalek::Signature; 3 + use axum::{Router, routing::post}; 13 4 use nanoid::nanoid; 14 - use serde::Deserialize; 15 5 use std::collections::HashSet; 16 6 use tokio::sync::Mutex; 17 - use tower_http::cors::{Any, CorsLayer}; 7 + use tower_http::cors::CorsLayer; 8 + use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; 18 9 10 + mod auth; 19 11 mod handlers; 12 + mod log; 20 13 mod types; 21 14 22 15 use handlers::*; 23 16 use types::*; 17 + 18 + use crate::auth::auth_test; 19 + use crate::log::log_req_res_bodies; 24 20 25 21 #[tokio::main] 26 22 async fn main() { 23 + tracing_subscriber::registry() 24 + .with( 25 + tracing_subscriber::EnvFilter::try_from_default_env() 26 + .unwrap_or_else(|_| "info,tower_http=info,axum=info".into()), 27 + ) 28 + .with(tracing_subscriber::fmt::layer()) 29 + .init(); 30 + 27 31 // TODO: should this be inside an Arc? 28 32 let state = AppState { 29 33 users: Arc::new(Mutex::new(Vec::new())), ··· 56 60 .route("/get-pings", post(get_pings)) 57 61 .with_state(state.clone()) 58 62 .layer(CorsLayer::permissive()) 59 - .layer(axum::middleware::from_fn_with_state(state, auth_test)); 63 + .layer(axum::middleware::from_fn_with_state(state, auth_test)) 64 + .layer(axum::middleware::from_fn(log_req_res_bodies)); 60 65 61 66 let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); 62 67 axum::serve(listener, app).await.unwrap(); 63 68 } 64 69 65 - async fn auth_test(State(state): State<AppState>, req: Request, next: Next) -> impl IntoResponse { 66 - let endpoint = req.uri().path().to_owned(); 67 - if endpoint != "/create-account" { 68 - // CURSED STUFF BEGIN 69 - let (parts, body) = req.into_parts(); 70 - let body_bytes = to_bytes(body, usize::MAX).await.unwrap(); 71 - let new_body = Body::from(body_bytes.clone()); 72 - let mut req = Request::from_parts(parts, new_body); 73 - // CURSED STUFF END 74 - 75 - let auth_header = req 76 - .headers() 77 - .get("x-auth") 78 - .and_then(|v| v.to_str().ok()) 79 - .unwrap_or_else(|| panic!("missing x-auth header")); 80 - 81 - let decoded_auth = BASE64_STANDARD 82 - .decode(auth_header) 83 - .unwrap_or_else(|_| panic!("invalid base64 in x-auth header")); 84 - 85 - let auth_str = String::from_utf8(decoded_auth) 86 - .unwrap_or_else(|_| panic!("invalid utf8 in x-auth header")); 87 - 88 - let auth_data: Auth = serde_json::from_str(&auth_str) 89 - .unwrap_or_else(|e| panic!("failed to parse x-auth JSON: {e}")); 90 - 91 - let users = state.users.lock().await; 92 - let user_id = auth_data.user_id; 93 - let user = users 94 - .iter() 95 - .find(|u| u.id == user_id) 96 - .unwrap_or_else(|| panic!("User not found")); 97 - let verifying_key = user.pub_key.clone(); 98 - drop(users); 99 - 100 - //////////////////////////////////// 101 - //////////////////////////////////// unsure 102 - //////////////////////////////////// 103 - 104 - let sig_vec = BASE64_STANDARD.decode(&auth_data.signature).unwrap(); 105 - let sig_bytes: [u8; 64] = sig_vec.try_into().expect("invalid signature length"); 106 - let signature = Signature::from_bytes(&sig_bytes); 107 - 108 - if let Err(err) = verifying_key.verify_strict(&body_bytes, &signature) { 109 - panic!("Signature verification failed: {err}"); 110 - } 111 - 112 - //////////////////////////////////// 113 - //////////////////////////////////// 114 - //////////////////////////////////// 115 - 116 - // TODO: Maybe make the endpoints an enum 117 - if endpoint == "/generate-signup-key" { 118 - let admin_id = state.admin_id.lock().await; 119 - if admin_id.as_ref() != Some(&user_id) { 120 - todo!("not allowed") 121 - } 122 - } 123 - 124 - req.extensions_mut().insert(user_id); 125 - 126 - return next.run(req).await; 127 - } 128 - 129 - return next.run(req).await; 130 - } 131 - 132 - #[derive(Debug, Deserialize, Clone)] 133 - struct Auth { 134 - user_id: String, 135 - signature: String, 136 - } 70 + // TODO: potential security risk of returning error messages directly to the user. nice for debugging tho :p
+54 -6
server/src/types.rs
··· 22 22 pub ring_buffer_cap: usize, 23 23 } 24 24 25 + #[derive(Debug, Deserialize, Clone)] 26 + pub struct AuthData { 27 + pub user_id: String, 28 + pub signature: String, 29 + } 30 + 25 31 pub struct RingBuffer { 26 32 pub ring: Box<[Option<EncryptedPing>]>, 27 33 pub idx: usize, ··· 80 86 pub id: String, 81 87 pub pub_key: VerifyingKey, 82 88 } 83 - // pub struct User(pub String); 84 89 85 90 #[derive(Debug, Clone, PartialEq, Eq, Hash)] 86 91 pub struct Link(String, String); ··· 116 121 117 122 impl IntoResponse for EncryptedPingVec { 118 123 fn into_response(self) -> Response { 119 - axum::Json(self.0).into_response() // TODO: check if this is correct 124 + axum::Json(self.0).into_response() 120 125 } 121 126 } 122 127 123 - pub struct MyErr(pub &'static str); 128 + #[derive(Debug)] 129 + pub struct SrvErr(pub String); 124 130 125 - impl IntoResponse for MyErr { 131 + impl IntoResponse for SrvErr { 126 132 fn into_response(self) -> Response { 127 - let body = format!(r#"{{"error":"{}"}}"#, self.0); // example: {"error":"something went wrong"} 128 - return (StatusCode::INTERNAL_SERVER_ERROR, body).into_response(); 133 + (StatusCode::INTERNAL_SERVER_ERROR, self.0).into_response() 129 134 } 130 135 } 136 + 137 + /// Central policy: what gets logged, what gets returned. 138 + pub fn mk_srv_err(msg: impl Into<String>, cause: Option<String>) -> SrvErr { 139 + let msg = msg.into(); 140 + 141 + match cause { 142 + Some(c) => { 143 + // Always log server-side 144 + eprintln!("[ERR] {msg} | cause: {c}"); 145 + 146 + // Only include cause in client response in debug builds 147 + if cfg!(debug_assertions) { 148 + SrvErr(format!("{msg} | cause: {c}")) 149 + } else { 150 + SrvErr(msg) 151 + } 152 + } 153 + None => { 154 + eprintln!("[ERR] {msg}"); 155 + SrvErr(msg) 156 + } 157 + } 158 + } 159 + 160 + #[macro_export] 161 + macro_rules! SrvErr { 162 + ($msg:expr) => { 163 + $crate::mk_srv_err($msg, None) 164 + }; 165 + ($msg:expr, $err:expr) => { 166 + $crate::mk_srv_err($msg, Some(format!("{:?}", $err))) 167 + }; 168 + } 169 + 170 + #[macro_export] 171 + macro_rules! ReqBail { 172 + ($msg:expr) => {{ 173 + return Err($crate::SrvErr!($msg)); 174 + }}; 175 + ($msg:expr, $err:expr) => {{ 176 + return Err($crate::SrvErr!($msg, $err)); 177 + }}; 178 + }
-47
server/test/autils.ts
··· 1 - import { expect } from "bun:test"; 2 - 3 - export const URL = "http://127.0.0.1:3000"; 4 - 5 - export async function generateUser(signup_key: string | undefined, should_be_admin: boolean = false): Promise<string> { 6 - if (signup_key === undefined) { 7 - throw new Error("signup_key was not provided or captured from server output"); 8 - } 9 - 10 - const res = await fetch(`${URL}/create-account`, { 11 - method: "POST", 12 - body: signup_key, 13 - }); 14 - const json = await res.json(); 15 - expect(res.status).toBe(200); 16 - expect(json).toEqual({ 17 - user_id: expect.any(String), 18 - is_admin: should_be_admin, 19 - }); 20 - 21 - return json.user_id; 22 - } 23 - 24 - export async function post(endpoint: string, user_id: string, data: Object | string | undefined): Promise<any> { 25 - const headers: Record<string, string> = { 26 - "x-auth": JSON.stringify({ user_id }), 27 - }; 28 - 29 - let stringified_data: string | undefined; 30 - 31 - if (typeof data === "object") { 32 - stringified_data = JSON.stringify(data); 33 - headers["Content-Type"] = "application/json"; 34 - } else { 35 - stringified_data = data; 36 - } 37 - 38 - const res = await fetch(`${URL}/${endpoint}`, { 39 - method: "POST", 40 - headers, 41 - body: stringified_data, 42 - }); 43 - 44 - expect(res.status).toBe(200); 45 - 46 - return await res.text(); 47 - }