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 = '