103 lines
4.6 KiB
JavaScript
103 lines
4.6 KiB
JavaScript
// Compatibility proof for site-sync's peer-secret sealing and peer signatures.
|
|
//
|
|
// compatVectors.json was recorded from earlier site-sync code with a fixed FAKE
|
|
// session secret. This suite asserts that lib/common/ (and site-sync's own peers
|
|
// store) still derives the same key-encryption key, opens a peer secret sealed by
|
|
// that code, seals what the old algorithm (plain AES-256-GCM with the recorded key)
|
|
// opens, and produces / verifies the same HMAC request and response signatures --
|
|
// so stored peer secrets and existing pairings keep working across 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 } = require(path.join(LIB, "common", "aesGcm"));
|
|
const protocol = require(path.join(LIB, "common", "peerProtocol"));
|
|
const peers = require(path.join(LIB, "peers"));
|
|
|
|
const OWNER = "compatVectors";
|
|
const TAG_BYTES = 16;
|
|
// Hex digits kept when a test truncates a 16-byte tag.
|
|
const SHORT_TAG_HEX = 24;
|
|
|
|
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 legacyOpenHex = (keyHex, sealed) => {
|
|
const decipher = crypto.createDecipheriv("aes-256-gcm", Buffer.from(keyHex, "hex"), Buffer.from(sealed.iv, "hex"), { authTagLength: TAG_BYTES });
|
|
decipher.setAuthTag(Buffer.from(sealed.tag, "hex"));
|
|
return Buffer.concat([decipher.update(Buffer.from(sealed.ciphertext, "hex")), decipher.final()]);
|
|
};
|
|
|
|
|
|
const kek = vectors.keys.siteSyncPeerKek;
|
|
const sealer = createSealer({ owner: OWNER, salt: kek.salt, info: kek.info });
|
|
const stored = vectors.sealed.siteSyncPeerSecret;
|
|
|
|
console.log("[peer-secret key-encryption key]");
|
|
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 sealed peer secret opens", () => {
|
|
assert.equal(sealer.openHex(stored.sealed).toString("hex"), stored.plaintextHex);
|
|
});
|
|
check("a truncated tag is rejected", () => {
|
|
assert.throws(() => sealer.openHex({ ...stored.sealed, tag: stored.sealed.tag.slice(0, SHORT_TAG_HEX) }));
|
|
});
|
|
check("a new seal opens with the old algorithm", () => {
|
|
assert.equal(legacyOpenHex(kek.keyHex, sealer.sealHex("round trip é")).toString("utf8"), "round trip é");
|
|
});
|
|
check("the peers store opens the recorded peer secret", () => {
|
|
const row = { peer_secret_ciphertext: stored.sealed.ciphertext, peer_secret_iv: stored.sealed.iv, peer_secret_tag: stored.sealed.tag };
|
|
assert.equal(peers.openSecret(row).toString("hex"), stored.plaintextHex);
|
|
});
|
|
|
|
console.log("\n[HMAC request + response signatures]");
|
|
const secret = Buffer.from(vectors.hmac.secretHex, "hex");
|
|
vectors.hmac.siteSync.requests.forEach((r, i) => {
|
|
check(`request #${i}: host, canonical and signature identical`, () => {
|
|
assert.equal(protocol.normalizeHost(r.targetHost), r.normalizedHost);
|
|
const canonical = protocol.buildCanonical({ ...r, targetHost: protocol.normalizeHost(r.targetHost) });
|
|
assert.equal(canonical, r.canonical);
|
|
assert.equal(protocol.sign(secret, canonical), r.signature);
|
|
assert.ok(protocol.verifySignature(secret, r.canonical, r.signature));
|
|
assert.ok(!protocol.verifySignature(secret, r.canonical, "00" + r.signature.slice(2)));
|
|
});
|
|
});
|
|
vectors.hmac.siteSync.responses.forEach((r, i) => {
|
|
check(`response #${i}: canonical and signature identical`, () => {
|
|
const response = { nonce: r.nonce, status: r.status, body: Buffer.from(r.bodyHex, "hex") };
|
|
assert.equal(protocol.buildResponseCanonical(response), r.canonical);
|
|
assert.equal(protocol.signResponse(secret, response), r.signature);
|
|
assert.ok(protocol.verifyResponseSignature(secret, response, r.signature));
|
|
assert.ok(!protocol.verifyResponseSignature(secret, { ...response, status: r.status + 1 }, r.signature));
|
|
});
|
|
});
|
|
|
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
process.exit(failed === 0 ? 0 : 1);
|