Self-hosted, federated location sharing app and server that prioritizes user privacy and security
end-to-end-encryption location-sharing privacy self-hosted federated
at azom 103 lines 3.4 kB view raw
1import { Store } from "./store.ts"; 2 3function bufToBase64(buf: ArrayBuffer): string { 4 return new Uint8Array(buf).toBase64(); 5} 6 7/** 8 * This function can throw an error 9 */ 10export async function createAccount( 11 server_url: string, 12 signup_key: string, 13): Promise<{ user_id: string; is_admin: boolean }> { 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); 18 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 }); 24 25 if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`); 26 const json = await response.json(); 27 28 // TODO validate data? 29 30 await Store.set("server_url", server_url); 31 await Store.set("user_id", json.user_id); 32 await Store.set("is_admin", json.is_admin); 33 await Store.set("priv_key", bufToBase64(privKeyRaw)); 34 await Store.set("friends", []); 35 36 return json; 37} 38 39// this api is laughably vulnerable to a replay attack currently, but not later with key chaining 40export async function generateSignupKey(): Promise<string> { 41 return await post("generate-signup-key"); 42} 43 44export async function requestFriendRequest(friend_id: string): Promise<void> { 45 await post("request-friend-request", friend_id); 46} 47 48export async function isFriendRequestAccepted(friend_id: string): Promise<boolean> { 49 const res = await post("is-friend-request-accepted", friend_id); 50 return res === "true"; 51} 52 53export async function sendPings(friend_id: string, ping: string): Promise<void> { 54 // later, accept a list of friend ids, but anyways this specific api won't stay in typescript for long since it needs to be run in the background 55 await post("send-pings", JSON.stringify([{ receiver_id: friend_id, encrypted_ping: ping }])); 56} 57 58export async function getPings(friend_id: string): Promise<string[]> { 59 const res = await post("get-pings", friend_id); 60 return JSON.parse(res); 61} 62 63/** 64 * This function can throw an error 65 */ 66async function post(endpoint: string, body: string | undefined = undefined) { 67 const user_id = await Store.get("user_id"); 68 const server_url = await Store.get("server_url"); 69 const privKey_b64 = await Store.get("priv_key"); 70 71 const bodyBytes = new TextEncoder().encode(body === undefined ? "" : body); 72 73 const privKeyBytes = Uint8Array.fromBase64(privKey_b64); 74 75 const privKey = await crypto.subtle.importKey("pkcs8", privKeyBytes, "Ed25519", false, [ 76 "sign", 77 ]); 78 79 const nonce = new Uint8Array(8); 80 crypto.getRandomValues(nonce); 81 const data_to_sign = new Uint8Array(bodyBytes.length + nonce.length); 82 data_to_sign.set(bodyBytes); 83 data_to_sign.set(nonce, bodyBytes.length); 84 const signature = await crypto.subtle.sign("Ed25519", privKey, data_to_sign); 85 86 const headers = { 87 "x-auth": JSON.stringify({ 88 user_id, 89 signature: bufToBase64(signature), 90 nonce: nonce.toBase64(), 91 }), 92 "Content-Type": "application/json", // TODO: not always json tho, but does it matter? 93 }; 94 95 const res = await fetch(`${server_url}/${endpoint}`, { 96 method: "POST", 97 headers, 98 body: bodyBytes, 99 }); 100 101 if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`); 102 return await res.text(); 103}