sc-postiz-poster/test/compatVectors.js
2026-10-19 01:20:42 -05:00

95 lines
3.8 KiB
JavaScript

// 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);