Guarantee only a Browser device can decrypt
You'll mint a key once, keep its public half on your server, and from then on encrypt to it whatever only that device may read. The installation needs to be enrolled first.
Ask for a key challenge
On your server, use issueKeyChallenge with purpose: "decrypt" and the device ID you expect, and send the keyChallenge string to the client with its nonce.
app.post("/key/challenge", async (req, res) => {
const deviceId = await deviceOf(accountOf(req));
const { nonce, keyChallenge } = await rh.issueKeyChallenge({
purpose: "decrypt",
expectedDevices: [deviceId],
});
res.json({ nonce, keyChallenge });
});In a browser, keys go through a browser extension you build: the page opens an envelope on every use, and a link handler would open your app each time.
Mint the key
In the page, send the key challenge to your extension as a mint message. Your app, the extension's native messaging host, answers it with RootHeraldMintKey, the attestation key blob and a buffer for the new key, keeps the key blob itself, and returns the certification. Post the certification and its nonce to your server.
const { nonce, keyChallenge } = await postJSON("/key/challenge", {});
const { certification } = await chrome.runtime.sendMessage(EXTENSION_ID, {
type: "mint",
keyChallenge,
});
await postJSON("/key/certify", { nonce, certification });Keep the key
On your server, pass the certification and nonce to certifyKey. It returns the key: keep the whole result with the account that deviceId belongs to, since encryptToDevice reads its alg and format as well as the jwk.
app.post("/key/certify", async (req, res) => {
const { nonce, certification } = req.body;
const key = await rh.certifyKey(nonce, certification);
await saveDecryptKey(accountOf(req), key.deviceId, key);
res.sendStatus(204);
});Encrypt to the key
On your server, use encryptToDevice with the key you kept and the bytes only this device may read. It runs locally and returns the JWE string; send that to the page.
import { encryptToDevice } from "@rootherald/node";
app.post("/session", async (req, res) => {
const key = await decryptKeyOf(accountOf(req));
const token = await issueSessionToken(accountOf(req));
res.json({ envelope: encryptToDevice(key, token) });
});Open it on the device
In the page, send the envelope to your extension as a decrypt message and use the plaintext it returns. Your app loads the blob with RootHeraldLoadKey and opens the envelope with RootHeraldDecrypt: no network, no challenge, no prompt.
const { envelope } = await postJSON("/session", {});
const { plaintext } = await chrome.runtime.sendMessage(EXTENSION_ID, {
type: "decrypt",
envelope,
});
useSessionToken(plaintext);If the TPM has been cleared, every blob is gone, the attestation key's included: your app gets RH_ERR_KEY_UNLOADABLE. Have it enroll again, then mint a new key. The device ID stays the same; the key ID is new.
Root Herald bridges are in development and coming soon: an extension and host your pages will be able to use directly, instead of building your own.