/* * 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 */ var cryptoPRNG = (function() { 'use strict'; return { _collectorsStarted: false, _collectors: [], _events: [], _entropy: '', _entropyCounter: 0, _maxEntropyCounter: 0, _collectorHashUpdater: null, _hashUpdaterInterval: 0, _fullEntropyCallback: null, _tools: null, _seedLength: 0, _prng: null, _privateKeyPem: '', _pinEntropy: 0, _usePrivateKeyEntropy: false, /** * Initializes all the round object fields. * * @function * @param hashUpdaterInterval * the interval, in millis, that the function that replaces the * current entropy content by an hash is called. By default it is * initialized to 5000. * @param maxEntropyCounter * number of maximum bits used to determine when the collectors * should stop collecting entropy. By default it is initialized * to 256. */ _initAndStart: function(hashUpdaterInterval, fullEntropyCallback) { this._collectorsStarted = false; this._collectors = []; this._events = []; this._entropy = ''; this._entropyCounter = 0; this._maxEntropyCounter = 256; this._collectorHashUpdater = null; this._hashUpdaterInterval = (hashUpdaterInterval ? hashUpdaterInterval : 1000); this._fullEntropyCallback = fullEntropyCallback; this._tools = cryptoPRNG.Tools.init(); this._initCollectors(); this._startCollectors(); }, /** * adds a new collector to the list of available connectors * * @param collector * must return an object that implements the startCollector and * stopCollector methods */ _addCollector: function(collector) { this._collectors.push(collector); }, /** * adds a new event to the list of available events * * @param type * event type * @param listener * function that will be executed */ _addEvent: function(type, listener) { this._events.push({'type': type, 'listener': listener}); }, /** * Initializes the random generator with the web kit random collector if * available. * * If it is not, it considers the rest of collectors: ajax calls collector, * JS calls execution collector, navigator information collector, math * random collector, web kit random collector, mouse events collectors, key * events collectors, load event collector, scroll collector) */ _initCollectors: function() { if (this._noWindow()) { this._addNonWindowEntropyCollectors(); return; } var _crypto = this._getCrypto(); if ((_crypto) && (_crypto.getRandomValues)) { this._addCollector(cryptoPRNG.Collectors.getWebKitRandomCollector( _crypto, this._maxEntropyCounter)); } else { this._addNonWindowEntropyCollectors(); // The API detects and delivers accelerometer data 50 times per // second if (window.DeviceOrientationEvent) { // Listen for the device orientation event and handle // DeviceOrientationEvent object this._addEvent( 'deviceorientation', cryptoPRNG.Collectors.getDeviceOrientationCollector); } else if (window.OrientationEvent) { // Listen for the MozOrientation event and handle // OrientationData object this._addEvent( 'MozOrientation', cryptoPRNG.Collectors.getDeviceOrientationCollector); } if (window.DeviceMotionEvent) { this._addEvent( 'devicemotion', cryptoPRNG.Collectors.getDeviceMotionCollector); } } }, _addNonWindowEntropyCollectors: function() { if (this._usePrivateKeyEntropy) { this._addCollector(cryptoPRNG.Collectors.getPrivateKeyCollector( this._privateKeyPem, this._pinEntropy)); } this._addCollector(cryptoPRNG.Collectors.getAjaxCollector()); this._addCollector(cryptoPRNG.Collectors.getJSExecutionCollector()); this._addCollector(cryptoPRNG.Collectors.getNavigatorInfoCollector()); this._addCollector(cryptoPRNG.Collectors.getMathRandomCollector()); // mouse events collectors this._addEvent('mousemove', cryptoPRNG.Collectors.getMouseMoveCollector); this._addEvent( 'mousewheel', cryptoPRNG.Collectors.getMouseWheelCollector); this._addEvent('mouseup', cryptoPRNG.Collectors.getMouseUpCollector); this._addEvent('mousedown', cryptoPRNG.Collectors.getMouseDownCollector); this._addEvent( 'touchstart', cryptoPRNG.Collectors.getTouchStartCollector); this._addEvent('touchmove', cryptoPRNG.Collectors.getTouchMoveCollector); this._addEvent('touchend', cryptoPRNG.Collectors.getTouchEndCollector); this._addEvent( 'gesturestart', cryptoPRNG.Collectors.getGestureStartCollector); this._addEvent( 'gestureend', cryptoPRNG.Collectors.getGestureEndCollector); // keyboard events collectors this._addEvent('keyup', cryptoPRNG.Collectors.getKeyUpCollector); this._addEvent('keydown', cryptoPRNG.Collectors.getKeyDownCollector); // page events collectors this._addEvent('load', cryptoPRNG.Collectors.getLoadCollector); this._addEvent('scroll', cryptoPRNG.Collectors.getScrollCollector); // requests collector collectors this._addEvent('beforeload', cryptoPRNG.Collectors.getRequestsCollector); }, _getCrypto: function() { return window.crypto || window.msCrypto; }, _noWindow: function() { return (typeof window === 'undefined'); }, /** * Start the collectors */ _startCollectors: function() { if (this._collectorsStarted) { return; } var i = 0; // start all the collectors while (i < this._collectors.length) { try { this._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 < this._events.length) { try { if (window.addEventListener) { window.addEventListener( this._events[i].type, this._events[i].listener, false); } else if (document.attachEvent) { document.attachEvent( 'on' + this._events[i].type, this._events[i].listener); } } catch (e) { // do not do anything about any exception that is thrown // by the execution of the events } i++; } this._tools._mdUpdate.start(); this._collectorHashUpdater = setInterval(function() { cryptoPRNG._hashUpdater(); }, this._hashUpdaterInterval); this._collectorsStarted = true; }, /** * Stop the collectors */ _stopCollectors: function() { if (!this._collectorsStarted) { return; } var i = 0; while (i < this._collectors.length) { try { this._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 < this._events.length) { try { if (window.removeEventListener) { window.removeEventListener( this._events[i].type, this._events[i].listener, false); } else if (document.detachEvent) { document.detachEvent( 'on' + this._events[i].type, this._events[i].listener); } } catch (e) { // do not do anything about any exception that is thrown // by the remove of the events } i++; } if (this._collectorHashUpdater) { clearInterval(this._collectorHashUpdater); } this._collectorsStarted = false; }, /** * Usually this method is called from the collectors and events. It adds * entropy to the already collected entropy and increments the entropy * counter * * @param data * the entropy data to be added * @param entropyCounter * the entropy counter to be added */ _collectEntropy: function(data, entropyCounter) { this._entropy += data; this._entropyCounter += (entropyCounter ? entropyCounter : 0); // if the entropy counter has reach the limit, update the hash and stop // the collectors if (this._entropyCounter >= this._maxEntropyCounter && this._collectorsStarted) { this._hashUpdater(); } }, /** * Sets the entropy data with the hash that is created using the current * entropy data value. If the entropy counter has reached the max entropy * counter set, it stops the collectors * * @return the generated entropy hash */ _hashUpdater: function() { var entropyHash = this._updateEntropyHash(); if (this._entropyCounter >= this._maxEntropyCounter && this._collectorsStarted) { entropyHash = this._stopCollectEntropy(); var newPRNG = new cryptoPRNG.PRNG(entropyHash, this._tools); if (this._fullEntropyCallback) { this._fullEntropyCallback(newPRNG); } else { this._prng = newPRNG; } } return entropyHash; }, _stopCollectEntropy: function() { // stop collecting entropy this._stopCollectors(); var entropyHash = this._updateEntropyHash(); this._entropy = ''; this._entropyCounter = 0; return entropyHash; }, _updateEntropyHash: function() { var digester = this._tools._mdUpdate; digester.update(this._entropy); var entropyHash = forge.util.bytesToHex(digester.digest().getBytes()); this._entropy = entropyHash; return entropyHash; }, /** * Set Private Key and an Entropy it adds to be used to collect Entropy. The * amount of randomness(entropy) depends on the pin that is used for Private * Key generating. * * @function * @param privateKey * {string} private key, as string in PEM format. * @param pinEntropy * the amount of randomness that can be extracted from the * Private Key. */ setDataForPrivateKeyEntropyCollector: function(privateKeyPem, pinEntropy) { if (privateKeyPem && (!isNaN(pinEntropy)) && (pinEntropy > 0)) { this._privateKeyPem = privateKeyPem; this._pinEntropy = pinEntropy; this._usePrivateKeyEntropy = true; } }, /** * Initializes collectors and starts them. Set Private Key and Entropy * before if this information should be used to collect Entropy. */ startEntropyCollection: function(entropyCollectorCallback) { var hashUpdaterInterval = 1000; this._initAndStart(hashUpdaterInterval, entropyCollectorCallback); }, /** * In the case that the maximum amount of entropy had been reached before * calling this method, it does not do anything, and a false is returned. * Otherwise, this method creates a PRNG if the collectors have not gathered * the maximum amount of entropy. In addition, it stops the collectors. * * This method return a true if the maximum entropy was already reached and * false in other case. */ stopEntropyCollectionAndCreatePRNG: function() { var generatedWithMaximumEntropy = true; if (!this._prng) { var entropyHash = this._stopCollectEntropy(); this._prng = new cryptoPRNG.PRNG(entropyHash, this._tools); generatedWithMaximumEntropy = false; } return generatedWithMaximumEntropy; }, /** * This method creates a PRNG from a given seed, in Hexadecimal format. This * method is to be used from a Worker, which has received the seed from the * main thread. * * @param seed * A string of 64 characters. It represents a 32-byte array in * Hexadecimal. * */ createPRNGFromSeed: function(seed) { this._prng = new cryptoPRNG.PRNG(seed, this._tools); }, /** * Generates a random array of bytes, which is then formatted to hexadecimal * This method is to be called from the main thread, and the given string is * to be passed to a Worker. */ generateRandomSeedInHex: function(lengthSeed) { this._seedLength = (lengthSeed ? lengthSeed : 32); var seedInBytes = this._prng.generate(this._seedLength); return forge.util.bytesToHex(seedInBytes); }, getPRNG: function() { if (!this._prng) { throw new Error('The PRNG has not been initialized yet'); } return this._prng; }, getEntropyCollectedAsPercentage: function() { if (this._entropyCounter >= this._maxEntropyCounter) { return 100; } return this._entropyCounter * 100 / this._maxEntropyCounter; } }; })(); cryptoPRNG.Tools = (function() { 'use strict'; return { _mdUpdate: null, _mdReseed: null, _formatKey: null, _formatSeed: null, _cipher: null, init: function() { this._mdUpdate = forge.md.sha256.create(); this._mdReseed = forge.md.sha256.create(); this._formatKey = function(key) { // convert the key into 32-bit integers var tmp = forge.util.createBuffer(key); key = [ tmp.getInt32(), tmp.getInt32(), tmp.getInt32(), tmp.getInt32(), tmp.getInt32(), tmp.getInt32(), tmp.getInt32(), tmp.getInt32() ]; // return the expanded key return forge.aes._expandKey(key, false); }; this._formatSeed = function(seed) { // convert seed into 32-bit integers var tmp = forge.util.createBuffer(seed); seed = [ tmp.getInt32(), tmp.getInt32(), tmp.getInt32(), tmp.getInt32() ]; return seed; }; this._cipher = function(key, counter) { var aes_output = []; forge.aes._updateBlock( this._formatKey(key), this._formatSeed('' + counter), aes_output, false); var aes_buffer = forge.util.createBuffer(); aes_buffer.putInt32(aes_output[0]); aes_buffer.putInt32(aes_output[1]); aes_buffer.putInt32(aes_output[2]); aes_buffer.putInt32(aes_output[3]); return aes_buffer.getBytes(); }; return this; } }; })(); cryptoPRNG.PRNG = function(entropyHash, tools) { 'use strict'; this._key = ''; this._counter = 0; this._tools = null; if (tools) { this._tools = tools; } else { this._tools = cryptoPRNG.Tools.init(); } this._entropyHash = entropyHash; }; cryptoPRNG.PRNG.prototype = (function() { 'use strict'; return { /** * Generates a random array of bytes using the gathered entropy data. * * @param count * the total number of random bytes to generate */ generate: function(count) { var keyAux; if (this._key === null || this._key === '') { this._reseed(); } // buffer where to store the random bytes var b = forge.util.createBuffer(); var limitReach = 1; while (b.length() < count) { b.putBytes(this._tools._cipher(this._key, this._counter)); this._counter++; if (b.length >= (limitReach * Math.pow(2, 20))) { keyAux = this._key; this._key = ''; for (var i = 0; i < 2; i++) { this._key += this._tools._cipher(keyAux, this._counter); this._counter++; } limitReach++; } } // do it two times to ensure a key with 256 bits keyAux = this._key; this._key = ''; for (var j = 0; j < 2; j++) { this._key += this._tools._cipher(keyAux, this._counter); this._counter++; } return b.getBytes(count); }, /** * Reseeds the generator */ _reseed: function() { var digester = this._tools._mdReseed; digester.start(); digester.update(this._entropyHash); digester.update(this._key); this._key = forge.util.bytesToHex(digester.digest().getBytes()); this._counter++; } }; })(); cryptoPRNG.Collectors = (function() { 'use strict'; var MIN_MATH_ROUND_ENTROPY_FACTOR = 1000000; function _getMathRoundWithEntropy(data) { return Math.round(data * MIN_MATH_ROUND_ENTROPY_FACTOR); } return { getAjaxCollector: function() { return { startCollector: function() { // if jQuery is included if (window.jQuery !== undefined) { $(window) .ajaxStart(function() { cryptoPRNG._collectEntropy((+new Date()), 1); }) .ajaxComplete(function() { cryptoPRNG._collectEntropy((+new Date()), 1); }); } }, stopCollector: function() { // if jQuery is included if (window.jQuery !== undefined) { $(window).unbind('ajaxStart'); $(window).unbind('ajaxComplete'); } return true; } }; }, getJSExecutionCollector: function() { return { _collectTimeout: null, startCollector: function() { var timer_start = (+new Date()), timer_end; this._collectTimeout = (function collect() { timer_end = (+new Date()); var total_time = timer_end - timer_start; // because of browser baseline time checks we limit it if (total_time > 20) { cryptoPRNG._collectEntropy((+new Date()), 1); } timer_start = timer_end; return setTimeout(collect, 0); })(); }, stopCollector: function() { clearTimeout(this._collectTimeout); } }; }, getNavigatorInfoCollector: function() { return { startCollector: function() { if (typeof(navigator) !== 'undefined') { var _navString = ''; for (var key in navigator) { if (typeof key !== 'undefined') { try { if (typeof(navigator[key]) === 'string') { _navString += navigator[key]; } } catch (e) { // ignore any kind of exception } } } cryptoPRNG._collectEntropy(_navString, 1); } }, stopCollector: function() { // just executed once, so no need to execute nothing more return true; } }; }, getMathRandomCollector: function() { return { startCollector: function() { cryptoPRNG._collectEntropy(Math.random(), 0); }, stopCollector: function() { // just executed once, so no need to execute nothing more return true; } }; }, getPrivateKeyCollector: function(privateKeyPem, pinEntropy) { var privateKey = forge.pki.privateKeyFromPem(privateKeyPem); return { pkCollector: true, startCollector: function() { cryptoPRNG._collectEntropy(privateKey.d, pinEntropy); }, stopCollector: function() { // just executed once, so no need to execute nothing more return true; } }; }, getWebKitRandomCollector: function(globalCrypto, entropyQuantity) { return { startCollector: function() { var numPositions = entropyQuantity / 32; // get cryptographically strong entropy in Webkit var ab = new Uint32Array(numPositions); globalCrypto.getRandomValues(ab); var data = ''; for (var i = 0; i < ab.length; i++) { data += '' + (ab[i]); } cryptoPRNG._collectEntropy(data, entropyQuantity); }, stopCollector: function() { // just executed once, so no need to execute nothing more return true; } }; }, getMouseMoveCollector: function(ev) { // to ensure compatibility with IE 8 ev = ev || window.event; cryptoPRNG._collectEntropy( (ev.x || ev.clientX || ev.offsetX || 0) + (ev.y || ev.clientY || ev.offsetY || 0) + (+new Date()), 3); }, getMouseWheelCollector: function(ev) { ev = ev || window.event; cryptoPRNG._collectEntropy((ev.offsetY || 0) + (+new Date()), 3); }, getMouseDownCollector: function(ev) { ev = ev || window.event; cryptoPRNG._collectEntropy( (ev.x || ev.clientX || ev.offsetX || 0) + (ev.y || ev.clientY || ev.offsetY || 0) + (+new Date()), 3); }, getMouseUpCollector: function(ev) { ev = ev || window.event; cryptoPRNG._collectEntropy( (ev.x || ev.clientX || ev.offsetX || 0) + (ev.y || ev.clientY || ev.offsetY || 0) + (+new Date()), 3); }, getTouchStartCollector: function(ev) { ev = ev || window.event; cryptoPRNG._collectEntropy( (ev.touches[0].pageX || ev.clientX || 0) + (ev.touches[0].pageY || ev.clientY || 0) + (+new Date()), 5); }, getTouchMoveCollector: function(ev) { ev = ev || window.event; cryptoPRNG._collectEntropy( (ev.touches[0].pageX || ev.clientX || 0) + (ev.touches[0].pageY || ev.clientY || 0) + (+new Date()), 3); }, getTouchEndCollector: function() { cryptoPRNG._collectEntropy((+new Date()), 1); }, getGestureStartCollector: function(ev) { ev = ev || window.event; cryptoPRNG._collectEntropy( (ev.touches[0].pageX || ev.clientX || 0) + (ev.touches[0].pageY || ev.clientY || 0) + (+new Date()), 5); }, getGestureEndCollector: function() { cryptoPRNG._collectEntropy((+new Date()), 1); }, motionX: null, motionY: null, motionZ: null, getDeviceMotionCollector: function(ev) { ev = ev || window.event; var acceleration = ev.accelerationIncludingGravity; var currentX = (_getMathRoundWithEntropy(acceleration.x) || 0); var currentY = (_getMathRoundWithEntropy(acceleration.y) || 0); var currentZ = (_getMathRoundWithEntropy(acceleration.z) || 0); var rotation = ev.rotationRate; if (rotation !== null) { currentX += _getMathRoundWithEntropy(rotation.alpha); currentY += _getMathRoundWithEntropy(rotation.beta); currentZ += _getMathRoundWithEntropy(rotation.gamma); } // The API detects and delivers accelerometer data 50 times per // second // even if there is any event related, so this is // a way to control if it really changed or not if ((cryptoPRNG.Collectors.motionX === null) || (cryptoPRNG.Collectors.motionY === null) || (cryptoPRNG.Collectors.motionZ === null) || (cryptoPRNG.Collectors.motionX !== currentX) || (cryptoPRNG.Collectors.motionY !== currentY) || (cryptoPRNG.Collectors.motionZ !== currentZ)) { cryptoPRNG.Collectors.motionX = currentX; cryptoPRNG.Collectors.motionY = currentY; cryptoPRNG.Collectors.motionZ = currentZ; cryptoPRNG._collectEntropy( currentX + currentY + currentZ + (+new Date()), 1); } }, deviceOrientationX: null, deviceOrientationY: null, deviceOrientationZ: null, getDeviceOrientationCollector: function(ev) { ev = ev || window.event; var currentX = (_getMathRoundWithEntropy(ev.gamma) || _getMathRoundWithEntropy(ev.x) || 0); var currentY = (_getMathRoundWithEntropy(ev.beta) || _getMathRoundWithEntropy(ev.y) || 0); var currentZ = (_getMathRoundWithEntropy(ev.alpha) || _getMathRoundWithEntropy(ev.z) || 0); // The API detects and delivers accelerometer data 50 times per // second // even if there is any event related, so this is // a way to control if it really changed or not if ((cryptoPRNG.Collectors.deviceOrientationX === null) || (cryptoPRNG.Collectors.deviceOrientationY === null) || (cryptoPRNG.Collectors.deviceOrientationZ === null) || (cryptoPRNG.Collectors.deviceOrientationX !== currentX) || (cryptoPRNG.Collectors.deviceOrientationY !== currentY) || (cryptoPRNG.Collectors.deviceOrientationZ !== currentZ)) { cryptoPRNG.Collectors.deviceOrientationX = currentX; cryptoPRNG.Collectors.deviceOrientationY = currentY; cryptoPRNG.Collectors.deviceOrientationZ = currentZ; cryptoPRNG._collectEntropy( currentX + currentY + currentZ + (+new Date()), 1); } }, getKeyDownCollector: function(ev) { ev = ev || window.event; cryptoPRNG._collectEntropy((ev.keyCode) + (+new Date()), 3); }, getKeyUpCollector: function(ev) { ev = ev || window.event; cryptoPRNG._collectEntropy((ev.keyCode) + (+new Date()), 3); }, getLoadCollector: function(ev) { ev = ev || window.event; cryptoPRNG._collectEntropy((ev.keyCode) + (+new Date()), 3); }, getScrollCollector: function(ev) { ev = ev || window.event; cryptoPRNG._collectEntropy((ev.keyCode) + (+new Date()), 3); }, getRequestsCollector: function(ev) { ev = ev || window.event; cryptoPRNG._collectEntropy((ev.keyCode) + (+new Date()), 3); } }; })();