'use strict'; const assert = require('assert'); const { randomBytes } = require('crypto'); const { CIPHER_INFO, MAC_INFO, bindingAvailable, NullCipher, createCipher, NullDecipher, createDecipher, init: cryptoInit, } = require('../lib/protocol/crypto.js'); (async () => { await cryptoInit; console.log(`Crypto binding ${bindingAvailable ? '' : 'not '}available`); { const PAIRS = [ // cipher, decipher ['native', 'native'], ['binding', 'native'], ['native', 'binding'], ['binding', 'binding'], ].slice(0, bindingAvailable ? 4 : 1); [ { cipher: null }, { cipher: 'chacha20-poly1305@openssh.com' }, { cipher: 'aes128-gcm@openssh.com' }, { cipher: 'aes128-cbc', mac: 'hmac-sha1-etm@openssh.com' }, { cipher: 'aes128-ctr', mac: 'hmac-sha1' }, { cipher: 'arcfour', mac: 'hmac-sha2-256-96' }, ].forEach((testConfig) => { for (const pair of PAIRS) { function onCipherData(data) { ciphered = Buffer.concat([ciphered, data]); } function onDecipherPayload(payload) { deciphered.push(payload); } function reset() { ciphered = Buffer.alloc(0); deciphered = []; } function reinit() { if (testConfig.cipher === null) { cipher = new NullCipher(1, onCipherData); decipher = new NullDecipher(1, onDecipherPayload); } else { cipher = createCipher(config); decipher = createDecipher(config); } } let ciphered; let deciphered; let cipher; let decipher; let macSize; let packet; let payload; let cipherInfo; let config; console.log('Testing cipher: %s, mac: %s (%s encrypt, %s decrypt) ...', testConfig.cipher, testConfig.mac || (testConfig.cipher === null ? '' : ''), pair[0], pair[1]); if (testConfig.cipher === null) { cipher = new NullCipher(1, onCipherData); decipher = new NullDecipher(1, onDecipherPayload); macSize = 0; } else { cipherInfo = CIPHER_INFO[testConfig.cipher]; let macInfo; let macKey; if (testConfig.mac) { macInfo = MAC_INFO[testConfig.mac]; macKey = randomBytes(macInfo.len); macSize = macInfo.actualLen; } else if (cipherInfo.authLen) { macSize = cipherInfo.authLen; } else { throw new Error('Missing MAC for cipher'); } const key = randomBytes(cipherInfo.keyLen); const iv = (cipherInfo.ivLen ? randomBytes(cipherInfo.ivLen) : Buffer.alloc(0)); config = { outbound: { onWrite: onCipherData, cipherInfo, cipherKey: Buffer.from(key), cipherIV: Buffer.from(iv), seqno: 1, macInfo, macKey: (macKey && Buffer.from(macKey)), forceNative: (pair[0] === 'native'), }, inbound: { onPayload: onDecipherPayload, decipherInfo: cipherInfo, decipherKey: Buffer.from(key), decipherIV: Buffer.from(iv), seqno: 1, macInfo, macKey: (macKey && Buffer.from(macKey)), forceNative: (pair[1] === 'native'), }, }; cipher = createCipher(config); decipher = createDecipher(config); if (pair[0] === 'binding') assert(/binding/i.test(cipher.constructor.name)); else assert(/native/i.test(cipher.constructor.name)); if (pair[1] === 'binding') assert(/binding/i.test(decipher.constructor.name)); else assert(/native/i.test(decipher.constructor.name)); } let expectedSeqno; // Test zero-length payload ============================================ payload = Buffer.alloc(0); expectedSeqno = 2; reset(); packet = cipher.allocPacket(payload.length); payload.copy(packet, 5); cipher.encrypt(packet); assert.strictEqual(decipher.decrypt(ciphered, 0, ciphered.length), undefined); assert.strictEqual(cipher.outSeqno, expectedSeqno); assert(ciphered.length >= 9 + macSize); assert.strictEqual(decipher.inSeqno, cipher.outSeqno); assert.strictEqual(deciphered.length, 1); assert.deepStrictEqual(deciphered[0], payload); // Test single byte payload ============================================ payload = Buffer.from([ 0xEF ]); expectedSeqno = 3; reset(); packet = cipher.allocPacket(payload.length); payload.copy(packet, 5); cipher.encrypt(packet); assert.strictEqual(decipher.decrypt(ciphered, 0, ciphered.length), undefined); assert.strictEqual(cipher.outSeqno, 3); assert(ciphered.length >= 9 + macSize); assert.strictEqual(decipher.inSeqno, cipher.outSeqno); assert.strictEqual(deciphered.length, 1); assert.deepStrictEqual(deciphered[0], payload); // Test large payload ================================================== payload = randomBytes(32 * 1024); expectedSeqno = 4; reset(); packet = cipher.allocPacket(payload.length); payload.copy(packet, 5); cipher.encrypt(packet); assert.strictEqual(decipher.decrypt(ciphered, 0, ciphered.length), undefined); assert.strictEqual(cipher.outSeqno, expectedSeqno); assert(ciphered.length >= 9 + macSize); assert.strictEqual(decipher.inSeqno, cipher.outSeqno); assert.strictEqual(deciphered.length, 1); assert.deepStrictEqual(deciphered[0], payload); // Test sequnce number rollover ======================================== payload = randomBytes(4); expectedSeqno = 0; cipher.outSeqno = decipher.inSeqno = (2 ** 32) - 1; reset(); packet = cipher.allocPacket(payload.length); payload.copy(packet, 5); cipher.encrypt(packet); assert.strictEqual(decipher.decrypt(ciphered, 0, ciphered.length), undefined); assert.strictEqual(cipher.outSeqno, expectedSeqno); assert(ciphered.length >= 9 + macSize); assert.strictEqual(decipher.inSeqno, cipher.outSeqno); assert.strictEqual(deciphered.length, 1); assert.deepStrictEqual(deciphered[0], payload); // Test chunked input -- split length bytes ============================ payload = randomBytes(32 * 768); expectedSeqno = 1; reset(); packet = cipher.allocPacket(payload.length); payload.copy(packet, 5); cipher.encrypt(packet); assert.strictEqual(decipher.decrypt(ciphered, 0, 2), undefined); assert.strictEqual(decipher.decrypt(ciphered, 2, ciphered.length), undefined); assert.strictEqual(cipher.outSeqno, expectedSeqno); assert(ciphered.length >= 9 + macSize); assert.strictEqual(decipher.inSeqno, cipher.outSeqno); assert.strictEqual(deciphered.length, 1); assert.deepStrictEqual(deciphered[0], payload); // Test chunked input -- split length from payload ===================== payload = randomBytes(32 * 768); expectedSeqno = 2; reset(); packet = cipher.allocPacket(payload.length); payload.copy(packet, 5); cipher.encrypt(packet); assert.strictEqual(decipher.decrypt(ciphered, 0, 4), undefined); assert.strictEqual(decipher.decrypt(ciphered, 4, ciphered.length), undefined); assert.strictEqual(cipher.outSeqno, expectedSeqno); assert(ciphered.length >= 9 + macSize); assert.strictEqual(decipher.inSeqno, cipher.outSeqno); assert.strictEqual(deciphered.length, 1); assert.deepStrictEqual(deciphered[0], payload); // Test chunked input -- split length and payload from MAC ============= payload = randomBytes(32 * 768); expectedSeqno = 3; reset(); packet = cipher.allocPacket(payload.length); payload.copy(packet, 5); cipher.encrypt(packet); assert.strictEqual( decipher.decrypt(ciphered, 0, ciphered.length - macSize), undefined ); assert.strictEqual( decipher.decrypt(ciphered, ciphered.length - macSize, ciphered.length), undefined ); assert.strictEqual(cipher.outSeqno, expectedSeqno); assert(ciphered.length >= 9 + macSize); assert.strictEqual(decipher.inSeqno, cipher.outSeqno); assert.strictEqual(deciphered.length, 1); assert.deepStrictEqual(deciphered[0], payload); // Test packet length checks =========================================== [0, 2 ** 32 - 1].forEach((n) => { reset(); packet = cipher.allocPacket(0); packet.writeUInt32BE(n, 0); // Overwrite packet length field cipher.encrypt(packet); let threw = false; try { decipher.decrypt(ciphered, 0, ciphered.length); } catch (ex) { threw = true; assert(ex instanceof Error); assert(/packet length/i.test(ex.message)); } if (!threw) throw new Error('Expected error'); // Recreate deciphers since errors leave them in an unusable state. // We recreate the ciphers as well so that internal states of both // ends match again. reinit(); }); // Test minimum padding length check =================================== if (testConfig.cipher !== null) { let payloadLen; const blockLen = cipherInfo.blockLen; if (/chacha|gcm/i.test(testConfig.cipher) || /etm/i.test(testConfig.mac)) { payloadLen = blockLen - 2; } else { payloadLen = blockLen - 6; } const minLen = 4 + 1 + payloadLen + (blockLen + 1); // We don't do strict equality checks here since the length of the // returned Buffer can vary due to implementation details. assert(cipher.allocPacket(payloadLen).length >= minLen); } // ===================================================================== cipher.free(); decipher.free(); if (testConfig.cipher === null) break; } }); } // Test createCipher()/createDecipher() exceptions { [ [ [true, null], /invalid config/i ], [ [{}], [/invalid outbound/i, /invalid inbound/i] ], [ [{ outbound: {}, inbound: {} }], [/invalid outbound\.onWrite/i, /invalid inbound\.onPayload/i] ], [ [ { outbound: { onWrite: () => {}, cipherInfo: true }, inbound: { onPayload: () => {}, decipherInfo: true }, }, { outbound: { onWrite: () => {}, cipherInfo: null }, inbound: { onPayload: () => {}, decipherInfo: null }, }, ], [/invalid outbound\.cipherInfo/i, /invalid inbound\.decipherInfo/i] ], [ [ { outbound: { onWrite: () => {}, cipherInfo: {}, cipherKey: {}, }, inbound: { onPayload: () => {}, decipherInfo: {}, decipherKey: {}, }, }, { outbound: { onWrite: () => {}, cipherInfo: { keyLen: 32 }, cipherKey: Buffer.alloc(8), }, inbound: { onPayload: () => {}, decipherInfo: { keyLen: 32 }, decipherKey: Buffer.alloc(8), }, }, ], [/invalid outbound\.cipherKey/i, /invalid inbound\.decipherKey/i] ], [ [ { outbound: { onWrite: () => {}, cipherInfo: { keyLen: 1, ivLen: 12 }, cipherKey: Buffer.alloc(1), cipherIV: true }, inbound: { onPayload: () => {}, decipherInfo: { keyLen: 1, ivLen: 12 }, decipherKey: Buffer.alloc(1), cipherIV: true }, }, { outbound: { onWrite: () => {}, cipherInfo: { keyLen: 1, ivLen: 12 }, cipherKey: Buffer.alloc(1), cipherIV: null }, inbound: { onPayload: () => {}, decipherInfo: { keyLen: 1, ivLen: 12 }, decipherKey: Buffer.alloc(1), cipherIV: null }, }, { outbound: { onWrite: () => {}, cipherInfo: { keyLen: 1, ivLen: 12 }, cipherKey: Buffer.alloc(1), cipherIV: {} }, inbound: { onPayload: () => {}, decipherInfo: { keyLen: 1, ivLen: 12 }, decipherKey: Buffer.alloc(1), cipherIV: {} }, }, { outbound: { onWrite: () => {}, cipherInfo: { keyLen: 1, ivLen: 12 }, cipherKey: Buffer.alloc(1), cipherIV: Buffer.alloc(1) }, inbound: { onPayload: () => {}, decipherInfo: { keyLen: 1, ivLen: 12 }, decipherKey: Buffer.alloc(1), cipherIV: Buffer.alloc(1) }, }, ], [/invalid outbound\.cipherIV/i, /invalid inbound\.decipherIV/i] ], [ [ { outbound: { onWrite: () => {}, cipherInfo: { keyLen: 1, ivLen: 0 }, cipherKey: Buffer.alloc(1), seqno: true }, inbound: { onPayload: () => {}, decipherInfo: { keyLen: 1, ivLen: 0 }, decipherKey: Buffer.alloc(1), seqno: true }, }, { outbound: { onWrite: () => {}, cipherInfo: { keyLen: 1, ivLen: 0 }, cipherKey: Buffer.alloc(1), seqno: -1 }, inbound: { onPayload: () => {}, decipherInfo: { keyLen: 1, ivLen: 0 }, decipherKey: Buffer.alloc(1), seqno: -1 }, }, { outbound: { onWrite: () => {}, cipherInfo: { keyLen: 1, ivLen: 0 }, cipherKey: Buffer.alloc(1), seqno: 2 ** 32 }, inbound: { onPayload: () => {}, decipherInfo: { keyLen: 1, ivLen: 0 }, decipherKey: Buffer.alloc(1), seqno: 2 ** 32 }, }, ], [/invalid outbound\.seqno/i, /invalid inbound\.seqno/i] ], [ [ { outbound: { onWrite: () => {}, cipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' }, cipherKey: Buffer.alloc(1), seqno: 0 }, inbound: { onPayload: () => {}, decipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' }, decipherKey: Buffer.alloc(1), seqno: 0 }, }, { outbound: { onWrite: () => {}, cipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' }, cipherKey: Buffer.alloc(1), seqno: 0, macInfo: true }, inbound: { onPayload: () => {}, decipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' }, decipherKey: Buffer.alloc(1), seqno: 0, macInfo: true }, }, { outbound: { onWrite: () => {}, cipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' }, cipherKey: Buffer.alloc(1), seqno: 0, macInfo: null }, inbound: { onPayload: () => {}, decipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' }, decipherKey: Buffer.alloc(1), seqno: 0, macInfo: null }, }, ], [/invalid outbound\.macInfo/i, /invalid inbound\.macInfo/i] ], [ [ { outbound: { onWrite: () => {}, cipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' }, cipherKey: Buffer.alloc(1), seqno: 0, macInfo: { keyLen: 16 } }, inbound: { onPayload: () => {}, decipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' }, decipherKey: Buffer.alloc(1), seqno: 0, macInfo: { keyLen: 16 } }, }, { outbound: { onWrite: () => {}, cipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' }, cipherKey: Buffer.alloc(1), seqno: 0, macInfo: { keyLen: 16 }, macKey: true }, inbound: { onPayload: () => {}, decipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' }, decipherKey: Buffer.alloc(1), seqno: 0, macInfo: { keyLen: 16 }, macKey: true }, }, { outbound: { onWrite: () => {}, cipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' }, cipherKey: Buffer.alloc(1), seqno: 0, macInfo: { keyLen: 16 }, macKey: null }, inbound: { onPayload: () => {}, decipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' }, decipherKey: Buffer.alloc(1), seqno: 0, macInfo: { keyLen: 16 }, macKey: null }, }, { outbound: { onWrite: () => {}, cipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' }, cipherKey: Buffer.alloc(1), seqno: 0, macInfo: { keyLen: 16 }, macKey: Buffer.alloc(1) }, inbound: { onPayload: () => {}, decipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' }, decipherKey: Buffer.alloc(1), seqno: 0, macInfo: { keyLen: 16 }, macKey: Buffer.alloc(1) }, }, ], [/invalid outbound\.macKey/i, /invalid inbound\.macKey/i] ], ].forEach((testCase) => { let errorChecks = testCase[1]; if (!Array.isArray(errorChecks)) errorChecks = [errorChecks[0], errorChecks[0]]; for (const input of testCase[0]) { assert.throws(() => createCipher(input), errorChecks[0]); assert.throws(() => createDecipher(input), errorChecks[1]); } }); } })();