UNPKG

@ronomon/crypto-async

Version:

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

584 lines (554 loc) 16.5 kB
const Node = { crypto: require('crypto'), process: process }; const BUFFER_EMPTY = Buffer.alloc(0); const CIPHER_BLOCK_MAX = 32; const assert = require('assert'); const seed = Math.floor(Math.random() * Math.pow(2, 32)); const random = (function() { var key = Buffer.alloc(32, 0); key.writeUInt32LE(seed, 0); var iv = Buffer.alloc(16); var cipher = Node.crypto.createCipheriv('aes-256-ctr', key, iv); var buffer; var offset; var denominator = Math.pow(2, 32); return function() { if (!buffer || offset + 4 > buffer.length) { buffer = cipher.update(Buffer.alloc(65536)); offset = 0; } var numerator = buffer.readUInt32LE(offset); offset += 4; return numerator / denominator; }; })(); const randomBuffer = (function() { var key = Buffer.alloc(32, 255); key.writeUInt32LE(seed, 0); var iv = Buffer.alloc(16); var cipher = Node.crypto.createCipheriv('aes-256-ctr', key, iv); var buffer = Buffer.alloc(1024 * 1024); return function(size) { if (size <= buffer.length) { return cipher.update(buffer.slice(0, size)); } else { return cipher.update(Buffer.alloc(size)); } }; })(); function randomElement(array) { return array[Math.floor(random() * array.length)]; } function randomSize() { if (random() < 0.05) return Math.floor(random() * 64); if (random() < 0.05) return 0; if (random() < 0.1) return Math.floor(random() * 524288); return Math.floor(random() * 1024); } const Cipher = { algorithm: [ { name: 'aes-128-ctr', keySize: 16, ivSize: 16, tagSize: 0 }, { name: 'aes-192-ctr', keySize: 24, ivSize: 16, tagSize: 0 }, { name: 'aes-256-ctr', keySize: 32, ivSize: 16, tagSize: 0 }, { name: 'aes-128-gcm', keySize: 16, ivSize: 12, tagSize: 16 }, { name: 'aes-256-gcm', keySize: 32, ivSize: 12, tagSize: 16 } ], parameters: function() { var self = this; var algorithm = randomElement(self.algorithm); var aead = algorithm.tagSize > 0; var encrypt = 1; var keySize = algorithm.keySize; var ivSize = algorithm.ivSize; var sourceSize = randomSize(); var targetSize = sourceSize + CIPHER_BLOCK_MAX; var aadSize = 0; var tagSize = algorithm.tagSize; if (aead) { aadSize = randomSize(); tagSize = tagSize - 4 + Math.round(random() * 4); // Leave enough entropy in tag (at least 96 bits) to detect corruption: assert(tagSize >= 12); } if (random() < 0.5) { var key = randomBuffer(keySize); var iv = randomBuffer(ivSize); var source = randomBuffer(sourceSize); if (aead || random() < 0.5) { var aad = randomBuffer(aadSize); var tag = randomBuffer(tagSize); return [ algorithm.name, encrypt, key, iv, source, aad, tag ]; } else { return [ algorithm.name, encrypt, key, iv, source ]; } } else { var keyOffset = randomSize(); var ivOffset = randomSize(); var sourceOffset = randomSize(); var targetOffset = randomSize(); var key = randomBuffer(keyOffset + keySize + randomSize()); var iv = randomBuffer(ivOffset + ivSize + randomSize()); var source = randomBuffer(sourceOffset + sourceSize + randomSize()); var target = randomBuffer(targetOffset + targetSize + randomSize()); if (aead || random() < 0.5) { var aadOffset = randomSize(); var tagOffset = randomSize(); var aad = randomBuffer(aadOffset + aadSize + randomSize()); var tag = randomBuffer(tagOffset + tagSize + randomSize()); return [ algorithm.name, encrypt, key, keyOffset, keySize, iv, ivOffset, ivSize, source, sourceOffset, sourceSize, target, targetOffset, aad, aadOffset, aadSize, tag, tagOffset, tagSize ]; } else { return [ algorithm.name, encrypt, key, keyOffset, keySize, iv, ivOffset, ivSize, source, sourceOffset, sourceSize, target, targetOffset ]; } } }, signatures: [ [ 'algorithm', 'encrypt', 'key', 'iv', 'source' ], [ 'algorithm', 'encrypt', 'key', 'iv', 'source', 'aad', 'tag' ], [ 'algorithm', 'encrypt', 'key', 'keyOffset', 'keySize', 'iv', 'ivOffset', 'ivSize', 'source', 'sourceOffset', 'sourceSize', 'target', 'targetOffset' ], [ 'algorithm', 'encrypt', 'key', 'keyOffset', 'keySize', 'iv', 'ivOffset', 'ivSize', 'source', 'sourceOffset', 'sourceSize', 'target', 'targetOffset', 'aad', 'aadOffset', 'aadSize', 'tag', 'tagOffset', 'tagSize' ] ] }; (function() { // We support ChaCha20-Poly1305 as of Node 10, except for testing. // Node 10 introduces OpenSSL 1.1, which we need for implementation support. // Later versions add these to the crypto module, which we need for testing. if (/^v\d+\.\d+\./.test(Node.process.version)) { var parts = Node.process.version.replace(/^v/, '').split('.'); var major = parseInt(parts[0], 10); var minor = parseInt(parts[1], 10); // Disable OCB (patented): // if (major > 10 || (major == 10 && minor >= 10)) { // // OCB is supported in Node from v10.10.0: // // https://github.com/nodejs/node/pull/22716 // Cipher.algorithm.push( // { name: 'aes-128-ocb', keySize: 16, ivSize: 12, tagSize: 16 } // ); // Cipher.algorithm.push( // { name: 'aes-256-ocb', keySize: 32, ivSize: 12, tagSize: 16 } // ); // } if (major > 11 || (major == 11 && minor >= 2)) { // ChaCha20-Poly1305 is supported in Node from v11.2.0: // https://github.com/nodejs/node/commit/5c596222433166a7c0274251cca1e55f3 Cipher.algorithm.push( { name: 'chacha20-poly1305', keySize: 32, ivSize: 12, tagSize: 16 } ); Cipher.algorithm.push( { name: 'chacha20', keySize: 32, ivSize: 16, tagSize: 0 } ); } } })(); const Hash = { algorithm: [ { name: 'md5', targetSize: 16 }, { name: 'sha1', targetSize: 20 }, { name: 'sha256', targetSize: 32 }, { name: 'sha512', targetSize: 64 }, { name: 'blake2s256', targetSize: 32 }, { name: 'blake2b512', targetSize: 64 } ], parameters: function() { var self = this; var algorithm = randomElement(self.algorithm); var sourceSize = randomSize(); var targetSize = algorithm.targetSize; if (random() < 0.5) { var source = randomBuffer(sourceSize); return [ algorithm.name, source ]; } else { var sourceOffset = randomSize(); var targetOffset = randomSize(); var source = randomBuffer(sourceOffset + sourceSize + randomSize()); var target = randomBuffer(targetOffset + targetSize + randomSize()); return [ algorithm.name, source, sourceOffset, sourceSize, target, targetOffset ]; } }, signatures: [ [ 'algorithm', 'source' ], [ 'algorithm', 'source', 'sourceOffset', 'sourceSize', 'target', 'targetOffset' ] ] }; const HMAC = { algorithm: [ { name: 'md5', targetSize: 16 }, { name: 'sha1', targetSize: 20 }, { name: 'sha256', targetSize: 32 }, { name: 'sha512', targetSize: 64 }, { name: 'blake2s256', targetSize: 32 }, { name: 'blake2b512', targetSize: 64 } ], parameters: function() { var self = this; var algorithm = randomElement(self.algorithm); var keySize = randomSize(); var sourceSize = randomSize(); var targetSize = algorithm.targetSize; if (random() < 0.5) { var key = randomBuffer(keySize); var source = randomBuffer(sourceSize); return [ algorithm.name, key, source ]; } else { var keyOffset = randomSize(); var sourceOffset = randomSize(); var targetOffset = randomSize(); var key = randomBuffer(keyOffset + keySize + randomSize()); var source = randomBuffer(sourceOffset + sourceSize + randomSize()); var target = randomBuffer(targetOffset + targetSize + randomSize()); return [ algorithm.name, key, keyOffset, keySize, source, sourceOffset, sourceSize, target, targetOffset ]; } }, signatures: [ [ 'algorithm', 'key', 'source' ], [ 'algorithm', 'key', 'keyOffset', 'keySize', 'source', 'sourceOffset', 'sourceSize', 'target', 'targetOffset' ] ] }; const Independent = {}; Independent.cipher = function(...args) { var self = this; if (args.length >= 19) return self.cipherExecute(...args); if (args.length >= 5 && args.length <= 8) { var target = Buffer.alloc(args[4].length + CIPHER_BLOCK_MAX); var aad = args.length >= 7 ? args[5] : BUFFER_EMPTY; var tag = args.length >= 7 ? args[6] : BUFFER_EMPTY; var params = [ args[0], // algorithm args[1], // encrypt args[2], // key 0, // keyOffset args[2].length, // keySize args[3], // iv 0, // ivOffset args[3].length, // ivSize args[4], // source 0, // sourceOffset args[4].length, // sourceSize target, // target 0, // targetOffset aad, // aad 0, // aadOffset aad.length, // aadSize tag, // tag 0, // tagOffset tag.length // tagSize ]; if (args.length === 5 || args.length === 7) { var targetSize = self.cipherExecute(...params); return target.slice(0, targetSize); } else { self.cipherExecute(...params, function(error, targetSize) { var end = args[args.length - 1]; if (error) return end(error); end(undefined, target.slice(0, targetSize)); } ); } } else if (args.length === 13) { var params = [...args, BUFFER_EMPTY, 0, 0, BUFFER_EMPTY, 0, 0]; return self.cipherExecute(...params); } else if (args.length === 14) { var end = args.pop(); // Remove callback. var params = [...args, BUFFER_EMPTY, 0, 0, BUFFER_EMPTY, 0, 0]; return self.cipherExecute(...params, function(error, targetSize) { if (error) return end(error); end(undefined, targetSize); } ); } else { throw new Error('unreachable'); } }; Independent.cipherExecute = function(...args) { if (args.length !== 19 && args.length !== 20) throw new Error('unreachable'); var algorithm = args[0]; var encrypt = args[1]; var key = args[2]; var keyOffset = args[3]; var keySize = args[4]; var iv = args[5]; var ivOffset = args[6]; var ivSize = args[7]; var source = args[8]; var sourceOffset = args[9]; var sourceSize = args[10]; var target = args[11]; var targetOffset = args[12]; var aad = args[13]; var aadOffset = args[14]; var aadSize = args[15]; var tag = args[16]; var tagOffset = args[17]; var tagSize = args[18]; // Slice only if necessary to avoid impacting benchmarks: if (key.length !== keySize) key = key.slice(keyOffset, keyOffset + keySize); if (iv.length !== ivSize) iv = iv.slice(ivOffset, ivOffset + ivSize); if (source.length !== sourceSize) { source = source.slice(sourceOffset, sourceOffset + sourceSize); } if (aad.length !== aadSize) aad = aad.slice(aadOffset, aadOffset + aadSize); if (tag.length !== tagSize) tag = tag.slice(tagOffset, tagOffset + tagSize); var options = {}; if (tagSize) options.authTagLength = tagSize; var method = encrypt === 1 ? 'createCipheriv' : 'createDecipheriv'; var cipher = Node.crypto[method](algorithm, key, iv, options); if (tagSize && !encrypt) { // "The decipher.setAuthTag() method must be called before decipher.final() // and can only be called once." if (tag.length !== tagSize) throw new Error('assumed tag is a slice'); cipher.setAuthTag(tag); } if (tagSize) { // We call setAAD() if cipher is an AEAD cipher (inferred from tagSize), // without regard to aadSize, because we want to test that empty aad buffers // (aadSize === 0) are handled the same as by Node. // "The cipher.setAAD() method must be called before cipher.update()." // "The decipher.setAAD() method must be called before decipher.update()." cipher.setAAD(aad); } var targetSize = 0; targetSize += cipher.update(source).copy(target, targetOffset); try { targetSize += cipher.final().copy(target, targetOffset + targetSize); } catch (error) { if (error.message === 'Unsupported state or unable to authenticate data') { error.message = 'corrupt'; } if (args.length === 19) throw error; return args[19](error); } if (tagSize && encrypt) { // "The cipher.getAuthTag() method should only be called after encryption // has been completed using the cipher.final() method." if (tag.length !== tagSize) throw new Error('assumed tag is a slice'); cipher.getAuthTag().copy(tag, 0); } if (args.length === 19) return targetSize; args[19](undefined, targetSize); }; Independent.hash = function(...args) { if (args.length === 2 || args.length === 3) { var algorithm = args[0]; var source = args[1]; var sourceOffset = 0; var sourceSize = source.length; var target = Buffer.alloc(64); var targetOffset = 0; } else if (args.length === 6 || args.length === 7) { var algorithm = args[0]; var source = args[1]; var sourceOffset = args[2]; var sourceSize = args[3]; var target = args[4]; var targetOffset = args[5]; } else { throw new Error('unreachable'); } var hash = Node.crypto.createHash(algorithm); hash.update(source.slice(sourceOffset, sourceOffset + sourceSize)); var targetSize = hash.digest().copy(target, targetOffset); if (arguments.length === 2) { return target.slice(targetOffset, targetOffset + targetSize); } else if (arguments.length === 3) { args[2](undefined, target.slice(targetOffset, targetOffset + targetSize)); } else if (arguments.length === 6) { return targetSize; } else if (arguments.length === 7) { args[6](undefined, targetSize); } else { throw new Error('unreachable'); } }; Independent.hmac = function(...args) { if (args.length === 3 || args.length === 4) { var algorithm = args[0]; var key = args[1]; var keyOffset = 0; var keySize = key.length; var source = args[2]; var sourceOffset = 0; var sourceSize = source.length; var target = Buffer.alloc(64); var targetOffset = 0; } else if (args.length === 9 || args.length === 10) { var algorithm = args[0]; var key = args[1]; var keyOffset = args[2]; var keySize = args[3]; var source = args[4]; var sourceOffset = args[5]; var sourceSize = args[6]; var target = args[7]; var targetOffset = args[8]; } else { throw new Error('unreachable'); } var hmac = Node.crypto.createHmac(algorithm, key.slice(keyOffset, keyOffset + keySize)); hmac.update(source.slice(sourceOffset, sourceOffset + sourceSize)); var targetSize = hmac.digest().copy(target, targetOffset); if (arguments.length === 3) { return target.slice(targetOffset, targetOffset + targetSize); } else if (arguments.length === 4) { args[3](undefined, target.slice(targetOffset, targetOffset + targetSize)); } else if (arguments.length === 9) { return targetSize; } else if (arguments.length === 10) { args[9](undefined, targetSize); } else { throw new Error('unreachable'); } }; module.exports.CIPHER_BLOCK_MAX = CIPHER_BLOCK_MAX; module.exports.cipher = Cipher; module.exports.hash = Hash; module.exports.hmac = HMAC; module.exports.independent = Independent; module.exports.random = random; module.exports.seed = seed;