'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const { PassThrough, Readable } = require('node:stream'); const { BridgeAuth, BridgeAuthError, computeCookieProof, computeServerProof, generatePairingSecret, readJsonBody, createRequestLimiter } = require('../src/bridge-auth'); const EXTENSION_ORIGIN = 'chrome-extension://onbkfpbggekakjddomjjnboippimlmch'; const CLIENT_NONCE = Buffer.alloc(32, 1).toString('base64url'); const protect = (plaintext) => Buffer.from(`protected:${plaintext}`, 'utf8'); const unprotect = (ciphertext) => { const value = Buffer.from(ciphertext).toString('utf8'); if (!value.startsWith('protected:')) throw new Error('bad ciphertext'); return value.slice('protected:'.length); }; function createAuth(options = {}) { return new BridgeAuth({ expectedOrigin: EXTENSION_ORIGIN, protect, unprotect, ...options }); } test('pairing secrets are random and persisted only in a versioned protected envelope', () => { const first = generatePairingSecret(); const second = generatePairingSecret(); assert.match(first, /^[A-Za-z0-9_-]{43}$/); assert.notEqual(first, second); const auth = createAuth(); const { secret, envelope } = auth.rotate(); assert.deepEqual(Object.keys(envelope).sort(), ['algorithm', 'ciphertext', 'digest', 'version']); assert.equal(envelope.version, 2); assert.equal(envelope.algorithm, 'electron-safe-storage'); assert.equal(JSON.stringify(envelope).includes(secret), false); assert.equal(Buffer.from(envelope.ciphertext, 'base64').toString('utf8').includes(secret), true, 'test protector is intentionally transparent after decoding; disk JSON itself is never plaintext'); }); test('protected envelope restores server authentication without request-time scrypt', () => { let protectCalls = 0; let unprotectCalls = 0; const first = new BridgeAuth({ protect(value) { protectCalls += 1; return protect(value); }, unprotect(value) { unprotectCalls += 1; return unprotect(value); } }); const { secret, envelope } = first.rotate(); const restored = new BridgeAuth({ envelope: JSON.parse(JSON.stringify(envelope)), protect, unprotect(value) { unprotectCalls += 1; return unprotect(value); } }); const challenge = restored.issueChallenge(CLIENT_NONCE); assert.equal(protectCalls, 1); assert.equal(unprotectCalls, 1); assert.equal(challenge.serverProof, computeServerProof(secret, CLIENT_NONCE, challenge.serverNonce)); assert.equal(Object.hasOwn(challenge, 'secret'), false); }); test('challenge proof and cookie proof are domain-separated, constant-time authenticated, and one-time', () => { const auth = createAuth(); const secret = auth.rotate().secret; const challenge = auth.issueChallenge(CLIENT_NONCE); const cookieRequest = { clientNonce: CLIENT_NONCE, serverNonce: challenge.serverNonce, deploymentUrl: 'https://customer.avasecurity.com', cookieValue: 'synthetic-cookie' }; cookieRequest.proof = computeCookieProof(secret, cookieRequest); assert.notEqual(challenge.serverProof, cookieRequest.proof); assert.equal(auth.authenticateCookie(cookieRequest), true); assert.equal(auth.authenticateCookie(cookieRequest), false, 'challenge replay must fail'); }); test('challenge cache is bounded, expires entries, and rejects malformed nonces', () => { let now = 100; const auth = createAuth({ maxChallenges: 1, challengeTtlMs: 50, now: () => now }); auth.rotate(); auth.issueChallenge(CLIENT_NONCE); assert.throws(() => auth.issueChallenge(Buffer.alloc(32, 2).toString('base64url')), (error) => error.code === 'CHALLENGE_CAPACITY'); now = 151; const replacement = auth.issueChallenge(Buffer.alloc(32, 2).toString('base64url')); assert.match(replacement.serverNonce, /^[A-Za-z0-9_-]{43}$/); assert.throws(() => auth.issueChallenge('short'), (error) => error.code === 'INVALID_NONCE'); }); test('expired and forged cookie proofs fail and consume the one-time challenge', () => { let now = 100; const auth = createAuth({ challengeTtlMs: 50, now: () => now }); const secret = auth.rotate().secret; const expired = auth.issueChallenge(CLIENT_NONCE); now = 151; assert.equal(auth.authenticateCookie({ clientNonce: CLIENT_NONCE, serverNonce: expired.serverNonce, deploymentUrl: 'https://customer.avasecurity.com', cookieValue: 'cookie', proof: computeCookieProof(secret, { clientNonce: CLIENT_NONCE, serverNonce: expired.serverNonce, deploymentUrl: 'https://customer.avasecurity.com', cookieValue: 'cookie' }) }), false); now = 200; const forged = auth.issueChallenge(CLIENT_NONCE); const request = { clientNonce: CLIENT_NONCE, serverNonce: forged.serverNonce, deploymentUrl: 'https://customer.avasecurity.com', cookieValue: 'cookie', proof: 'A'.repeat(43) }; assert.equal(auth.authenticateCookie(request), false); request.proof = computeCookieProof(secret, request); assert.equal(auth.authenticateCookie(request), false, 'forged attempt consumes challenge'); }); test('rotation clears outstanding challenges and revoke disables challenge issuance', () => { const auth = createAuth(); auth.rotate(); const prior = auth.issueChallenge(CLIENT_NONCE); auth.rotate(); assert.equal(auth.authenticateCookie({ clientNonce: CLIENT_NONCE, serverNonce: prior.serverNonce, deploymentUrl: 'https://customer.avasecurity.com', cookieValue: 'cookie', proof: 'A'.repeat(43) }), false); auth.revoke(); assert.throws(() => auth.issueChallenge(CLIENT_NONCE), (error) => error.code === 'PAIRING_UNAVAILABLE'); assert.equal(auth.envelope, null); }); test('invalid origin and protected-envelope dependencies fail closed', () => { assert.throws(() => new BridgeAuth({ expectedOrigin: 'https://example.test', protect, unprotect }), (error) => error instanceof BridgeAuthError && error.code === 'INVALID_EXTENSION_ORIGIN'); assert.throws(() => new BridgeAuth({ expectedOrigin: 'chrome-extension://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', protect, unprotect }), (error) => error instanceof BridgeAuthError && error.code === 'INVALID_EXTENSION_ORIGIN'); assert.throws(() => new BridgeAuth(), (error) => error.code === 'SECURE_STORAGE_UNAVAILABLE'); assert.throws(() => new BridgeAuth({ protect, unprotect, envelope: { version: 1, algorithm: 'scrypt' } }), (error) => error.code === 'INVALID_SECRET_ENVELOPE'); }); test('readJsonBody accepts a bounded JSON object and rejects malformed or oversized input', async () => { assert.deepEqual(await readJsonBody(Readable.from(['{"ok":true}']), { maxBytes: 64 }), { ok: true }); await assert.rejects(readJsonBody(Readable.from(['{"nope"']), { maxBytes: 64 }), (error) => error.code === 'MALFORMED_JSON'); await assert.rejects(readJsonBody(Readable.from(['{"value":"', 'x'.repeat(100), '"}']), { maxBytes: 32 }), (error) => error.code === 'BODY_TOO_LARGE'); await assert.rejects(readJsonBody(Readable.from(['[]']), { maxBytes: 64 }), (error) => error.code === 'INVALID_JSON_BODY'); }); test('readJsonBody aborts a slow body at its absolute deadline', async () => { const stream = new PassThrough(); const pending = readJsonBody(stream, { maxBytes: 64, deadlineMs: 15 }); stream.write('{'); await assert.rejects(pending, (error) => error.code === 'BODY_DEADLINE_EXCEEDED'); }); test('request limiter rejects excess concurrent work before it begins', async () => { const limiter = createRequestLimiter({ maxConcurrent: 1 }); let release; const active = limiter.run(() => new Promise((resolve) => { release = resolve; })); let entered = false; await assert.rejects(limiter.run(async () => { entered = true; }), (error) => error.code === 'TOO_MANY_REQUESTS'); assert.equal(entered, false); release('done'); assert.equal(await active, 'done'); assert.equal(limiter.active, 0); });