agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
79 lines • 3.65 kB
JavaScript
;
/**
* hosting/signin/seal — the sealed transaction cookie: a sign-in attempt's
* secrets kept in the BROWSER that started it, readable only by this server.
*
* An OIDC sign-in needs `state`, `nonce`, the PKCE verifier and `returnTo` to
* survive the trip to the identity provider and back. Keeping them on the
* server would be state an unauthenticated `GET /auth/login` loop can grow
* without bound, and state a second replica does not have. So they are SEALED
* (AES-256-GCM, authenticated encryption) into a per-attempt cookie:
*
* - nothing is written on the server — the transaction is bounded by the
* cookie's own lifetime (10 minutes);
* - the cookie name is bound into the seal (GCM additional data), so a sealed
* value moved to another attempt's cookie does not open;
* - it binds the login to the browser that started it, which is what stops
* login forgery (an attacker's own callback URL, completed in a victim's
* browser, finds no cookie to open);
* - one cookie per attempt, so two tabs signing in at once do not break each
* other.
*
* The key is `IDENTITY_COOKIE_KEY_FILE` (32 bytes, shared by replicas) or,
* unset, random per process — a restart then invalidates logins in progress,
* and the banner says so.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.unseal = exports.seal = exports.sealKeyFrom = exports.randomSealKey = void 0;
const node_crypto_1 = require("node:crypto");
const IV_BYTES = 12;
const TAG_BYTES = 16;
/** A fresh random key, for a process that was given none. */
function randomSealKey() {
return (0, node_crypto_1.randomBytes)(32);
}
exports.randomSealKey = randomSealKey;
/** Check a key read from a file: exactly 32 bytes, raw or base64/base64url/hex text. */
function sealKeyFrom(bytes) {
if (bytes.length === 32)
return bytes;
const text = bytes.toString('utf8').trim();
for (const encoding of ['base64url', 'base64', 'hex']) {
const decoded = Buffer.from(text, encoding);
if (decoded.length === 32)
return decoded;
}
throw new TypeError(`[hosting] a cookie sealing key is 32 bytes (raw, or as base64 / hex text); this one is not. ` +
`Make one with: openssl rand -base64 32`);
}
exports.sealKeyFrom = sealKeyFrom;
/** Seal a JSON value; `bound` (the cookie name) must match when it is opened. */
function seal(key, bound, value) {
const iv = (0, node_crypto_1.randomBytes)(IV_BYTES);
const cipher = (0, node_crypto_1.createCipheriv)('aes-256-gcm', key, iv);
cipher.setAAD(Buffer.from(bound, 'utf8'));
const body = Buffer.concat([cipher.update(JSON.stringify(value), 'utf8'), cipher.final()]);
return Buffer.concat([iv, cipher.getAuthTag(), body]).toString('base64url');
}
exports.seal = seal;
/** Open a sealed value, or `undefined` when it was not sealed by this key for `bound`. */
function unseal(key, bound, sealed) {
try {
const bytes = Buffer.from(sealed, 'base64url');
if (bytes.length < IV_BYTES + TAG_BYTES + 1)
return undefined;
const decipher = (0, node_crypto_1.createDecipheriv)('aes-256-gcm', key, bytes.subarray(0, IV_BYTES));
decipher.setAAD(Buffer.from(bound, 'utf8'));
decipher.setAuthTag(bytes.subarray(IV_BYTES, IV_BYTES + TAG_BYTES));
const plain = Buffer.concat([
decipher.update(bytes.subarray(IV_BYTES + TAG_BYTES)),
decipher.final(),
]);
return JSON.parse(plain.toString('utf8'));
}
catch {
return undefined;
}
}
exports.unseal = unseal;
//# sourceMappingURL=seal.js.map