| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391 |
- /** @fileOverview Random number generator.
- *
- * @author Emily Stark
- * @author Mike Hamburg
- * @author Dan Boneh
- */
- /** @namespace Random number generator
- *
- * @description
- * <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>
- */
- sjcl.random = {
- /** Generate several random words, and return them in an array
- * @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) {
- 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;
-
- 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":
- if (Object.prototype.toString.call(data) !== "[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; }
-
- if (window.addEventListener) {
- window.addEventListener("load", this._loadTimeCollector, false);
- window.addEventListener("mousemove", this._mouseCollector, false);
- } else if (document.attachEvent) {
- document.attachEvent("onload", this._loadTimeCollector);
- document.attachEvent("onmousemove", this._mouseCollector);
- }
- 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._loadTimeCollector, false);
- window.removeEventListener("mousemove", this._mouseCollector, false);
- } else if (window.detachEvent) {
- window.detachEvent("onload", this._loadTimeCollector);
- window.detachEvent("onmousemove", this._mouseCollector);
- }
- 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];
- }
- },
-
- /* private */
- _pools : [new sjcl.hash.sha256()],
- _poolEntropy : [0],
- _reseedCount : 0,
- _robins : {},
- _eventId : 0,
-
- _collectorIds : {},
- _collectorIdNext : 0,
-
- _strength : 0,
- _poolStrength : 0,
- _nextReseed : 0,
- _key : [0,0,0,0,0,0,0,0],
- _counter : [0,0,0,0],
- _cipher : undefined,
- _defaultParanoia : 6,
-
- /* event listener stuff */
- _collectorsStarted : false,
- _callbacks : {progress: {}, seeded: {}},
- _callbackI : 0,
-
- /* constants */
- _NOT_READY : 0,
- _READY : 1,
- _REQUIRES_RESEED : 2,
- _MAX_WORDS_PER_BURST : 65536,
- _PARANOIA_LEVELS : [0,48,64,96,128,192,256,384,512,768,1024],
- _MILLISECONDS_PER_RESEED : 30000,
- _BITS_PER_RESEED : 80,
-
- /** 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);
- },
-
- _mouseCollector: function (ev) {
- var x = ev.x || ev.clientX || ev.offsetX, y = ev.y || ev.clientY || ev.offsetY;
- sjcl.random.addEntropy([x,y], 2, "mouse");
- },
-
- _loadTimeCollector: function (ev) {
- sjcl.random.addEntropy((new Date()).valueOf(), 2, "loadtime");
- },
-
- _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);
- }
- }
- };
- (function(){
- try {
- // get cryptographically strong entropy in Webkit
- var ab = new Uint32Array(32);
- crypto.getRandomValues(ab);
- sjcl.random.addEntropy(ab, 1024, "crypto.getRandomValues");
- } catch (e) {
- // no getRandomValues :-(
- }
- })();
|