From 768a458ace6c7f953daad0ddc0fd9aa5171f5db3 Mon Sep 17 00:00:00 2001 From: Zhaofeng Date: Tue, 13 Oct 2026 08:46:29 +0800 Subject: [PATCH] Fix native ChaCha nonce corruption during nested writes --- lib/protocol/crypto.js | 2 + test/test-protocol-chacha-reentrancy.js | 77 +++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 test/test-protocol-chacha-reentrancy.js diff --git a/lib/protocol/crypto.js b/lib/protocol/crypto.js index 851cc738..c65a5f46 100644 --- a/lib/protocol/crypto.js +++ b/lib/protocol/crypto.js @@ -194,6 +194,8 @@ class ChaChaPolyCipherNative { if (this._dead) return; + // Isolate nonce from synchronous nested forwardOut encryption. + const POLY1305_OUT_COMPUTE = Buffer.alloc(16); // Generate Poly1305 key POLY1305_OUT_COMPUTE[0] = 0; // Set counter to 0 (little endian) writeUInt32BE(POLY1305_OUT_COMPUTE, this.outSeqno, 12); diff --git a/test/test-protocol-chacha-reentrancy.js b/test/test-protocol-chacha-reentrancy.js new file mode 100644 index 00000000..b2ff52ba --- /dev/null +++ b/test/test-protocol-chacha-reentrancy.js @@ -0,0 +1,77 @@ +'use strict'; + +const assert = require('assert'); +const { + CIPHER_INFO, + createCipher, + createDecipher, + init: cryptoInit, +} = require('../lib/protocol/crypto.js'); + +(async () => { + await cryptoInit; + const cipherInfo = CIPHER_INFO['chacha20-poly1305@openssh.com']; + const key = Buffer.alloc(64, 0x42); + const payload = Buffer.concat([ + Buffer.from([5, 0, 0, 0, 12]), + Buffer.from('ssh-userauth'), + ]); + + for (const nested of [false, true]) { + for (const seqno of [0, 3, 0xFFFFFFFF]) { + const chunks = []; + const received = []; + const outer = createCipher({ + outbound: { + cipherInfo, + cipherKey: Buffer.alloc(64, 0x23), + seqno: 9, + forceNative: true, + onWrite() {}, + }, + }); + const inner = createCipher({ + outbound: { + cipherInfo, + cipherKey: key, + seqno, + forceNative: true, + onWrite(data) { + chunks.push(Buffer.from(data)); + if (nested) { + const packet = outer.allocPacket(1); + packet[5] = 2; + outer.encrypt(packet); + } + }, + }, + }); + const decipher = createDecipher({ + inbound: { + decipherInfo: cipherInfo, + decipherKey: key, + seqno, + forceNative: true, + onPayload(data) { + received.push(Buffer.from(data)); + }, + }, + }); + + const packet = inner.allocPacket(payload.length); + payload.copy(packet, 5); + inner.encrypt(packet); + const wire = Buffer.concat(chunks); + decipher.decrypt(wire, 0, wire.length); + assert.deepStrictEqual(received, [payload], + `nested=${nested}, seqno=${seqno}`); + inner.free(); + outer.free(); + decipher.free(); + } + } + console.log('Native ChaCha nested-write regression passed (6 cases)'); +})().catch((err) => { + console.error(err); + process.exitCode = 1; +});