632 lines
20 KiB
JavaScript
632 lines
20 KiB
JavaScript
'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'),
|
|
},
|
|
};
|
|
try {
|
|
cipher = createCipher(config);
|
|
} catch (ex) {
|
|
if (ex.code === 'ERR_OSSL_EVP_UNSUPPORTED'
|
|
|| /unsupported/i.test(ex.message)) {
|
|
console.log(
|
|
' ... skipping because cipher is unsupported by OpenSSL'
|
|
);
|
|
continue;
|
|
}
|
|
throw ex;
|
|
}
|
|
try {
|
|
decipher = createDecipher(config);
|
|
} catch (ex) {
|
|
if (ex.code === 'ERR_OSSL_EVP_UNSUPPORTED'
|
|
|| /unsupported/i.test(ex.message)) {
|
|
console.log(
|
|
' ... skipping because cipher is unsupported by OpenSSL'
|
|
);
|
|
continue;
|
|
}
|
|
throw ex;
|
|
}
|
|
|
|
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]);
|
|
}
|
|
});
|
|
}
|
|
})();
|