Self-hosted, federated location sharing app and server that prioritizes user privacy and security
end-to-end-encryption
location-sharing
privacy
self-hosted
federated
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}