123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609 |
- '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 ? '<none>' : '<implicit>'),
- 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]);
- }
- });
- }
- })();
|