carlos / contra Public

Clone
git clone https://amadan.net/carlos/contra

Plain git — no account needed to clone.

Download

Download this file

1// The sharing panel on the vault page.
2//
3// Split out of vault.js because that file has a line budget and this is a
4// second concern. Everything that decides anything lives in
5// crypto/sharing.js; this moves text between a form and it.
6//
7// # The copy rule this file exists to hold
8//
9// PROTOCOL.md's revocation answer has three parts and the screen must not
10// soften any of them: access dies immediately, the key rotates forward, and
11// **nothing already downloaded is un-downloaded**. The third is the one every
12// product in this category quietly omits. It is said here, before the button,
13// in those words.
14
15import { randomBytes, toHex } from "../crypto/bytes.js";
16import { net } from "../crypto/net.js";
17import * as sharing from "../crypto/sharing.js";
18
19const $ = (sel, root = document) => root.querySelector(sel);
20
21function el(tag, attrs = {}, ...kids) {
22 const node = document.createElement(tag);
23 for (const [k, v] of Object.entries(attrs)) node.setAttribute(k, v);
24 node.append(...kids.filter((k) => k !== null && k !== undefined));
25 return node;
26}
27
28function say(box, text, kind = "working") {
29 box.replaceChildren(el("p", { "data-kind": kind }, text));
30}
31
32// wireSharing takes the opened vault and a way to re-read the item list,
33// because rotation has to re-seal every item and cannot do that from a
34// summary.
35export function wireSharing(root, opened, vault, store) {
36 const panel = $("[data-sharing]", root);
37 if (!panel) return;
38
39 // Your own account id, so you can give it to somebody who wants to share
40 // with you. It is not a secret — it is a random 128-bit label — but it is
41 // the only handle there is, because there is no directory to look you up
42 // in and deliberately so.
43 $("[data-my-account]", panel).textContent = toHex(opened.accountId);
44
45 if (!vault.owned) {
46 // A member sees that it is shared with them and nothing else. Who else is
47 // in it is the owner's business.
48 panel.setAttribute("data-role", "member");
49 say($("[data-share-status]", panel),
50 "This vault is shared with you by somebody else. Only its owner can " +
51 "change who can see it.", "ok");
52 $("[data-share-form]", panel).hidden = true;
53 return;
54 }
55
56 const status = $("[data-share-status]", panel);
57 const list = $("[data-members]", panel);
58 let current = vault;
59
60 async function draw() {
61 const rows = await sharing.members(net, current);
62 if (rows.length === 0) {
63 list.replaceChildren(el("li", {}, "Not shared with anybody."));
64 return;
65 }
66 list.replaceChildren(...rows.map((m) => el("li", { "data-member": m.account_id_hex },
67 el("code", {}, m.account_id_hex),
68 el("button", { type: "button", "data-remove": m.account_id_hex }, "Remove"))));
69 }
70
71 $("[data-share-form]", panel).addEventListener("submit", async (e) => {
72 e.preventDefault();
73 const idHex = $("[name=account_id]", panel).value.trim().toLowerCase();
74 try {
75 say(status, "Looking them up…");
76 const who = await sharing.lookupAccount(net, idHex);
77
78 say(status, "Sealing the vault key to them…");
79 await sharing.share(net, current, who.account_id_hex, who.public_key_hex);
80
81 $("[name=account_id]", panel).value = "";
82 await draw();
83 say(status, "Shared. They will see it the next time they unlock.", "ok");
84 } catch (err) {
85 console.error("CONTRA could not share the vault", err);
86 say(status, err.status === 404
87 ? "No account with that id, or they have not finished signing up."
88 : "Could not share this vault. The browser console has the detail.",
89 "error");
90 }
91 });
92
93 list.addEventListener("click", async (e) => {
94 const idHex = e.target.getAttribute?.("data-remove");
95 if (!idHex) return;
96 if (!$("[data-understood]", panel).checked) {
97 say(status, "Tick the box first: removing somebody cannot take back " +
98 "what they have already downloaded.", "error");
99 return;
100 }
101 try {
102 // Revocation IS rotation. The whole vault is re-sealed under a new key
103 // and every remaining member is re-wrapped, in one transaction — so
104 // there is no moment where the removed member is still holding a key
105 // that opens anything current.
106 say(status, "Re-keying the vault…");
107 const rows = await sharing.members(net, current);
108 const keep = [];
109 for (const m of rows) {
110 if (m.account_id_hex === idHex) continue;
111 const who = await sharing.lookupAccount(net, m.account_id_hex);
112 keep.push({
113 account_id_hex: who.account_id_hex,
114 public_key_hex: who.public_key_hex,
115 });
116 }
117
118 say(status, "Re-sealing every item…");
119 const items = await store.list();
120 await sharing.rotate(net, opened, current, items, keep, randomBytes(32));
121
122 // Every key this page is holding is now the old one, so the page is
123 // reloaded rather than patched. Rotation is rare and a stale K_vault in
124 // a live tab is the kind of state that produces items nobody can open.
125 say(status, "Removed, and the vault has a new key. What they already " +
126 "downloaded is still theirs — nothing can take that back. " +
127 "Reloading…", "ok");
128 setTimeout(() => window.location.reload(), 1500);
129 } catch (err) {
130 console.error("CONTRA could not remove the member", err);
131 say(status, err.status === 409
132 ? "Somebody changed this vault while that was being prepared. " +
133 "Reload and try again."
134 : "Could not remove them. Nothing was changed.", "error");
135 }
136 });
137
138 draw().catch((err) => console.error("CONTRA could not list members", err));
139}
140