"sha384-bC+ArqaU/QjwcbSzAMUu6nMqClMDFOG4cHed3KFtzzYt052HX2z+hJ0Timbw2yoX", "rc-seal.js" => "sha384-9iwPEKMkzo7FSo5YaKL+wOCJSA+zaGiotVBJcMkzBnGex/wljIj/v1Nkz5mHu2rF", ]; const POSTCARD_MAX = 65536; // the zoo seals postcards of words, nothing bigger, into a person's storage return [ "n" => 26, "try" => "seal a postcard", "wing" => "Sealed", "kind" => "self", "title" => "Sealed means sealed", "promise" => "A file an app seals for you opens on your devices and nowhere else; the server keeps only what it cannot read.", "block" => "storage's sealing block: rc-crypt.js and rc-seal.js (https://storage./seal/v1/) seal in the browser with a key only your devices and your recovery key open; the app's server forwards keyring calls and stores envelopes it cannot read.", "files" => ["lib/bulk.php", "lib/robotseal.php", "lib/seal.php"], "show" => function (?array $me): string { $s = stored(26); $last = $s && $s["ok"] ? '

The self-check seals a fresh postcard for the robot each time, ' . ago((int)$s["at"]) . ': ' . (int)($s["data"]["size"] ?? 0) . ' bytes in the bucket, none of them its words.

' : ""; if (!$me) return '

Sign in to seal a postcard only your devices can open.

' . $last; $tags = ""; foreach (SEAL_JS as $f => $sri) $tags .= '"; return $tags . '

Loading…

' . $last; }, // The app's own route to storage for its page: keyring calls as they are; begin only for the // postcard, sealed; commit and urls as they are. Storage checks the person and the folder. "api" => function (string $do, ?array $me, array $in, bool $post): ?array { if ($do !== "storage" || !$post) return null; if (!$me) return ["error" => "sign in first", "status" => 401]; $op = (string)($in["op"] ?? ""); $body = is_array($in["body"] ?? null) ? $in["body"] : []; if (!in_array($op, ["keyring", "begin", "commit", "urls"], true)) return ["error" => "no such call", "status" => 400]; if ($op === "begin") { $f = $body["files"] ?? []; if (count($f) !== 1 || ($f[0]["path"] ?? "") !== "sealed/postcard.rce" || ($f[0]["type"] ?? "") !== "application/x-rc-encrypted" || (int)($f[0]["size"] ?? 0) > POSTCARD_MAX) return ["error" => "the zoo saves one sealed postcard, at most 64 KB", "status" => 400]; } if ($op === "urls") $body = ["paths" => ["sealed/postcard.rce"]]; [$s, $r] = storage_call($op, $body); return $r + ["status" => $s]; }, "check" => function (): array { require_once ZOO_ROOT . "/lib/robotseal.php"; $s = robot_sealing(); [$k, $kr] = robot_zoo_key($s); $words = postcard_words(); [$file, $env] = robot_seal_postcard($k, $words); [$u, $held] = robot_postcard_bytes(); $word = substr($words, strrpos($words, " ") + 1, -1); if ($held !== $env) return [false, "the bucket holds other bytes than the robot sealed"]; if (substr($held, 0, 4) !== "RCE1") return [false, "the stored object does not start with an envelope header"]; if (str_contains($held, $word) || str_contains($held, "robot")) return [false, "the stored bytes contain the words"]; try { [$meta, $back] = envelope_open(fn($id) => $id === $kr["mk_id"] ? $k : null, $held); } catch (Throwable $e) { return [false, "the robot's own device cannot open what it sealed: " . $e->getMessage()]; } if ($back !== $words) return [false, "the robot's device opened other words than it sealed"]; try { envelope_open(random_bytes(32), $held); return [false, "a key that is not the robot's opened the postcard"]; } catch (Throwable) {} // Back of house (ST-2): last night's sample of sealed objects, each starting with an envelope header. [$c, $sc] = [0, null]; $h = curl_init(rc_app_url("storage") . "/sealcheck.json"); curl_setopt_array($h, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10]); $sc = json_decode((string)curl_exec($h), true); $night = is_array($sc) && $sc["at"] ? ($sc["ok"] ? "last night's header check: {$sc['sampled']} of {$sc['sealed']} sealed objects sampled, every one an envelope" : "last night's header check found {$sc['bad']} sealed object(s) without an envelope header") : "the nightly header check has not run yet"; $data = ["id" => $file["id"], "size" => strlen($held), "at" => $file["updated"], "head" => bin2hex(substr($held, 0, 24)), "words" => strlen($words), "sealcheck" => $sc]; if (is_array($sc) && $sc["at"] && !$sc["ok"]) return [false, $night, $data]; if (is_array($sc) && $sc["at"] && time() - (int)$sc["at"] > 26 * 3600) $data["amber"] = true; return [true, "sealed " . strlen($words) . " characters for the robot into a " . strlen($held) . "-byte envelope (Apps/Zoo/sealed/postcard.rce, file {$file['id']}): the bucket's bytes hold none of the words and open only with the robot's key; $night", $data]; }, "script" => <<<'JS' (async () => { const box = document.getElementById("seal26"); if (!box) return; const say = (html) => { box.innerHTML = html; }; if (!window.RCSeal) { say('

storage\'s sealing scripts did not load, or not the ones this page pins

'); return; } const call = async (op, body) => { const j = await zoo.call("/api/26/storage", { op, body }); if (j.error) throw new Error(j.error); return j; }; const seal = RCSeal.app({ app: "zoo", user: box.dataset.user, call: (body) => call("keyring", body) }); window.zooSeal = seal; const hex = (u) => [...u].map((x) => x.toString(16).padStart(2, "0")).join(" "); const when = (t) => new Date(t * 1000).toLocaleString(undefined, { hour: "2-digit", minute: "2-digit", month: "short", day: "numeric" }); // The postcard as storage and the bucket hold it, and, on a device that can, opened. async function postcard() { const u = (await call("urls", {})).files[0]; if (!u || !u.url) return null; const bytes = new Uint8Array(await (await fetch(u.url)).arrayBuffer()); let opened = null, why = ""; if (seal.status === "ready") { try { const d = await RCCrypt.decrypt((id) => seal.keyFor(id), new Blob([bytes])); opened = { words: await d.blob.text(), meta: d.meta }; } catch (e) { why = e.message; } } return { u, bytes, opened, why }; } function held(p) { const head = hex(p.bytes.slice(0, 48)); return '

What storage and the bucket hold (' + p.bytes.length + ' bytes, as B2 serves them)

' + '
' + zoo.esc(head) + ' …
' + '

It starts RCE1: an envelope. Your words are not in it. What the server knows: ' + p.u.size + ' bytes, saved ' + zoo.esc(when(p.u.updated)) + ', a random file name. Not the words, not that they are words.

'; } async function render(flash) { try { await seal.load(); } catch (e) { say('

' + zoo.esc(e.message) + '

'); return; } const st = seal.status; if (st === "unsupported") { say('

This browser cannot seal (no WebCrypto or IndexedDB here).

'); return; } if (st === "nostorage" || st === "none") { say('

The zoo seals a postcard with a key of its own, kept under yours: only your devices and your recovery key open it.

' + '

' + '

' + (st === "nostorage" ? "Storage opens in a small window: you make your key and write down a recovery key, once." : "Storage opens in a small window and hands the zoo your key's public half.") + '

'); document.getElementById("seal-setup").onclick = async (ev) => { ev.target.disabled = true; document.getElementById("seal-out").textContent = "in storage's window…"; try { await seal.setup(); render("The zoo now seals with its own key, kept under yours."); } catch (e) { ev.target.disabled = false; document.getElementById("seal-out").textContent = e.message; } }; return; } const p = await postcard().catch(() => null); if (st === "locked") { say('

locked This browser cannot open your sealed postcard.' + (seal.why ? ' ' + zoo.esc(seal.why) + '' : '') + ' Card 27 lets it in.

' + (p ? held(p) : '')); return; } let html = (flash ? '

' + zoo.esc(flash) + '

' : '') + '

'; if (p) { html += '

Opened on this device

' + (p.opened ? '
' + zoo.esc(p.opened.words) + '

sealed ' + zoo.esc(new Date(p.opened.meta.mtime).toLocaleString()) + '

' : '

does not open: ' + zoo.esc(p.why) + '

') + '
' + held(p) + '
'; } say(html); document.getElementById("seal-form").onsubmit = async (ev) => { ev.preventDefault(); const out = document.getElementById("seal-out"), words = document.getElementById("seal-words").value; out.textContent = "sealing in this browser…"; try { await seal.load(); if (seal.status !== "ready") throw new Error("this browser cannot seal right now: " + (seal.why || seal.status)); const env = await RCCrypt.encrypt(seal.key, new Blob([words]), { name: "postcard.txt", type: "text/plain" }); const b = (await call("begin", { files: [{ path: "sealed/postcard.rce", size: env.size, type: RCCrypt.TYPE }] })).files[0]; if (!b.url) throw new Error(b.error || "storage would not begin it"); const put = await fetch(b.url, { method: "PUT", headers: { "Content-Type": b.type }, body: env }); if (!put.ok) throw new Error("B2 answered " + put.status); const c = (await call("commit", { ids: [b.id] })).files[0]; if (!c.file) throw new Error(c.error || "storage would not commit it"); render("Sealed here, sent straight to B2 as " + env.size + " bytes."); } catch (e) { out.textContent = e.message; } }; } window.zooSealRender = render; render(); })(); JS, ];