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