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 consistent-ui 105 lines 3.7 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 await Store.set("is_sharing", true); // i'm adding this so it works in settings, i think it's more user friendly to have the location to be on by default, but... less privacy friendly? wait no.. nvm. it won't share location with the server since they don't have any added friends yet :) 37 38 return json; 39} 40 41// this api is laughably vulnerable to a replay attack currently, but not later with key chaining 42export async function generateSignupKey(): Promise<string> { 43 return await post("generate-signup-key"); 44} 45 46export async function requestFriendRequest(friend_id: string): Promise<void> { 47 await post("request-friend-request", friend_id); 48} 49 50export async function isFriendRequestAccepted(friend_id: string): Promise<boolean> { 51 const res = await post("is-friend-request-accepted", friend_id); 52 return res === "true"; 53} 54 55export async function sendPings(friend_id: string, ping: string): Promise<void> { 56 // 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 57 await post("send-pings", JSON.stringify([{ receiver_id: friend_id, encrypted_ping: ping }])); 58} 59 60export async function getPings(friend_id: string): Promise<string[]> { 61 const res = await post("get-pings", friend_id); 62 return JSON.parse(res); 63} 64 65/** 66 * This function can throw an error 67 */ 68async function post(endpoint: string, body: string | undefined = undefined) { 69 const user_id = await Store.get("user_id"); 70 const server_url = await Store.get("server_url"); 71 const privKey_b64 = await Store.get("priv_key"); 72 73 const bodyBytes = new TextEncoder().encode(body === undefined ? "" : body); 74 75 const privKeyBytes = Uint8Array.fromBase64(privKey_b64); 76 77 const privKey = await crypto.subtle.importKey("pkcs8", privKeyBytes, "Ed25519", false, [ 78 "sign", 79 ]); 80 81 const nonce = new Uint8Array(8); 82 crypto.getRandomValues(nonce); 83 const data_to_sign = new Uint8Array(bodyBytes.length + nonce.length); 84 data_to_sign.set(bodyBytes); 85 data_to_sign.set(nonce, bodyBytes.length); 86 const signature = await crypto.subtle.sign("Ed25519", privKey, data_to_sign); 87 88 const headers = { 89 "x-auth": JSON.stringify({ 90 user_id, 91 signature: bufToBase64(signature), 92 nonce: nonce.toBase64(), 93 }), 94 "Content-Type": "application/json", // TODO: not always json tho, but does it matter? 95 }; 96 97 const res = await fetch(`${server_url}/${endpoint}`, { 98 method: "POST", 99 headers, 100 body: bodyBytes, 101 }); 102 103 if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`); 104 return await res.text(); 105}