95 lines
3.8 KiB
JavaScript
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);
|