visit(rc_app_url("storage") . "/"); if ($code !== 200) throw new RuntimeException("storage answered the robot with $code"); $done = true; } return $b; } // Storage's own page API, as the robot's browser at storage. function robot_storage_seal(array $in): array { [$code, , $j] = robot_at_storage()->post(rc_app_url("storage") . "/api/seal", $in); if ($code !== 200) throw new RuntimeException("storage's /api/seal answered $code: " . (is_array($j) ? ($j["error"] ?? "") : substr((string)$j, 0, 120))); return $j; } // The zoo's own route to storage, as the zoo's page calls it. function robot_zoo_storage(string $op, array $body): array { [$code, , $j] = robot_at_zoo()->post("https://" . env("RC_HOST") . "/api/26/storage", ["op" => $op, "body" => $body]); if ($code !== 200) throw new RuntimeException("the zoo's /api/26/storage ($op) answered $code: " . (is_array($j) ? ($j["error"] ?? "") : substr((string)$j, 0, 120))); return $j; } function rdev(array $d): array { return ["id" => $d["id"], "priv" => ec_load($d["pem"]), "pub" => $d["pub"], "pin" => $d["pin"] ?? null, "epoch" => $d["epoch"] ?? 0, "code" => null]; } function new_rdev(): array { $k = ec_new(); return ["id" => s_b64url(random_bytes(18)), "pem" => ec_pem($k), "pub" => s_b64(ec_raw($k))]; } function rdev_entry(array $d, string $k, string $via, string $name): array { return ["id" => $d["id"], "name" => $name, "pub" => $d["pub"], "wrap" => wrap_for_device($d["pub"], $k), "created" => time(), "via" => $via]; } // The robot's devices, set up once per environment: a storage device holding its root (and the // recovery key), and a zoo device holding the zoo's key for it. function robot_sealing(): array { $uid = (string)robot_user()["id"]; $s = json_decode((string)@file_get_contents(rseal_path()), true); if (is_array($s) && ($s["user"] ?? null) === $uid) return $s; robot_at_storage(); $uid = (string)robot_user()["id"]; $have = robot_storage_seal(["op" => "get", "fresh" => true]); if ($have["keyring"] !== null) throw new RuntimeException("the robot has a keyring at storage but not its devices (robot-seal.json was lost): its sealed postcard cannot be opened"); $root = random_bytes(32); [$rb, $phrase] = new_recovery_key(); $sd = new_rdev(); $e = new_root_wrap_key($root); $kr = ["v" => 2, "mk_id" => mk_id($root), "epoch" => 1, "root" => $e, "recovery" => recovery_wrap($rb, $root) + ["created" => time()], "devices" => [rdev_entry($sd, $root, "created", "Zoo robot")], "older" => []]; $kr["mac"] = keyring_mac($root, $kr, $uid, "-"); robot_storage_seal(["op" => "put", "keyring" => $kr, "newproof" => mk_proof($root)]); $sd["pin"] = $kr["mk_id"]; $sd["epoch"] = 1; // The zoo's key, wrapped to the root's public half: the robot holds the root here, so it opens // the entry and the public half it wraps to is the root's own (in a browser: storage's window). open_root_wrap_key($root, $e); $app = random_bytes(32); $zd = new_rdev(); $akr = ["v" => 2, "mk_id" => mk_id($app), "epoch" => 1, "root" => wrap_for_root(["id" => $e["id"], "pub" => $e["pub"]], "zoo", $app), "devices" => [rdev_entry($zd, $app, "created", "Zoo robot")], "older" => []]; $akr["mac"] = keyring_mac($app, $akr, $uid, "zoo"); robot_zoo_storage("keyring", ["op" => "put", "keyring" => $akr, "newproof" => mk_proof($app)]); $zd["pin"] = $akr["mk_id"]; $zd["epoch"] = 1; $s = ["user" => $uid, "storage_dev" => $sd, "zoo_dev" => $zd, "phrase" => $phrase, "made" => time()]; rseal_save($s); return $s; } // The zoo's key for the robot, as its zoo device opens it (rc-seal.js's trust()): [key, keyring]. function robot_zoo_key(array &$s): array { $kr = robot_zoo_storage("keyring", ["op" => "get", "fresh" => true])["keyring"] ?? null; $k = trust_keyring($kr, rdev($s["zoo_dev"]), $s["user"], "zoo"); $s["zoo_dev"]["pin"] = $kr["mk_id"]; $s["zoo_dev"]["epoch"] = $kr["epoch"]; rseal_save($s); return [$k, $kr]; } // A keyring the robot changes: one more epoch, its MAC, the proof of the key it holds. function robot_zoo_write(array &$s, string $k, array $kr, array $devices): array { $next = array_merge($kr, ["epoch" => $kr["epoch"] + 1, "devices" => $devices]); unset($next["mac"]); $next["mac"] = keyring_mac($k, $next, $s["user"], "zoo"); $kr = robot_zoo_storage("keyring", ["op" => "put", "keyring" => $next, "proof" => mk_proof($k)])["keyring"]; $s["zoo_dev"]["epoch"] = $kr["epoch"]; rseal_save($s); return $kr; } const POSTCARD = "sealed/postcard.rce"; // Seal words under the zoo's key and save them as the robot's postcard, through the public paths: // begin, the bytes straight to B2, commit. [file, envelope bytes]. function robot_seal_postcard(string $k, string $words): array { $env = envelope_seal($k, $words, ["name" => "postcard.txt", "type" => "text/plain"]); $b = robot_zoo_storage("begin", ["files" => [["path" => POSTCARD, "size" => strlen($env), "type" => ENV_TYPE]]])["files"][0] ?? []; if (empty($b["url"])) throw new RuntimeException("storage would not begin the postcard: " . json_encode($b)); $put = put_many([[$b["url"], $env, $b["type"]]])[0]; if ($put !== 200) throw new RuntimeException("B2 answered the PUT with $put"); $c = robot_zoo_storage("commit", ["ids" => [$b["id"]]])["files"][0] ?? []; if (empty($c["file"])) throw new RuntimeException("storage would not commit the postcard: " . json_encode($c)); return [$c["file"], $env]; } // The robot's postcard as the bucket holds it (a presigned GET, like a browser's). function robot_postcard_bytes(): array { $u = robot_zoo_storage("urls", ["paths" => [POSTCARD]])["files"][0] ?? []; if (empty($u["url"])) throw new RuntimeException("storage has no postcard for the robot: " . json_encode($u)); [[$code, $bytes]] = get_many([$u["url"]]); if ($code !== 200) throw new RuntimeException("the bucket answered $code"); return [$u, $bytes]; } function postcard_words(): string { $w = ["otter", "lantern", "harbour", "juniper", "comet", "pebble", "meadow", "falcon", "teacup", "glacier", "cinnamon", "lighthouse"]; return "Sealed at " . gmdate("H:i") . " UTC by the zoo's robot. Today's word: " . $w[random_int(0, count($w) - 1)] . "-" . bin2hex(random_bytes(3)) . "."; }