| 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 | |
| 15 | import { randomBytes, toHex } from "../crypto/bytes.js"; |
| 16 | import { net } from "../crypto/net.js"; |
| 17 | import * as sharing from "../crypto/sharing.js"; |
| 18 | |
| 19 | const $ = (sel, root = document) => root.querySelector(sel); |
| 20 | |
| 21 | function 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 | |
| 28 | function 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. |
| 35 | export 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 | |