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(
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}