import assert from 'node:assert/strict'; import { test } from 'node:test'; import { authMessage, buildAuthPayload, canonicalJson, decodeResponse, ed25519PublicKeyHex, frame, pqDecrypt, pqEncrypt, pqTransportKey, signAuth, } from '../nodes/BrowserCli/protocol'; // Known-answer vectors produced by the real Python implementation // (browser_cli.auth.*) so the TypeScript port is provably byte-compatible. // Regenerate with the snippet in the PR description if the protocol changes. const PEM = '-----BEGIN PRIVATE KEY-----\n' + 'MC4CAQAwBQYDK2VwBCIEIAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4f\n' + '-----END PRIVATE KEY-----\n'; const PUB_HEX = '03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8'; const MSG = { id: 'abc-123', command: 'dom.type', args: { selector: '#q', text: 'héllo ☃ "x"' }, user_agent: 'browser-cli/0.15.4', accept_encoding: { ser: ['json'], comp: [] }, }; const CANON = '{"accept_encoding":{"comp":[],"ser":["json"]},"args":{"selector":"#q",' + '"text":"h\\u00e9llo \\u2603 \\"x\\""},"command":"dom.type","id":"abc-123",' + '"user_agent":"browser-cli/0.15.4"}'; const NONCE_HEX = '11'.repeat(32); const SIG_NOPQ = '13734cce77d1c861995e003041c66e555569d53df660b0584858247eee2e98fc' + 'ad13a4cef880c0fe732f8559e990e889d343f002f2e6554d4149bb5d7e31670e'; const SECRET_HEX = '202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f'; const SIG_PQ = 'e06f97ee0b628d7c84f570dc5ea07490eade9372da2ef145c9f06521b05778c8' + '25b9b268e15e75b7e0bbde784071852b9c6046dfee0839057f5e096b968e8f04'; const TKEY_REQ = 'cf792b1e9b96642ef86c113b4ab5826661fb64e9f04d028c7f10f514ae6553d4'; const TKEY_RESP = '72f3219bc809b7fbec0185f514b577cfec60d1264193da6afcc27d860f0382b7'; const DECRYPT_ENV = { alg: 'ML-KEM-768+ChaCha20Poly1305', nonce: 'a77e8a72e13f49aab713e0dc', ciphertext: '7e4f75fa68098ea9a162dfd49af7824526186b77e9ac346b58f30d73df2bef88d5e6cd', }; const DECRYPT_PLAIN = 'hello world payload'; test('canonicalJson matches Python json.dumps(sort_keys, ensure_ascii)', () => { assert.equal(canonicalJson(MSG), CANON); }); test('canonicalJson sorts nested keys and omits undefined', () => { assert.equal(canonicalJson({ b: 1, a: { d: 4, c: 3 }, z: undefined }), '{"a":{"c":3,"d":4},"b":1}'); }); test('ed25519PublicKeyHex derives the raw public key from the PEM', () => { assert.equal(ed25519PublicKeyHex(PEM), PUB_HEX); }); test('ed25519PublicKeyHex tolerates escaped newlines and surrounding whitespace', () => { assert.equal(ed25519PublicKeyHex(PEM.replace(/\n/g, '\\n')), PUB_HEX); assert.equal(ed25519PublicKeyHex(` \n${PEM}\n `), PUB_HEX); }); test('ed25519PublicKeyHex rebuilds a PEM with its line breaks stripped', () => { // What a single-line/password credential field does to a multi-line PEM. assert.equal(ed25519PublicKeyHex(PEM.replace(/\n/g, '')), PUB_HEX); assert.equal(ed25519PublicKeyHex(PEM.replace(/\n/g, ' ')), PUB_HEX); }); test('loadPrivateKey throws a helpful error on a non-PEM value', () => { assert.throws(() => ed25519PublicKeyHex(PUB_HEX), /Invalid Ed25519 private key/); }); test('signAuth reproduces the Python signature without PQ', () => { assert.equal(signAuth(PEM, NONCE_HEX, MSG, null), SIG_NOPQ); }); test('signAuth reproduces the Python signature bound to a PQ secret', () => { assert.equal(signAuth(PEM, NONCE_HEX, MSG, Buffer.from(SECRET_HEX, 'hex')), SIG_PQ); }); test('authMessage binds nonce + payload hash (+ secret) at the expected lengths', () => { assert.equal(authMessage(NONCE_HEX, MSG, null).length, 64); assert.equal(authMessage(NONCE_HEX, MSG, Buffer.from(SECRET_HEX, 'hex')).length, 96); }); test('pqTransportKey matches Python HKDF for both directions', () => { const secret = Buffer.from(SECRET_HEX, 'hex'); assert.equal(pqTransportKey(secret, 'request').toString('hex'), TKEY_REQ); assert.equal(pqTransportKey(secret, 'response').toString('hex'), TKEY_RESP); }); test('pqDecrypt opens a Python-produced ChaCha20-Poly1305 envelope', () => { const plain = pqDecrypt(Buffer.from(SECRET_HEX, 'hex'), 'response', DECRYPT_ENV); assert.equal(plain.toString('utf8'), DECRYPT_PLAIN); }); test('pqEncrypt/pqDecrypt round-trip', () => { const secret = Buffer.from(SECRET_HEX, 'hex'); const env = pqEncrypt(secret, 'request', Buffer.from('round trip ✓', 'utf8')); assert.equal(env.alg, 'ML-KEM-768+ChaCha20Poly1305'); assert.equal(pqDecrypt(secret, 'request', env).toString('utf8'), 'round trip ✓'); }); test('pqDecrypt rejects an unknown envelope', () => { assert.throws(() => pqDecrypt(Buffer.alloc(32), 'response', { alg: 'nope', nonce: '00', ciphertext: '00' }), /unsupported/); }); test('frame prepends a 4-byte little-endian length', () => { const out = frame(Buffer.from('abc')); assert.equal(out.readUInt32LE(0), 3); assert.equal(out.subarray(4).toString(), 'abc'); }); test('buildAuthPayload signs in the clear when no PQ kex is offered', async () => { const { payload, pqSecret } = await buildAuthPayload({ ...MSG }, { type: 'challenge', nonce: NONCE_HEX }, PEM); assert.equal(pqSecret, null); assert.equal((payload as any).pubkey, PUB_HEX); assert.equal((payload as any).sig, SIG_NOPQ); }); test('buildAuthPayload returns the bare message for a no-auth endpoint', async () => { const base = { ...MSG }; const { payload, pqSecret } = await buildAuthPayload(base, { type: 'challenge', nonce: NONCE_HEX }, null); assert.equal(pqSecret, null); assert.equal(payload, base); }); test('decodeResponse parses plain JSON and decrypts PQ envelopes', () => { assert.deepEqual(decodeResponse(Buffer.from('{"success":true,"data":[]}'), null), { success: true, data: [] }); const secret = Buffer.from(SECRET_HEX, 'hex'); const env = pqEncrypt(secret, 'response', Buffer.from('{"success":true,"data":1}', 'utf8')); const raw = Buffer.from(JSON.stringify({ encrypted: env })); assert.deepEqual(decodeResponse(raw, secret), { success: true, data: 1 }); });