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();