mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge branch 'csprng' into devel
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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(); };
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user