From 2413a8d3ed5ffb10e7deb20c3e6292b96e4a2a0e Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Tue, 24 Sep 2013 11:35:12 -0700 Subject: [PATCH] Use cryptographic PRNGs when available. This means node's crypto.randomBytes on the server, and window.crypto.getRandomValues on the client. If node's crypto.randomBytes throws an exception, we fall back to crypto.pseudoRandomBytes. If window.crypto.getRandomValues isn't supported by the browser, we fall back to the alea generator that we had been using previously. --- docs/client/packages/random.html | 12 +-- packages/random/random.js | 144 ++++++++++++++++--------- packages/random/random_tests.js | 14 +++ packages/spark/spark_tests.js | 2 +- packages/test-helpers/seeded_random.js | 2 +- 5 files changed, 113 insertions(+), 61 deletions(-) diff --git a/docs/client/packages/random.html b/docs/client/packages/random.html index 8f0490613d..72c7ce9f51 100644 --- a/docs/client/packages/random.html +++ b/docs/client/packages/random.html @@ -3,8 +3,11 @@ ## `random` The `random` package provides several functions for generating random -numbers. It uses a Meteor-provided random number generator that does not depend -on the browser's facilities. +numbers. It uses a cryptographically strong pseudorandom number generator when +possible, but falls back to a weaker random number generator when +cryptographically strong randomness is not available (on older browsers or on +servers that don't have enough entropy to seed the cryptographically strong +generator).
{{#dtdd "Random.id()"}} @@ -25,10 +28,5 @@ Returns a random string of `n` hexadecimal digits. {{/dtdd}}
-{{#note}} -In the current implementation, random values do not come from a -cryptographically strong pseudorandom number generator. Future releases will -improve this, particularly on the server. -{{/note}} {{/better_markdown}} diff --git a/packages/random/random.js b/packages/random/random.js index 5faddb9d4c..59e94683e8 100644 --- a/packages/random/random.js +++ b/packages/random/random.js @@ -1,3 +1,15 @@ +// We use cryptographically strong PRNGs (crypto.getRandomBytes() on the server, +// window.crypto.getRandomValues() in the browser) when available. If these +// PRNGs fail, we fall back to the Alea PRNG, which is not cryptographically +// strong, and we seed it with various sources such as the date, Math.random, +// and window size on the client. When using crypto.getRandomValues(), our +// primitive is hexString(), from which we construct fraction(). When using +// window.crypto.getRandomValues() or alea, the primitive is fraction and we use +// that to construct hex string. + +if (Meteor.isServer) + var nodeCrypto = Npm.require('crypto'); + // see http://baagoe.org/en/wiki/Better_random_numbers_for_javascript // for a full discussion and Alea implementation. var Alea = function () { @@ -75,52 +87,79 @@ var Alea = function () { var UNMISTAKABLE_CHARS = "23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz"; -var create = function (/* arguments */) { - - var random = Alea.apply(null, arguments); - - var self = {}; - - var bind = function (fn) { - return _.bind(fn, self); - }; - - return _.extend(self, { - _Alea: Alea, - - create: create, - - fraction: random, - - choice: bind(function (arrayOrString) { - var index = Math.floor(this.fraction() * arrayOrString.length); - if (typeof arrayOrString === "string") - return arrayOrString.substr(index, 1); - else - return arrayOrString[index]; - }), - - id: bind(function() { - var digits = []; - // Length of 17 preserves around 96 bits of entropy, which is the - // amount of state in our PRNG - for (var i = 0; i < 17; i++) { - digits[i] = this.choice(UNMISTAKABLE_CHARS); - } - return digits.join(""); - }), - - hexString: bind(function (digits) { - var hexDigits = []; - for (var i = 0; i < digits; ++i) { - hexDigits.push(this.choice("0123456789abcdef")); - } - return hexDigits.join(''); - }) - }); +// If seeds are provided, then the alea PRNG will be used, since cryptographic +// PRNGs (Node crypto and window.crypto.getRandomValues) don't allow us to +// specify seeds. The caller is responsible for making sure to provide a seed +// for alea if a csprng is not available. +var RandomGenerator = function (seedArray) { + var self = this; + if (seedArray !== undefined) + self.alea = Alea.apply(null, seedArray); + self._Alea = Alea; }; -// instantiate RNG. Heuristically collect entropy from various sources +RandomGenerator.prototype.fraction = function () { + var self = this; + if (self.alea) { + return self.alea(); + } else if (nodeCrypto) { + var numerator = parseInt(self.hexString(8), 16); + return numerator * 2.3283064365386963e-10; // 2^-32 + } else if (typeof window !== "undefined" && window.crypto && + window.crypto.getRandomValues) { + var array = new Uint32Array(1); + window.crypto.getRandomValues(array); + return array[0] * 2.3283064365386963e-10; // 2^-32 + } +}; + +RandomGenerator.prototype.hexString = function (digits) { + var self = this; + if (nodeCrypto && ! self.alea) { + var numBytes = Math.ceil(digits / 2); + var bytes; + // Try to get cryptographically strong randomness. Fall back to + // non-cryptographically strong if not available. + try { + bytes = nodeCrypto.randomBytes(numBytes); + } catch (e) { + // XXX should re-throw any error except insufficient entropy + bytes = nodeCrypto.pseudoRandomBytes(numBytes); + } + var result = bytes.toString("hex"); + // If the number of digits is odd, we'll have generated an extra 4 bits + // of randomness, so we need to trim the last digit. + return result.substring(0, digits); + } else { + var hexDigits = []; + for (var i = 0; i < digits; ++i) { + hexDigits.push(self.choice("0123456789abcdef")); + } + return hexDigits.join(''); + } +}; + +RandomGenerator.prototype.id = function () { + var digits = []; + var self = this; + // Length of 17 preserves around 96 bits of entropy, which is the + // amount of state in the Alea PRNG. + for (var i = 0; i < 17; i++) { + digits[i] = self.choice(UNMISTAKABLE_CHARS); + } + return digits.join(""); +}; + +RandomGenerator.prototype.choice = function (arrayOrString) { + var index = Math.floor(this.fraction() * arrayOrString.length); + if (typeof arrayOrString === "string") + return arrayOrString.substr(index, 1); + else + return arrayOrString[index]; +}; + +// instantiate RNG. Heuristically collect entropy from various sources when a +// cryptographic PRNG isn't available. // client sources var height = (typeof window !== 'undefined' && window.innerHeight) || @@ -143,12 +182,13 @@ var width = (typeof window !== 'undefined' && window.innerWidth) || var agent = (typeof navigator !== 'undefined' && navigator.userAgent) || ""; -// server sources -var pid = (typeof process !== 'undefined' && process.pid) || 1; +if (nodeCrypto || + (typeof window !== "undefined" && + window.crypto && window.crypto.getRandomValues)) + Random = new RandomGenerator(); +else + Random = new RandomGenerator([new Date(), height, width, agent, Math.random()]); -// XXX On the server, use the crypto module (OpenSSL) instead of this PRNG. -// (Make Random.fraction be generated from Random.hexString instead of the -// other way around, and generate Random.hexString from crypto.randomBytes.) -Random = create([ - new Date(), height, width, agent, pid, Math.random() -]); +Random.create = function () { + return new RandomGenerator(arguments); +}; diff --git a/packages/random/random_tests.js b/packages/random/random_tests.js index 52e5b852e5..940afbb640 100644 --- a/packages/random/random_tests.js +++ b/packages/random/random_tests.js @@ -13,3 +13,17 @@ Tinytest.add('random', function (test) { test.equal(random.id(), "shxDnjWWmnKPEoLhM"); test.equal(random.id(), "6QTjB8C5SEqhmz4ni"); }); + +// node crypto and window.crypto.getRandomValues() don't let us specify a seed, +// but at least test that the output is in the right format. +Tinytest.add('random - format', function (test) { + var idLen = 17; + test.equal(Random.id().length, idLen); + var numDigits = 9; + var hexStr = Random.hexString(numDigits); + test.equal(hexStr.length, numDigits); + parseInt(hexStr, 16); // should not throw + var frac = Random.fraction(); + test.isTrue(frac < 1.0); + test.isTrue(frac >= 0.0); +}); diff --git a/packages/spark/spark_tests.js b/packages/spark/spark_tests.js index cab0f7ea94..e543ade932 100644 --- a/packages/spark/spark_tests.js +++ b/packages/spark/spark_tests.js @@ -1876,7 +1876,7 @@ Tinytest.add("spark - leaderboard, " + idGeneration, function(test) { })); var idGen; if (idGeneration === 'STRING') - idGen = Random.id; + idGen = _.bind(Random.id, Random); else idGen = function () { return new LocalCollection._ObjectID(); }; diff --git a/packages/test-helpers/seeded_random.js b/packages/test-helpers/seeded_random.js index 0094a20258..171a970486 100644 --- a/packages/test-helpers/seeded_random.js +++ b/packages/test-helpers/seeded_random.js @@ -3,7 +3,7 @@ SeededRandom = function(seed) { // seed may be a string or any type return new SeededRandom(seed); seed = seed || "seed"; - this.gen = new Random._Alea(seed); // from random.js + this.gen = Random.create(seed)._Alea; // from random.js }; SeededRandom.prototype.next = function() { return this.gen();