UNPKG

@ronomon/crypto-async

Version:

Fast, reliable cipher, hash and hmac methods executed in Node's threadpool for multi-core throughput.

500 lines (478 loc) 15.8 kB
var assert = require('assert'); var binding = require('.'); var common = require('./common.js'); var Queue = require('@ronomon/queue'); function Bad(a, b, offset, size) { // Corrupt two buffers for testing AEAD authentication: if (offset === undefined) offset = 0; if (size === undefined) size = a.length; if (size === 0) return 0; var corruptOffset = offset + Math.floor(common.random() * size); a[corruptOffset] = (a[corruptOffset] + 1) & 255; b[corruptOffset] = (b[corruptOffset] + 1) & 255; a.corrupt = true; b.corrupt = true; return 1; } function Compare(test, a, b, key) { try { if (a.length !== b.length) { throw new Error( key + ': a.length !== b.length (' + a.length + ' !== ' + b.length + ')' ); } for (var index = 0, length = a.length; index < length; index++) { var x = a[index]; var y = b[index]; if ( typeof x === 'string' || typeof x === 'number' || x === undefined || typeof y === 'string' || typeof y === 'number' || y === undefined ) { if (x !== y) { throw new Error( key + '[' + index + ']: ' + JSON.stringify(x) + ' !== ' + JSON.stringify(y) ); } } else if (Buffer.isBuffer(x)) { if (!Buffer.isBuffer(y)) { console.log('a: ' + x.toString('hex')); console.log('b: ' + JSON.stringify(y)); throw new Error(key + '[' + index + ']: ' + 'buffer vs non-buffer'); } if (!x.equals(y)) { if ( test.method === 'cipher' && key === 'arguments' && index === 11 && x.different === true && y.different === true ) { continue; } throw new Error(key + '[' + index + ']: ' + 'buffers are different'); } } else if (x instanceof Error) { if (!(y instanceof Error)) { console.log('a:', x); console.log('b:', y); throw new Error(key + '[' + index + ']: ' + 'error vs non-error'); } if (x.toString() !== y.toString()) { console.log('a:', x); console.log('b:', y); throw new Error(key + '[' + index + ']: ' + 'errors are different'); } } else { console.log(x); console.log(y); throw new Error(key + '[' + index + ']: ' + 'unsupported type'); } } } catch (exception) { console.log(''); console.log('Failed test ' + test.number.toString().padStart(6, '0') + ':'); console.log(''); throw exception; } } function Copy(parameters) { return parameters.map( function(parameter) { if (typeof parameter === 'number') return parameter; if (typeof parameter === 'string') return parameter; if (Buffer.isBuffer(parameter)) return Buffer.from(parameter); console.log(parameter); throw new Error('unsupported parameter type'); } ); } function Execute(test, parameters, engine, end) { if (test.sync) { try { var result = engine[test.method].apply(engine, parameters); } catch (exception) { return end(exception); } end(undefined, result); } else { engine[test.method].apply(engine, parameters.concat(end)); } } function Inspect(test, parameters) { var tuples; common[test.method].signatures.some( function(signature) { if (signature.length !== parameters.length) return false; tuples = []; signature.forEach( function(key, index) { var value = parameters[index]; if (Buffer.isBuffer(value)) { var corrupt = ''; if (test.method === 'cipher' && value.corrupt) { corrupt = ' [corrupt]'; } tuples.push(key + '=' + value.length + corrupt); } else { tuples.push(key + '=' + value); } } ); return true; } ); if (!tuples) throw new Error('unsupported parameters signature'); var sync = test.sync ? ' sync' : ''; tuples.unshift(test.method + '()' + sync); tuples.unshift(test.number.toString().padStart(6, '0')); console.log(tuples[0] + ' ' + tuples.slice(1).join('\r\n ') + '\r\n'); } function Probe(method, signature, algorithm) { // First, generate valid signature arguments, which we can then corrupt: const args = []; // We use maxInt, maxOffset and maxSize to test range checks: const maxInt = 2147483647; // We must provide exact-sized keys, IVs and tags for simple signatures: // We don't do range checks for simple signatures. const maxOffset = signature.indexOf('target') === -1 ? 0 : 1; const maxSize = 256; signature.forEach( function(key, index) { switch (key) { case 'algorithm': return args.push(algorithm.name); case 'encrypt': return args.push(0); case 'key': return args.push(Buffer.alloc(maxOffset + (algorithm.keySize || 32))); case 'source': case 'aad': return args.push(Buffer.alloc(maxOffset + maxSize)); case 'target': if (method === 'cipher') { return args.push(Buffer.alloc(maxOffset + maxSize + 32)); } else { return args.push(Buffer.alloc(maxOffset + maxSize)); } case 'iv': return args.push(Buffer.alloc(maxOffset + algorithm.ivSize)); case 'tag': // The extra byte is used to test E_TAG_INVALID for non-AEAD ciphers: return args.push(Buffer.alloc(maxOffset + algorithm.tagSize + 1)); case 'keyOffset': case 'ivOffset': case 'sourceOffset': case 'targetOffset': case 'aadOffset': case 'tagOffset': return args.push(maxOffset); case 'keySize': case 'ivSize': case 'sourceSize': var buffer = args[index - 2]; assert(Buffer.isBuffer(buffer)); return args.push(buffer.length - maxOffset); case 'aadSize': return args.push(algorithm.tagSize ? maxSize : 0); case 'tagSize': return args.push(algorithm.tagSize); } throw new Error('unsupported signature key: ' + key); } ); // Next, corrupt these valid arguments: const map = []; const nonInt = [ -Infinity, Infinity, NaN, -1, 0.0000001, 1.0000001, '', {}, [], null, true, false, undefined, Buffer.alloc(0), -Math.pow(2, 53), Math.pow(2, 53) ]; // Wrong number of arguments: // Cipher method signatures may overlap. map.push([[], 'E_ARGUMENTS']); if (method !== 'cipher' || args.length === 5) { map.push([args.slice(1), 'E_ARGUMENTS']); } if (method !== 'cipher' || args.length === 7) { map.push([[...args, function() {}, null], 'E_ARGUMENTS']); } // AEAD ciphers must always provide a tag: // If the signature is not an AEAD signature then index.js should pass NO_TAG. // The binding must then throw E_TAG_INVALID. if ( method === 'cipher' && algorithm.tagSize > 0 && signature.indexOf('tag') === -1 ) { assert(signature.indexOf('aad') === -1); assert(signature.length === 5 || signature.length === 13); map.push([args, 'E_TAG_INVALID']); map.push([args, 'E_TAG_INVALID']); } // Bad callbacks: map.push([[...args, 0], 'E_CALLBACK']); map.push([[...args, 1], 'E_CALLBACK']); map.push([[...args, {}], 'E_CALLBACK']); map.push([[...args, null], 'E_CALLBACK']); map.push([[...args, undefined], 'E_CALLBACK']); signature.forEach( function(key, index) { function check(code, values) { values.forEach( function(value) { var copy = args.slice(0); copy[index] = value; map.push([copy, code]); } ); } switch (key) { case 'algorithm': check('E_ALGORITHM', [ 0, {}, null, true, false, undefined, Buffer.alloc(0) ]); if (method === 'cipher') { // These are a subset of disabled ciphers: check('E_ALGORITHM_DISABLED', [ 'aes-128-cbc', 'aes-192-cbc', 'aes-256-cbc', 'aes-128-ccm', 'aes-192-ccm', 'aes-256-ccm', 'aes-128-ecb', 'aes-192-ecb', 'aes-256-ecb', 'aes-128-xts', 'aes-256-xts', 'rc4' ]); } return check('E_ALGORITHM_UNKNOWN', ['', 'unknown']); case 'encrypt': return check('E_ENCRYPT', nonInt); case 'key': case 'iv': case 'source': case 'target': case 'aad': case 'tag': return check('E_' + key.toUpperCase(), [ 0, 1, '', {}, [], null, true, false, undefined ]); case 'keyOffset': case 'keySize': case 'ivOffset': case 'ivSize': case 'sourceOffset': case 'sourceSize': case 'targetOffset': case 'aadOffset': case 'aadSize': case 'tagOffset': case 'tagSize': assert(maxOffset > 0); check('E_' + key.replace(/([A-Z])/, '_$1').toUpperCase(), nonInt); check('E_' + key.replace(/([A-Z].+)/, '_Range').toUpperCase(), [ // Exercise overflow: // (size > INT_MAX - offset) // (offset > INT_MAX - size) maxInt - maxOffset + 1, // Exercise range: // (offset + size > length) maxOffset + maxSize + 1 ]); if (method === 'cipher') { if (key === 'keySize') { check('E_KEY_INVALID', [0, algorithm.keySize - 1]); } if (key === 'ivSize') { check('E_IV_INVALID', [0, algorithm.ivSize - 1]); } if (key === 'aadSize') { if (algorithm.tagSize === 0) { check('E_AAD_INVALID', [2]); } } if (key === 'tagSize') { if (algorithm.tagSize) { check('E_TAG_INVALID', [0]); } else { check('E_TAG_INVALID', [1]); } } } } } ); // Finally, execute these arguments synchronously and assert exceptions: map.forEach( function(tuple) { var params = tuple[0]; var code = tuple[1]; assert(binding.hasOwnProperty(code)); assert(typeof binding[code] === 'string'); assert(binding[code]); try { binding[method].apply(binding, params); } catch (exception) { if (exception.message === binding[code]) { console.log( 'PASS: Exception:', method + '()', 'args=' + params.length, code + '=' + JSON.stringify(exception.message) ); return; } console.log(method, params.length, params); throw new Error('unexpected exception: ' + exception.message); } console.log(method, params.length, params); throw new Error('expected ' + code); } ); } assert(common.CIPHER_BLOCK_MAX === binding.CIPHER_BLOCK_MAX); // Test exceptions: ['cipher', 'hash', 'hmac'].forEach( function(method) { var signatures = common[method].signatures; var algorithms = common[method].algorithm; // We use for loops to avoid polluting any thrown stack trace: for (var si = 0; si < signatures.length; si++) { for (var ai = 0; ai < algorithms.length; ai++) { Probe(method, signatures[si], algorithms[ai]); } } } ); console.log('\r\n SEED=' + common.seed + '\r\n'); // Test execution: var queue = new Queue(8); queue.onData = function(test, end) { var a = common[test.method].parameters(); var b = Copy(a); var bad = 0; function run() { Inspect(test, a); Execute(test, a, binding, function(...x) { Execute(test, b, common.independent, function(...y) { Compare(test, x, y, 'result'); Compare(test, a, b, 'arguments'); if (test.method != 'cipher') return end(); // Test cipher roundtrip by decrypting ciphertext: if (a[1] === 0) { if (bad) { // We must assert that an error is raised. We do not rely on the // independent implementation (by using Compare) since it also // uses OpenSSL. // This is how we found: // https://github.com/openssl/openssl/issues/8345 assert(y[0] instanceof Error); assert(x[0] instanceof Error); assert(x[0].message === y[0].message); assert(x[0].message === binding.E_CORRUPT); } return end(); } a[1] = 0; b[1] = 0; if (a.length <= 8) { assert(Buffer.isBuffer(x[1])); // Set source = target: a[4] = x[1]; b[4] = y[1]; } else { assert(Number.isInteger(x[1])); // Set source = target: a[8] = a[11]; b[8] = b[11]; // Set sourceOffset = targetOffset: a[9] = a[12]; b[9] = b[12]; // Set sourceSize = targetSize: a[10] = x[1]; b[10] = y[1]; // Set target = (targetOffset + targetSize): a[11] = Buffer.alloc(a[12] + x[1] + common.CIPHER_BLOCK_MAX, 255); b[11] = Buffer.alloc(b[12] + y[1] + common.CIPHER_BLOCK_MAX, 255); } // Test that AEAD cipher detects corruption: if ((a.length === 7 || a.length === 8) && a[6].length > 0) { if (common.random() < 0.1) bad |= Bad(a[2], b[2]); // key if (common.random() < 0.1) bad |= Bad(a[3], b[3]); // iv if (common.random() < 0.1) bad |= Bad(a[4], b[4]); // source if (common.random() < 0.1) bad |= Bad(a[5], b[5]); // aad if (common.random() < 0.1) bad |= Bad(a[6], b[6]); // tag } else if ((a.length === 19 || a.length === 20) && a[18] > 0) { if (common.random() < 0.1) bad |= Bad(a[2], b[2], b[3], b[4]); if (common.random() < 0.1) bad |= Bad(a[5], b[5], b[6], b[7]); if (common.random() < 0.1) bad |= Bad(a[8], b[8], b[9], b[10]); if (common.random() < 0.1) bad |= Bad(a[13], b[13], b[14], b[15]); if (common.random() < 0.1) bad |= Bad(a[16], b[16], b[17], b[18]); if (bad) { // Mark the target buffers as different for Compare(): // For AEAD cipher zero-copy interface, target buffers will be // different if anything else is corrupt. This is because the // independent implementation throws once final() is called, // before copying to the target. This is not the case for AEAD // cipher non-zero-copy interface, as both the independent // implementation and index.js throw before returning target. a[11].different = true; b[11].different = true; } } run(); } ); } ); } run(); }; queue.onEnd = function(error) { if (error) throw error; console.log(new Array(16 + 1).join('=')); console.log('PASSED ALL TESTS'); console.log(new Array(16 + 1).join('=')); }; var tests = []; var methods = ['cipher', 'hash', 'hmac']; for (var index = 0; index < 8000; index++) { tests.push({ number: index + 1, method: methods[Math.floor(common.random() * methods.length)], sync: common.random() < 0.5 }); } queue.concat(tests); queue.end();