UNPKG

sjcl-aws

Version:

Stanford Javascript Crypto Library

525 lines (449 loc) 16.9 kB
/** @fileOverview Random number generator. * * @author Emily Stark * @author Mike Hamburg * @author Dan Boneh * @author Michael Brooks * @author Steve Thomas */ /** * @class Random number generator * @description * <b>Use sjcl.random as a singleton for this class!</b> * <p> * This random number generator is a derivative of Ferguson and Schneier's * generator Fortuna. It collects entropy from various events into several * pools, implemented by streaming SHA-256 instances. It differs from * ordinary Fortuna in a few ways, though. * </p> * * <p> * Most importantly, it has an entropy estimator. This is present because * there is a strong conflict here between making the generator available * as soon as possible, and making sure that it doesn't "run on empty". * In Fortuna, there is a saved state file, and the system is likely to have * time to warm up. * </p> * * <p> * Second, because users are unlikely to stay on the page for very long, * and to speed startup time, the number of pools increases logarithmically: * a new pool is created when the previous one is actually used for a reseed. * This gives the same asymptotic guarantees as Fortuna, but gives more * entropy to early reseeds. * </p> * * <p> * The entire mechanism here feels pretty klunky. Furthermore, there are * several improvements that should be made, including support for * dedicated cryptographic functions that may be present in some browsers; * state files in local storage; cookies containing randomness; etc. So * look for improvements in future versions. * </p> * @constructor */ sjcl.prng = function(defaultParanoia) { /* private */ this._pools = [new sjcl.hash.sha256()]; this._poolEntropy = [0]; this._reseedCount = 0; this._robins = {}; this._eventId = 0; this._collectorIds = {}; this._collectorIdNext = 0; this._strength = 0; this._poolStrength = 0; this._nextReseed = 0; this._key = [0,0,0,0,0,0,0,0]; this._counter = [0,0,0,0]; this._cipher = undefined; this._defaultParanoia = defaultParanoia; /* event listener stuff */ this._collectorsStarted = false; this._callbacks = {progress: {}, seeded: {}}; this._callbackI = 0; /* constants */ this._NOT_READY = 0; this._READY = 1; this._REQUIRES_RESEED = 2; this._MAX_WORDS_PER_BURST = 65536; this._PARANOIA_LEVELS = [0,48,64,96,128,192,256,384,512,768,1024]; this._MILLISECONDS_PER_RESEED = 30000; this._BITS_PER_RESEED = 80; }; sjcl.prng.prototype = { /** Generate several random words, and return them in an array. * A word consists of 32 bits (4 bytes) * @param {Number} nwords The number of words to generate. */ randomWords: function (nwords, paranoia) { var out = [], i, readiness = this.isReady(paranoia), g; if (readiness === this._NOT_READY) { throw new sjcl.exception.notReady("generator isn't seeded"); } else if (readiness & this._REQUIRES_RESEED) { this._reseedFromPools(!(readiness & this._READY)); } for (i=0; i<nwords; i+= 4) { if ((i+1) % this._MAX_WORDS_PER_BURST === 0) { this._gate(); } g = this._gen4words(); out.push(g[0],g[1],g[2],g[3]); } this._gate(); return out.slice(0,nwords); }, setDefaultParanoia: function (paranoia, allowZeroParanoia) { if (paranoia === 0 && allowZeroParanoia !== "Setting paranoia=0 will ruin your security; use it only for testing") { throw new sjcl.exception.invalid("Setting paranoia=0 will ruin your security; use it only for testing"); } this._defaultParanoia = paranoia; }, /** * Add entropy to the pools. * @param data The entropic value. Should be a 32-bit integer, array of 32-bit integers, or string * @param {Number} estimatedEntropy The estimated entropy of data, in bits * @param {String} source The source of the entropy, eg "mouse" */ addEntropy: function (data, estimatedEntropy, source) { source = source || "user"; var id, i, tmp, t = (new Date()).valueOf(), robin = this._robins[source], oldReady = this.isReady(), err = 0, objName; id = this._collectorIds[source]; if (id === undefined) { id = this._collectorIds[source] = this._collectorIdNext ++; } if (robin === undefined) { robin = this._robins[source] = 0; } this._robins[source] = ( this._robins[source] + 1 ) % this._pools.length; switch(typeof(data)) { case "number": if (estimatedEntropy === undefined) { estimatedEntropy = 1; } this._pools[robin].update([id,this._eventId++,1,estimatedEntropy,t,1,data|0]); break; case "object": objName = Object.prototype.toString.call(data); if (objName === "[object Uint32Array]") { tmp = []; for (i = 0; i < data.length; i++) { tmp.push(data[i]); } data = tmp; } else { if (objName !== "[object Array]") { err = 1; } for (i=0; i<data.length && !err; i++) { if (typeof(data[i]) !== "number") { err = 1; } } } if (!err) { if (estimatedEntropy === undefined) { /* horrible entropy estimator */ estimatedEntropy = 0; for (i=0; i<data.length; i++) { tmp= data[i]; while (tmp>0) { estimatedEntropy++; tmp = tmp >>> 1; } } } this._pools[robin].update([id,this._eventId++,2,estimatedEntropy,t,data.length].concat(data)); } break; case "string": if (estimatedEntropy === undefined) { /* English text has just over 1 bit per character of entropy. * But this might be HTML or something, and have far less * entropy than English... Oh well, let's just say one bit. */ estimatedEntropy = data.length; } this._pools[robin].update([id,this._eventId++,3,estimatedEntropy,t,data.length]); this._pools[robin].update(data); break; default: err=1; } if (err) { throw new sjcl.exception.bug("random: addEntropy only supports number, array of numbers or string"); } /* record the new strength */ this._poolEntropy[robin] += estimatedEntropy; this._poolStrength += estimatedEntropy; /* fire off events */ if (oldReady === this._NOT_READY) { if (this.isReady() !== this._NOT_READY) { this._fireEvent("seeded", Math.max(this._strength, this._poolStrength)); } this._fireEvent("progress", this.getProgress()); } }, /** Is the generator ready? */ isReady: function (paranoia) { var entropyRequired = this._PARANOIA_LEVELS[ (paranoia !== undefined) ? paranoia : this._defaultParanoia ]; if (this._strength && this._strength >= entropyRequired) { return (this._poolEntropy[0] > this._BITS_PER_RESEED && (new Date()).valueOf() > this._nextReseed) ? this._REQUIRES_RESEED | this._READY : this._READY; } else { return (this._poolStrength >= entropyRequired) ? this._REQUIRES_RESEED | this._NOT_READY : this._NOT_READY; } }, /** Get the generator's progress toward readiness, as a fraction */ getProgress: function (paranoia) { var entropyRequired = this._PARANOIA_LEVELS[ paranoia ? paranoia : this._defaultParanoia ]; if (this._strength >= entropyRequired) { return 1.0; } else { return (this._poolStrength > entropyRequired) ? 1.0 : this._poolStrength / entropyRequired; } }, /** start the built-in entropy collectors */ startCollectors: function () { if (this._collectorsStarted) { return; } this._eventListener = { loadTimeCollector: this._bind(this._loadTimeCollector), mouseCollector: this._bind(this._mouseCollector), keyboardCollector: this._bind(this._keyboardCollector), accelerometerCollector: this._bind(this._accelerometerCollector), touchCollector: this._bind(this._touchCollector) }; if (window.addEventListener) { window.addEventListener("load", this._eventListener.loadTimeCollector, false); window.addEventListener("mousemove", this._eventListener.mouseCollector, false); window.addEventListener("keypress", this._eventListener.keyboardCollector, false); window.addEventListener("devicemotion", this._eventListener.accelerometerCollector, false); window.addEventListener("touchmove", this._eventListener.touchCollector, false); } else if (document.attachEvent) { document.attachEvent("onload", this._eventListener.loadTimeCollector); document.attachEvent("onmousemove", this._eventListener.mouseCollector); document.attachEvent("keypress", this._eventListener.keyboardCollector); } else { throw new sjcl.exception.bug("can't attach event"); } this._collectorsStarted = true; }, /** stop the built-in entropy collectors */ stopCollectors: function () { if (!this._collectorsStarted) { return; } if (window.removeEventListener) { window.removeEventListener("load", this._eventListener.loadTimeCollector, false); window.removeEventListener("mousemove", this._eventListener.mouseCollector, false); window.removeEventListener("keypress", this._eventListener.keyboardCollector, false); window.removeEventListener("devicemotion", this._eventListener.accelerometerCollector, false); window.removeEventListener("touchmove", this._eventListener.touchCollector, false); } else if (document.detachEvent) { document.detachEvent("onload", this._eventListener.loadTimeCollector); document.detachEvent("onmousemove", this._eventListener.mouseCollector); document.detachEvent("keypress", this._eventListener.keyboardCollector); } this._collectorsStarted = false; }, /* use a cookie to store entropy. useCookie: function (all_cookies) { throw new sjcl.exception.bug("random: useCookie is unimplemented"); },*/ /** add an event listener for progress or seeded-ness. */ addEventListener: function (name, callback) { this._callbacks[name][this._callbackI++] = callback; }, /** remove an event listener for progress or seeded-ness */ removeEventListener: function (name, cb) { var i, j, cbs=this._callbacks[name], jsTemp=[]; /* I'm not sure if this is necessary; in C++, iterating over a * collection and modifying it at the same time is a no-no. */ for (j in cbs) { if (cbs.hasOwnProperty(j) && cbs[j] === cb) { jsTemp.push(j); } } for (i=0; i<jsTemp.length; i++) { j = jsTemp[i]; delete cbs[j]; } }, _bind: function (func) { var that = this; return function () { func.apply(that, arguments); }; }, /** Generate 4 random words, no reseed, no gate. * @private */ _gen4words: function () { for (var i=0; i<4; i++) { this._counter[i] = this._counter[i]+1 | 0; if (this._counter[i]) { break; } } return this._cipher.encrypt(this._counter); }, /* Rekey the AES instance with itself after a request, or every _MAX_WORDS_PER_BURST words. * @private */ _gate: function () { this._key = this._gen4words().concat(this._gen4words()); this._cipher = new sjcl.cipher.aes(this._key); }, /** Reseed the generator with the given words * @private */ _reseed: function (seedWords) { this._key = sjcl.hash.sha256.hash(this._key.concat(seedWords)); this._cipher = new sjcl.cipher.aes(this._key); for (var i=0; i<4; i++) { this._counter[i] = this._counter[i]+1 | 0; if (this._counter[i]) { break; } } }, /** reseed the data from the entropy pools * @param full If set, use all the entropy pools in the reseed. */ _reseedFromPools: function (full) { var reseedData = [], strength = 0, i; this._nextReseed = reseedData[0] = (new Date()).valueOf() + this._MILLISECONDS_PER_RESEED; for (i=0; i<16; i++) { /* On some browsers, this is cryptographically random. So we might * as well toss it in the pot and stir... */ reseedData.push(Math.random()*0x100000000|0); } for (i=0; i<this._pools.length; i++) { reseedData = reseedData.concat(this._pools[i].finalize()); strength += this._poolEntropy[i]; this._poolEntropy[i] = 0; if (!full && (this._reseedCount & (1<<i))) { break; } } /* if we used the last pool, push a new one onto the stack */ if (this._reseedCount >= 1 << this._pools.length) { this._pools.push(new sjcl.hash.sha256()); this._poolEntropy.push(0); } /* how strong was this reseed? */ this._poolStrength -= strength; if (strength > this._strength) { this._strength = strength; } this._reseedCount ++; this._reseed(reseedData); }, _keyboardCollector: function () { this._addCurrentTimeToEntropy(1); }, _mouseCollector: function (ev) { var x, y; try { x = ev.x || ev.clientX || ev.offsetX || 0; y = ev.y || ev.clientY || ev.offsetY || 0; } catch (err) { // Event originated from a secure element. No mouse position available. x = 0; y = 0; } if (x != 0 && y!= 0) { this.addEntropy([x,y], 2, "mouse"); } this._addCurrentTimeToEntropy(0); }, _touchCollector: function(ev) { var touch = ev.touches[0] || ev.changedTouches[0]; var x = touch.pageX || touch.clientX, y = touch.pageY || touch.clientY; this.addEntropy([x,y],1,"touch"); this._addCurrentTimeToEntropy(0); }, _loadTimeCollector: function () { this._addCurrentTimeToEntropy(2); }, _addCurrentTimeToEntropy: function (estimatedEntropy) { if (typeof window !== 'undefined' && window.performance && typeof window.performance.now === "function") { //how much entropy do we want to add here? this.addEntropy(window.performance.now(), estimatedEntropy, "loadtime"); } else { this.addEntropy((new Date()).valueOf(), estimatedEntropy, "loadtime"); } }, _accelerometerCollector: function (ev) { var ac = ev.accelerationIncludingGravity.x||ev.accelerationIncludingGravity.y||ev.accelerationIncludingGravity.z; if(window.orientation){ var or = window.orientation; if (typeof or === "number") { this.addEntropy(or, 1, "accelerometer"); } } if (ac) { this.addEntropy(ac, 2, "accelerometer"); } this._addCurrentTimeToEntropy(0); }, _fireEvent: function (name, arg) { var j, cbs=sjcl.random._callbacks[name], cbsTemp=[]; /* TODO: there is a race condition between removing collectors and firing them */ /* I'm not sure if this is necessary; in C++, iterating over a * collection and modifying it at the same time is a no-no. */ for (j in cbs) { if (cbs.hasOwnProperty(j)) { cbsTemp.push(cbs[j]); } } for (j=0; j<cbsTemp.length; j++) { cbsTemp[j](arg); } } }; /** an instance for the prng. * @see sjcl.prng */ sjcl.random = new sjcl.prng(6); (function(){ // function for getting nodejs crypto module. catches and ignores errors. function getCryptoModule() { try { return require('crypto'); } catch (e) { return null; } } try { var buf, crypt, ab; // get cryptographically strong entropy depending on runtime environment if (typeof module !== 'undefined' && module.exports && (crypt = getCryptoModule()) && crypt.randomBytes) { buf = crypt.randomBytes(1024/8); buf = new Uint32Array(new Uint8Array(buf).buffer); sjcl.random.addEntropy(buf, 1024, "crypto.randomBytes"); } else if (typeof window !== 'undefined' && typeof Uint32Array !== 'undefined') { ab = new Uint32Array(32); if (window.crypto && window.crypto.getRandomValues) { window.crypto.getRandomValues(ab); } else if (window.msCrypto && window.msCrypto.getRandomValues) { window.msCrypto.getRandomValues(ab); } else { return; } // get cryptographically strong entropy in Webkit sjcl.random.addEntropy(ab, 1024, "crypto.getRandomValues"); } else { // no getRandomValues :-( } } catch (e) { if (typeof window !== 'undefined' && window.console) { console.log("There was an error collecting entropy from the browser:"); console.log(e); //we do not want the library to fail due to randomness not being maintained. } } }());