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