27, "wing" => "Sealed", "kind" => "self", "title" => "A new device needs an old one", "promise" => "A device that never opened your sealed files gets in only with a code typed on one that has, or with your recovery key.", "block" => "rc-seal.js: seal.request() shows a code, seal.approve(code) on a device that has the key lets the new one in, seal.useStorage() lets storage's window do it with your recovery key; every keyring is checked under the key it guards.", "files" => ["lib/robotseal.php", "lib/seal.php"], "show" => function (?array $me): string { if (!$me) return '

Sign in, seal a postcard (26), then open this page in a private window.

'; return '

Loading…

'; }, "check" => function (): array { require_once ZOO_ROOT . "/lib/robotseal.php"; $s = robot_sealing(); [$kA, $kr] = robot_zoo_key($s); $words = postcard_words(); robot_seal_postcard($kA, $words); $steps = []; // B: a browser that never opened it. It shows a code and asks. $bk = ec_new(); $B = ["id" => s_b64url(random_bytes(18)), "priv" => $bk, "pub" => s_b64(ec_raw($bk)), "pin" => null, "epoch" => 0, "code" => new_code()]; try { robot_zoo_storage("keyring", ["op" => "request", "id" => $B["id"], "name" => "Robot's new device", "pub" => $B["pub"], "tag" => request_tag($B["code"], $B["id"], $B["pub"])]); try { trust_keyring($kr, $B, $s["user"], "zoo"); return [false, "a device that was never let in opened the keyring"]; } catch (Throwable) {} [, $held] = robot_postcard_bytes(); try { envelope_open(fn() => null, $held); return [false, "the postcard opened with no key"]; } catch (Throwable) {} $steps[] = "locked, showing a code"; // A, the robot's first device, is told a wrong code: no waiting device matches it. $reqs = robot_zoo_storage("keyring", ["op" => "get", "fresh" => true])["requests"]; $mine = array_values(array_filter($reqs, fn($q) => $q["id"] === $B["id"])); if (!$mine) return [false, "storage does not show the new device's request"]; $wrong = new_code(); foreach ($reqs as $q) if (check_request($wrong, $q)) return [false, "a wrong code matched a waiting device"]; $steps[] = "a wrong code refused"; // The server swaps the waiting device's public key for one of its own (the check plays the server). $swapped = $mine[0]; $swapped["pub"] = s_b64(ec_raw(ec_new())); if (check_request($B["code"], $swapped)) return [false, "a key the server swapped into the request was accepted"]; $steps[] = "a key the server swapped into the request refused"; // The right code: A wraps the zoo's key for B and vouches for it under the code. $q = $mine[0]; if (!check_request($B["code"], $q)) return [false, "the right code did not match the request"]; $w = wrap_for_device($q["pub"], $kA); $kr = robot_zoo_write($s, $kA, $kr, [...$kr["devices"], ["id" => $q["id"], "name" => $q["name"], "pub" => $q["pub"], "wrap" => $w, "ok" => approval_tag($B["code"], $q["id"], $q["pub"], $w, $kr["mk_id"]), "created" => time(), "via" => "approved"]]); // The server hands B a wrap of a key of its own instead (keeping A's tag): refused. $fake = $kr; foreach ($fake["devices"] as &$d) if ($d["id"] === $B["id"]) $d["wrap"] = wrap_for_device($B["pub"], random_bytes(32)); unset($d); try { trust_keyring($fake, $B, $s["user"], "zoo"); return [false, "a wrap of a key the server chose was accepted"]; } catch (Throwable) {} $steps[] = "a key the server swapped in for the new device refused"; $kB = trust_keyring(robot_zoo_storage("keyring", ["op" => "get", "fresh" => true])["keyring"], $B, $s["user"], "zoo"); [, $held] = robot_postcard_bytes(); [, $back] = envelope_open(fn($id) => $id === $kr["mk_id"] ? $kB : null, $held); if ($back !== $words) return [false, "the new device opened other words"]; $steps[] = "with the right code, the new device opens the postcard"; // C: nothing but the recovery key. In a browser this happens in storage's window. $root = robot_storage_seal(["op" => "get", "fresh" => true])["keyring"]; $R = recovery_unwrap($s["phrase"], $root["recovery"]); if (mk_id($R) !== $root["mk_id"] || !check_keyring($R, $root, $s["user"], "-")) return [false, "the recovery key does not open a keyring that checks out"]; $M = unwrap_from_root(open_root_wrap_key($R, $root["root"]), $root["root"]["pub"], "zoo", $kr["mk_id"], $kr["root"]); if (!check_keyring($M, $kr, $s["user"], "zoo")) return [false, "the zoo's keyring does not check out under the key the recovery key opened"]; [, $back] = envelope_open(fn($id) => $id === $kr["mk_id"] ? $M : null, $held); if ($back !== $words) return [false, "the recovery key opened other words"]; $steps[] = "the recovery key opens it too"; } finally { // B leaves again, so the robot's keyring does not grow every five minutes. try { [$kA, $kr] = robot_zoo_key($s); if (array_filter($kr["devices"], fn($d) => $d["id"] === $B["id"])) robot_zoo_write($s, $kA, $kr, array_values(array_filter($kr["devices"], fn($d) => $d["id"] !== $B["id"]))); robot_zoo_storage("keyring", ["op" => "decline", "id" => $B["id"]]); } catch (Throwable) {} } return [true, "a new device of the robot's: " . implode("; ", $steps), ["steps" => $steps]]; }, "script" => <<<'JS' (async () => { const box = document.getElementById("seal27"); if (!box) return; for (let i = 0; i < 100 && !window.zooSeal; i++) await new Promise((r) => setTimeout(r, 100)); const seal = window.zooSeal; if (!seal) { box.innerHTML = '

Card 26 did not start (see there).

'; return; } let stop = null; async function render(flash) { if (stop) { stop(); stop = null; } for (let i = 0; i < 100 && seal.status === "loading"; i++) await new Promise((r) => setTimeout(r, 100)); const st = seal.status; if (st === "nostorage" || st === "none" || st === "unsupported") { box.innerHTML = '

Seal a postcard first (26).

'; return; } if (st === "locked") { let code = "…"; try { code = await seal.request(); } catch (e) { code = ""; } box.innerHTML = '

This browser has never opened your postcard. On a device that has, type this code in card 27:

' + '

' + zoo.esc(code) + '

It works for 15 minutes. Waiting…

' + '

' + '

Storage opens in a small window; your recovery key is typed there, never here.

'; document.getElementById("seal-recover").onclick = async (ev) => { ev.target.disabled = true; document.getElementById("seal-rout").textContent = "in storage's window…"; try { await seal.useStorage(); window.zooSealRender && window.zooSealRender("Let in with your recovery key, in storage's window."); render(); } catch (e) { ev.target.disabled = false; document.getElementById("seal-rout").textContent = e.message; } }; stop = seal.poll((s) => { if (s === "ready") { window.zooSealRender && window.zooSealRender("Let in by a device that had the key."); render(); } }); return; } const devs = seal.devices.map((d) => '
  • ' + zoo.esc(d.name) + (d.mine ? ' this browser' : '') + ' ' + zoo.esc(d.via) + '
  • ').join(""); box.innerHTML = (flash ? '

    ' + zoo.esc(flash) + '

    ' : '') + '

    Open this page in a private window (or on another device), sign in, and type the code it shows here:

    ' + '

    ' + '

    Your devices that open the zoo\'s postcards:

    '; document.getElementById("seal-approve").onsubmit = async (ev) => { ev.preventDefault(); const out = document.getElementById("seal-aout"); out.textContent = "checking the code…"; try { const n = await seal.approve(document.getElementById("seal-code-in").value); render("Let in: " + n + "."); } catch (e) { out.textContent = e.message; } }; } render(); })(); JS, ];