diff --git a/src/crypto/random.js b/src/crypto/random.js index 9c85ca5f..51caad81 100644 --- a/src/crypto/random.js +++ b/src/crypto/random.js @@ -24,8 +24,6 @@ var type_mpi = require('../type/mpi.js'); var nodeCrypto = null; -var randomBuffer = null; -var randomBufferIndex = 0; if (typeof window === 'undefined') { nodeCrypto = require('crypto'); @@ -86,13 +84,8 @@ module.exports = { } else if (nodeCrypto) { var bytes = nodeCrypto.randomBytes(4); buf[0] = (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]; - } else if (randomBuffer) { - if (randomBufferIndex < buf.length) { - throw new Error('Random number buffer depleted.') - } - for (var i = 0; i < buf.length; i++) { - buf[i] = randomBuffer[--randomBufferIndex]; - } + } else if (this.randomBuffer.buffer) { + this.randomBuffer.get(buf); } else { throw new Error('No secure random number generator available.'); } @@ -135,13 +128,55 @@ module.exports = { return min.add(r); }, - /** - * Set array of random numbers to buffer - * @param {Uint32Array} buf - */ - seedRandom: function(buf) { - randomBuffer = buf; - randomBufferIndex = buf.length; - } + randomBuffer: new RandomBuffer() }; + +/** + * Buffer for secure random numbers + */ +function RandomBuffer() { + this.buffer = null; + this.size = null; +} + +/** + * Initialize buffer + * @param {Integer} size size of buffer + */ +RandomBuffer.prototype.init = function(size) { + this.buffer = new Uint32Array(size); + this.size = 0; +}; + +/** + * Concat array of secure random numbers to buffer + * @param {Uint32Array} buf + */ +RandomBuffer.prototype.set = function(buf) { + if (!this.buffer) { + throw new Error('RandomBuffer is not initialized'); + } + var freeSpace = this.buffer.length - this.size; + if (buf.length > freeSpace) { + buf = buf.subarray(0, freeSpace); + } + this.buffer.set(buf, this.size); + this.size += buf.length; +}; + +/** + * Take numbers out of buffer and copy to array + * @param {Uint32Array} buf the destination array + */ +RandomBuffer.prototype.get = function(buf) { + if (!this.buffer) { + throw new Error('RandomBuffer is not initialized'); + } + if (this.size < buf.length) { + throw new Error('Random number buffer depleted.') + } + for (var i = 0; i < buf.length; i++) { + buf[i] = this.buffer[--this.size]; + } +}; diff --git a/src/packet/public_key.js b/src/packet/public_key.js index f751ea60..35965c47 100644 --- a/src/packet/public_key.js +++ b/src/packet/public_key.js @@ -185,6 +185,14 @@ PublicKey.prototype.getFingerprint = function () { } }; +/** + * Returns bit size of key + * @return {int} Number of bits + */ +PublicKey.prototype.getBitSize = function () { + return this.mpi[0].byteLength() * 8; +}; + /** * Fix custom types after cloning */ diff --git a/src/worker/async_proxy.js b/src/worker/async_proxy.js index 6b5b7852..ec08c5c6 100644 --- a/src/worker/async_proxy.js +++ b/src/worker/async_proxy.js @@ -15,33 +15,76 @@ // License along with this library; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +/** + * @requires crypto + * @requires enums + * @requires packet + * @requires type_keyid + * @requires key + * @module async_proxy + */ + var crypto = require('../crypto'), packet = require('../packet'), key = require('../key.js'), - type_keyid = require('../type/keyid.js'); + type_keyid = require('../type/keyid.js'), + enums = require('../enums.js'); +var INITIAL_SEED = 4096, // bytes seeded to worker + SEED_REQUEST = 4096, // bytes seeded after worker request + RSA_FACTOR = 2, + DSA_FACTOR = 2, + ELG_FACTOR = 2; + +/** + * Initializes a new proxy and loads the web worker + * @constructor + * @param {String} path The path to the worker or 'openpgp.worker.js' by default + */ function AsyncProxy(path) { this.worker = new Worker(path || 'openpgp.worker.js'); this.worker.onmessage = this.onMessage.bind(this); + this.seedRandom(INITIAL_SEED); // FIFO this.tasks = []; } +/** + * Message handling + */ AsyncProxy.prototype.onMessage = function(event) { var msg = event.data; switch (msg.event) { case 'method-return': this.tasks.shift()(msg.err ? new Error(msg.err) : null, msg.data); break; + case 'request-seed': + this.seedRandom(SEED_REQUEST); + break; default: throw new Error('Unknown Worker Event.'); } }; +/** + * Send message to worker with random data + * @param {Integer} size Number of bytes to send + */ AsyncProxy.prototype.seedRandom = function(size) { + var buf = this.getRandomBuffer(size); + this.worker.postMessage({event: 'seed-random', buf: buf}); +}; + +/** + * Get Uint32Array with random numbers + * @param {Integer} size Length of buffer + * @return {Uint32Array} + */ +AsyncProxy.prototype.getRandomBuffer = function(size) { + if (!size) return null; var buf = new Uint32Array(size); crypto.random.getRandomValues(buf); - this.worker.postMessage({event: 'seed-random', buf: buf}); + return buf; }; /** @@ -51,6 +94,50 @@ AsyncProxy.prototype.terminate = function() { this.worker.terminate(); }; +/** + * Estimation on how much random bytes are required to process the operation + * @param {String} op 'enc', 'sig' or 'gen' + * @param {Array} publicKeys + * @param {Array} privateKeys + * @param {Object} options + * @return {Integer} number of bytes required + */ +AsyncProxy.prototype.entropyEstimation = function(op, publicKeys, privateKeys, options) { + var requ = 0; // required entropy in bytes + switch (op) { + case 'enc': + requ += 32; // max. size of session key + requ += 16; // max. size CFB prefix random + publicKeys && publicKeys.forEach(function(key) { + var subKeyPackets = key.getSubkeyPackets(); + for (var i = 0; i < subKeyPackets.length; i++) { + if (enums.write(enums.publicKey, subKeyPackets[i].algorithm) == enums.publicKey.elgamal) { + var keyByteSize = subKeyPackets[i].mpi[0].byteLength(); + requ += keyByteSize * ELG_FACTOR; // key byte size for ElGamal keys + break; + } + } + }); + break; + case 'sig': + privateKeys && privateKeys.forEach(function(key) { + if (enums.write(enums.publicKey, key.primaryKey.algorithm) == enums.publicKey.dsa) { + requ += 32 * DSA_FACTOR; // 32 bytes for DSA keys + } + }); + break; + case 'gen': + requ += 8; // salt for S2K; + requ += 16; // CFB initialization vector + requ += (Math.ceil(options.numBits / 8) + 1) * RSA_FACTOR; + requ = requ * 2; // * number of key packets + break; + default: + throw new Error('Unknown operation.'); + } + return requ; +}; + /** * Encrypts message text with keys * @param {Array} keys array of keys, used to encrypt the message @@ -58,13 +145,15 @@ AsyncProxy.prototype.terminate = function() { * @param {Function} callback receives encrypted ASCII armored message */ AsyncProxy.prototype.encryptMessage = function(keys, text, callback) { + var estimation = this.entropyEstimation('enc', keys); keys = keys.map(function(key) { return key.toPacketlist(); }); this.worker.postMessage({ event: 'encrypt-message', keys: keys, - text: text + text: text, + seed: this.getRandomBuffer(estimation) }); this.tasks.push(callback); }; @@ -77,6 +166,8 @@ AsyncProxy.prototype.encryptMessage = function(keys, text, callback) { * @param {Function} callback receives encrypted ASCII armored message */ AsyncProxy.prototype.signAndEncryptMessage = function(publicKeys, privateKey, text, callback) { + var estimation = this.entropyEstimation('enc', publikKeys) + + this.entropyEstimation('sig', null, [privateKey]); publicKeys = publicKeys.map(function(key) { return key.toPacketlist(); }); @@ -85,7 +176,8 @@ AsyncProxy.prototype.signAndEncryptMessage = function(publicKeys, privateKey, te event: 'sign-and-encrypt-message', publicKeys: publicKeys, privateKey: privateKey, - text: text + text: text, + seed: this.getRandomBuffer(estimation) }); this.tasks.push(callback); }; @@ -144,13 +236,15 @@ AsyncProxy.prototype.decryptAndVerifyMessage = function(privateKey, publicKeys, * @param {Function} callback receives ASCII armored message */ AsyncProxy.prototype.signClearMessage = function(privateKeys, text, callback) { + var estimation = this.entropyEstimation('sig', null, privateKeys); privateKeys = privateKeys.map(function(key) { return key.toPacketlist(); }); this.worker.postMessage({ event: 'sign-clear-message', privateKeys: privateKeys, - text: text + text: text, + seed: this.getRandomBuffer(estimation) }); this.tasks.push(callback); }; @@ -192,13 +286,13 @@ AsyncProxy.prototype.verifyClearSignedMessage = function(publicKeys, message, ca * @param {Function} callback receives object with key and public and private armored texts */ AsyncProxy.prototype.generateKeyPair = function(keyType, numBits, userId, passphrase, callback) { - this.seedRandom(Math.round(numBits / 2)); this.worker.postMessage({ event: 'generate-key-pair', keyType: keyType, numBits: numBits, userId: userId, - passphrase: passphrase + passphrase: passphrase, + seed: this.getRandomBuffer(this.entropyEstimation('gen', null, null, {numBits: numBits})) }); this.tasks.push(function(err, data) { if (data) { diff --git a/src/worker/worker.js b/src/worker/worker.js index a8b9c9a4..664dc4e3 100644 --- a/src/worker/worker.js +++ b/src/worker/worker.js @@ -19,14 +19,21 @@ window = {}; // to make UMD bundles work importScripts('openpgp.js'); +var MIN_SIZE_RANDOM_BUFFER = 8192; +var MAX_SIZE_RANDOM_BUFFER = 16384; + +window.openpgp.crypto.random.randomBuffer.init(MAX_SIZE_RANDOM_BUFFER); onmessage = function (event) { var data = null, - err = null, + err = null, msg = event.data; + if (msg.seed) { + window.openpgp.crypto.random.randomBuffer.set(msg.seed); + } switch (msg.event) { case 'seed-random': - window.openpgp.crypto.random.seedRandom(msg.buf); + window.openpgp.crypto.random.randomBuffer.set(msg.buf); break; case 'encrypt-message': try { @@ -35,7 +42,7 @@ onmessage = function (event) { } catch (e) { err = e.message; } - postMessage({event: 'method-return', data: data, err: err}); + response({event: 'method-return', data: data, err: err}); break; case 'sign-and-encrypt-message': try { @@ -45,7 +52,7 @@ onmessage = function (event) { } catch (e) { err = e.message; } - postMessage({event: 'method-return', data: data, err: err}); + response({event: 'method-return', data: data, err: err}); break; case 'decrypt-message': try { @@ -55,7 +62,7 @@ onmessage = function (event) { } catch (e) { err = e.message; } - postMessage({event: 'method-return', data: data, err: err}); + response({event: 'method-return', data: data, err: err}); break; case 'decrypt-and-verify-message': try { @@ -66,7 +73,7 @@ onmessage = function (event) { } catch (e) { err = e.message; } - postMessage({event: 'method-return', data: data, err: err}); + response({event: 'method-return', data: data, err: err}); break; case 'sign-clear-message': try { @@ -75,7 +82,7 @@ onmessage = function (event) { } catch (e) { err = e.message; } - postMessage({event: 'method-return', data: data, err: err}); + response({event: 'method-return', data: data, err: err}); break; case 'verify-clear-signed-message': try { @@ -86,7 +93,7 @@ onmessage = function (event) { } catch (e) { err = e.message; } - postMessage({event: 'method-return', data: data, err: err}); + response({event: 'method-return', data: data, err: err}); break; case 'generate-key-pair': try { @@ -95,13 +102,20 @@ onmessage = function (event) { } catch (e) { err = e.message; } - postMessage({event: 'method-return', data: data, err: err}); + response({event: 'method-return', data: data, err: err}); break; default: throw new Error('Unknown Worker Event.'); } }; +function response(event) { + if (window.openpgp.crypto.random.randomBuffer.size < MIN_SIZE_RANDOM_BUFFER) { + postMessage({event: 'request-seed'}); + } + postMessage(event); +} + function packetlistCloneToKey(packetlistClone) { var packetlist = window.openpgp.packet.List.fromStructuredClone(packetlistClone); return new window.openpgp.key.Key(packetlist);