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 auth 3.2 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(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; 33} 34 35// this api is laughably vulnerable to a replay attack currently, but not later with key chaining 36export async function generateSignupKey(): Promise<string> { 37 return await post("generate-signup-key"); 38} 39 40export async function requestFriendRequest(friend_id: string): Promise<void> { 41 await post("request-friend-request", friend_id); 42} 43 44export async function isFriendRequestAccepted(friend_id: string): Promise<boolean> { 45 const res = await post("is-friend-request-accepted", friend_id); 46 return res === "true"; 47} 48 49export async function sendPings(friend_id: string, ping: string): Promise<void> { 50 // 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 51 await post("send-pings", JSON.stringify([{ receiver_id: friend_id, encrypted_ping: ping }])); 52} 53 54export async function getPings(friend_id: string): Promise<string[]> { 55 const res = await post("get-pings", friend_id); 56 return JSON.parse(res); 57} 58 59/** 60 * This function can throw an error 61 */ 62async function post(endpoint: string, body: string | undefined = undefined) { 63 const user_id = await Store.get("user_id"); 64 const server_url = await Store.get("server_url"); 65 const privKey_b64 = await Store.get("priv_key"); 66 67 const bodyBytes = new TextEncoder().encode(body === undefined ? "" : body); 68 69 const privKeyBytes = Uint8Array.fromBase64(privKey_b64); 70 71 const privKey = await crypto.subtle.importKey("pkcs8", privKeyBytes.buffer, "Ed25519", false, ["sign"]); 72 73 const signature = await crypto.subtle.sign("Ed25519", privKey, bodyBytes); 74 const signature_b64 = bufToBase64(signature); 75 76 const headers = { 77 "x-auth": JSON.stringify({ user_id, signature: signature_b64 }), 78 "Content-Type": "application/json", // TODO: not always json tho, but does it matter? 79 }; 80 81 const res = await fetch(`${server_url}/${endpoint}`, { 82 method: "POST", 83 headers, 84 body: bodyBytes, // TODO: do we need to send bodyBytes instead to match server side auth? 85 }); 86 87 if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`); 88 return await res.text(); 89}