Merge branch 'csprng' into devel

This commit is contained in:
Emily Stark
2013-09-25 15:55:45 -07:00
5 changed files with 113 additions and 61 deletions

View File

@@ -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).
<dl class="callbacks">
{{#dtdd "Random.id()"}}
@@ -25,10 +28,5 @@ Returns a random string of `n` hexadecimal digits.
{{/dtdd}}
</dl>
{{#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}}
</template>

View File

@@ -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);
};

View File

@@ -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);
});

View File

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

View File

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