/* * Copyright 2018 Scytl Secure Electronic Voting SA * * All rights reserved * * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package */ /* jshint node:true */ 'use strict'; var Tools = require('../tools'); var AjaxCollector = require('./collectors/ajax'); var PrivateKeyCollector = require('./collectors/privatekey'); var JSExecutionCollector = require('./collectors/jsexecution'); var NavigatorInfoCollector = require('./collectors/navigatorinfo'); var MathRandomCollector = require('./collectors/mathrandom'); var WebKitRandomCollector = require('./collectors/webkitrandom'); var DeviceOrientationCollector = require('./events/deviceorientation'); var DeviceMotionCollector = require('./events/devicemotion'); var MouseMoveCollector = require('./events/mousemove'); var MouseWheelCollector = require('./events/mousewheel'); var MouseButtonCollector = require('./events/mousebutton'); var TouchStartCollector = require('./events/touchstart'); var TouchMoveCollector = require('./events/touchmove'); var TouchEndCollector = require('./events/touchend'); var KeyCollector = require('./events/key'); var codec = require('scytl-codec'); module.exports = EntropyManager; /** * Manager in charge of collecting the amount of entropy required to perform * secure random operations. * * @class EntropyManager * @private * @param {Object} * [options] An object containing optional arguments. * *

* This object can be used to override the default values of the entropy * manager, by making use of the following fields. For further explanation, see * the README.md file for this module. *

*

*/ function EntropyManager(options) { ////////////////////////////////// PRIVATE ////////////////////////////////// var that = this; var tools_; var maxEntropyCounter_; var hashUpdaterInterval_; var windowObject_; var cryptoApi_; var privateKeyData_; var collectors_ = []; var events_ = []; var collectorHashUpdater_ = null; var entropyHashHex_ = ''; var entropyCounter_ = 0; var isCollecting_ = false; var fullEntropyReached_ = false; var fullEntropyCallback_ = null; /** * Initializes the entropy manager. * * @function init * @memberof EntropyManager * @param {Object} * [options] The optional arguments passed by the constructor to * override the default values of the entropy manager */ function init(options) { tools_ = new Tools(); // Optional arguments. options = options || {}; // Amount of entropy to accumulate before stopping collectors. Default is // 256. maxEntropyCounter_ = options.maxEntropyCounter || 256; // Interval (in milliseconds) at which entropy data is to be hashed. Default // is 1 second. hashUpdaterInterval_ = options.hashUpdaterInterval || 1000; // "window" object provided to collectors. Default is built-in "window" // object. windowObject_ = options.windowObject || (typeof window !== 'undefined') ? window : null; // Cryptographic library supplied to WebKit collector (if being used). // Default is user's built-in cryptographic library. cryptoApi_ = options.cryptoApi || (windowObject_ ? (windowObject_.crypto || windowObject_.msCrypto) : null); // Private key collector data. By default, this collector is not used. privateKeyData_ = options.privateKeyData || null; // Set up standard set of collectors, unless specified not to. if (!options.noDefaultCollectors) { // Set up the default collectors. setupCollectors(); } } /** * Sets up all of the entropy collectors to be used. If WebKit is available, * its entropy collector will be used exclusively. Otherwise, a combination * of other types of collectors will used, including the ajax calls * collector, JS calls execution collector, navigator information collector, * math random collector, private key collector, mouse events collectors, * key events collectors, load event collector and scroll collector. * * @function setupCollectors * @memberof EntropyManager */ function setupCollectors() { if (!windowObject_) { addNonWindowCollectors(); return; } if (cryptoApi_ && (cryptoApi_.getRandomValues)) { that.addCollector(new WebKitRandomCollector( collectEntropy, cryptoApi_, maxEntropyCounter_)); } else { addNonWindowCollectors(); // The API detects and delivers accelerometer data 50 times per // second if (window.DeviceOrientationEvent) { // Listen for the device orientation event and handle // DeviceOrientationEvent object that.addEvent( 'deviceorientation', new DeviceOrientationCollector(collectEntropy).handleEvent); } else if (window.OrientationEvent) { // Listen for the MozOrientation event and handle // OrientationData object that.addEvent( 'MozOrientation', new DeviceOrientationCollector(collectEntropy).handleEvent); } if (window.DeviceMotionEvent) { that.addEvent( 'devicemotion', new DeviceMotionCollector(collectEntropy).handleEvent); } } } /** * Registers the entropy collectors that do not need a window object to * work. * * @function addNonWindowCollectors * @memberof EntropyManager */ function addNonWindowCollectors() { addCollectors([ AjaxCollector, JSExecutionCollector, NavigatorInfoCollector, MathRandomCollector ]); if (privateKeyData_) { collectors_.push(new PrivateKeyCollector( collectEntropy, privateKeyData_.privateKeyPem, privateKeyData_.pinEntropy)); } addEvents({ 'mousemove': MouseMoveCollector, 'mousewheel': MouseWheelCollector, 'mouseup': MouseButtonCollector, 'mousedown': MouseButtonCollector, 'touchstart': TouchStartCollector, 'touchmove': TouchMoveCollector, 'touchend': TouchEndCollector, 'gesturestart': TouchStartCollector, 'gestureend': TouchEndCollector, 'keyup': KeyCollector, 'keydown': KeyCollector, 'load': KeyCollector, 'scroll': KeyCollector, 'beforeload': KeyCollector, }); } /** * Registers a set of entropy collectors from an array of their * constructors. * * @function addCollectors * @memberof EntropyManager * @param {Array} * collectors The array of entropy collector constructors. */ function addCollectors(collectors) { for (var i = 0; i < collectors.length; ++i) { collectors_.push(new collectors[i](collectEntropy)); } } /** * Registers a set of entropy events from a map of their constructors, using * the event names as keys. * * @function addEvents * @memberof EntropyManager * @param {Object} * events The map of entropy event constructors, using the event * names as keys. */ function addEvents(events) { for (var i = 0, eventNames = Object.keys(events), size = eventNames.length; i < size; i++) { that.addEvent( eventNames[i], (new events[eventNames[i]](collectEntropy)).handleEvent); } } /** * Registers an entropy collector. The "collector" argument can be an * object, in which case it will be added directly to the collector array, * or it can be a constructor, in which case it will be instantiated and the * resulting object will be added to the collector array. * * @function addCollector * @memberof EntropyManager * @param {Object} * collector An object or constructor that implements the * "startCollector" and "stopCollector" methods. * @returns {Object} The instantiation of the entropy collector that was * registered. */ this.addCollector = function(Collector) { if (typeof Collector === 'function') { // If a constructor is passed, create a new object and link it to the // entropy collector. Collector = new Collector(collectEntropy); } collectors_.push(Collector); return Collector; }; /** * Registers a new event. * * @function addEvent * @memberof EntropyManager * @param {string} * type The event type. * @param {function} * listener The event listener. */ this.addEvent = function(type, listener) { events_.push({'type': type, 'listener': listener}); }; /** * Adds entropy to the entropy collected so far, and increments the entropy * counter. This method is usually called internally by the entropy collectors * and the events but is made public here for use as a callback function when * adding individual collectors via the secure random service. * * @function collectEntropy * @memberof EntropyManager * @param {string} * data The entropy hash data to be added. * @param {number} * increment The entropy counter increment to be added. */ this.collectEntropy = function(data, increment) { return collectEntropy(data, increment); }; /** * Adds entropy to the entropy collected so far, and increments the entropy * counter. This method is usually called by the entropy collectors and the * events. * * @function collectEntropy * @memberof EntropyManager * @param {string} * data The entropy hash data to be added. * @param {number} * increment The entropy counter increment to be added. */ function collectEntropy(data, increment) { entropyHashHex_ += data; entropyCounter_ += (increment ? increment : 0); // if the entropy counter has reached the limit, update the hash and stop // the collectors if (isCollecting_ && (entropyCounter_ >= maxEntropyCounter_)) { // Full entropy reached. fullEntropyReached_ = true; // Stop collecting entropy. that.stopCollectors(); // Make the full entropy callback, if present. if (typeof fullEntropyCallback_ === 'function') { fullEntropyCallback_(); } } } /** * Updates the entropy hash, using the entropy collected so far. * * @function updateEntropyHash * @memberof EntropyManager */ function updateEntropyHash() { var digester = tools_.getUpdateDigester(); digester.update(entropyHashHex_); entropyHashHex_ = codec.hexEncode(codec.binaryDecode(digester.digest().getBytes())); } /////////////////////////////////// PUBLIC /////////////////////////////////// /** * Starts the entropy collectors. * * @function startCollectors * @memberof EntropyManager * @param {function} * fullEntropyCallback The function that is called when the * required amount of entropy has been collected. */ this.startCollectors = function(fullEntropyCallback) { if (isCollecting_) { // Entropy collection is in progress -- nothing to do. return; } else { isCollecting_ = true; } if (fullEntropyCallback) { // Assign the full entropy callback. fullEntropyCallback_ = fullEntropyCallback; } var i = 0; // start all the collectors while (i < collectors_.length) { try { collectors_[i].startCollector(); } catch (e) { // do not do anything about any exception that is thrown // by the execution of the collectors } i++; } i = 0; // start all the events while (i < events_.length) { try { if (window.addEventListener) { window.addEventListener(events_[i].type, events_[i].listener, false); } else if (document.attachEvent) { document.attachEvent('on' + events_[i].type, events_[i].listener); } } catch (e) { // do not do anything about any exception that is thrown // by the execution of the events } i++; } tools_.getUpdateDigester().start(); collectorHashUpdater_ = setInterval(function() { updateEntropyHash(); }, hashUpdaterInterval_); }; /** * Stops the entropy collectors. This method will have no effect if the * required amount of entropy has already been collected. * * @function stopCollectors * @memberof EntropyManager * @returns {boolean} True if the required amount of entropy has been * collected, false otherwise. */ this.stopCollectors = function() { if (!isCollecting_) { // Entropy collection is not in progress -- nothing to do. if (entropyCounter_ >= maxEntropyCounter_) { return true; } else { return false; } } else { isCollecting_ = false; } var i = 0; while (i < collectors_.length) { try { collectors_[i].stopCollector(); } catch (e) { // do not do anything about any exception that is thrown // by the execution of the collectors } i++; } i = 0; while (i < events_.length) { try { if (window.removeEventListener) { window.removeEventListener( events_[i].type, events_[i].listener, false); } else if (document.detachEvent) { document.detachEvent('on' + events_[i].type, events_[i].listener); } } catch (e) { // do not do anything about any exception that is thrown // by the removal of the events } i++; } if (collectorHashUpdater_) { clearInterval(collectorHashUpdater_); } updateEntropyHash(); return fullEntropyReached_; }; /** * @function getCollectors. * @memberof EntropyManager * @returns {Array} The array of collectors currently in use. */ this.getCollectors = function() { return collectors_; }; /** * @function getEntropyHash * @memberof EntropyManager * @returns {Uint8Array} The hash of entropy that has been collected so far. */ this.getEntropyHash = function() { return codec.hexDecode(entropyHashHex_); }; /** * @function getEntropyPercentage returns {number} The percentage of the * required entropy that has been collected so far. * @memberof EntropyManager */ this.getEntropyPercentage = function() { if (entropyCounter_ >= maxEntropyCounter_) { return 100; } return entropyCounter_ * 100 / maxEntropyCounter_; }; /** * @function isCollecting * @memberof EntropyManager * @returns {boolean} True if entropy is still being collected, false * otherwise. */ this.isCollecting = function() { return isCollecting_; }; //////////////////////////////// CONSTRUCTOR //////////////////////////////// // Initialize the object. init(options); }