// Compatibility proof for postiz-poster's sealed API key. // // compatVectors.json was recorded from earlier postiz-poster code with a fixed // FAKE session secret. This suite asserts that lib/common/ (and the plugin's own // lib/apiKey.js) still derives the same key, opens an API key sealed by that code, // rejects a truncated blob, and seals what the old algorithm (plain AES-256-GCM // with the recorded key) opens -- so a configured API key survives upgrades. // // node test/compatVectors.js // // Exit code is 0 only if every check passed. const assert = require("node:assert/strict"); const crypto = require("node:crypto"); const fs = require("node:fs"); const path = require("node:path"); const LIB = path.resolve(__dirname, "..", "lib"); const vectors = JSON.parse(fs.readFileSync(path.join(__dirname, "compatVectors.json"), "utf8")); process.env.SALTCORN_SESSION_SECRET = vectors.sessionSecret; const { deriveKey } = require(path.join(LIB, "common", "sessionKey")); const { createSealer, isSealedString } = require(path.join(LIB, "common", "aesGcm")); const apiKey = require(path.join(LIB, "apiKey")); const OWNER = "compatVectors"; const IV_BYTES = 12; const TAG_BYTES = 16; const BLOB_PREFIX = "scs1:"; // Tag bytes left in a deliberately truncated blob. const SHORT_TAG_BYTES = 4; let passed = 0; let failed = 0; const check = (name, fn) => { try { fn(); passed++; console.log(` PASS ${name}`); } catch (e) { failed++; console.log(` FAIL ${name}\n ${e && e.message ? e.message.split("\n")[0] : e}`); } }; // The OLD algorithm, independently: AES-256-GCM with the recorded key. const legacyOpen = (keyHex, iv, tag, ciphertext) => { const decipher = crypto.createDecipheriv("aes-256-gcm", Buffer.from(keyHex, "hex"), iv, { authTagLength: TAG_BYTES }); decipher.setAuthTag(tag); return Buffer.concat([decipher.update(ciphertext), decipher.final()]); }; const legacyOpenString = (keyHex, blob) => { const raw = Buffer.from(blob.slice(BLOB_PREFIX.length), "base64"); return legacyOpen(keyHex, raw.subarray(0, IV_BYTES), raw.subarray(IV_BYTES, IV_BYTES + TAG_BYTES), raw.subarray(IV_BYTES + TAG_BYTES)).toString("utf8"); }; const kek = vectors.keys.postizApiKeyKek; const sealer = createSealer({ owner: OWNER, salt: kek.salt, info: kek.info }); const stored = vectors.sealed.postizApiKey; console.log("[API key sealing]"); check("HKDF key matches the recorded one", () => { assert.equal(deriveKey({ owner: OWNER, salt: kek.salt, info: kek.info, bytes: kek.bytes }).toString("hex"), kek.keyHex); }); check("the recorded API key blob opens", () => { assert.equal(sealer.openString(stored.blob), stored.plaintext); }); check("a truncated blob is rejected", () => { const raw = Buffer.from(stored.blob.slice(BLOB_PREFIX.length), "base64"); assert.equal(sealer.openString(BLOB_PREFIX + raw.subarray(0, IV_BYTES + SHORT_TAG_BYTES).toString("base64")), null); }); check("a new string seal opens with the old algorithm", () => { const blob = sealer.sealString("new-api-key"); assert.ok(isSealedString(blob)); assert.equal(legacyOpenString(kek.keyHex, blob), "new-api-key"); }); check("a new struct seal opens with the old algorithm", () => { const sealed = sealer.sealHex("round trip é"); assert.equal(legacyOpen(kek.keyHex, Buffer.from(sealed.iv, "hex"), Buffer.from(sealed.tag, "hex"), Buffer.from(sealed.ciphertext, "hex")).toString("utf8"), "round trip é"); }); console.log("\n[lib/apiKey.js binding]"); check("resolveApiKey opens the recorded sealed key", () => { assert.equal(apiKey.resolveApiKey({ apiKey: stored.blob }), stored.plaintext); }); console.log(`\n${passed} passed, ${failed} failed`); process.exit(failed === 0 ? 0 : 1);