You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

141 lines
4.9 KiB
JavaScript

'use strict';
const Promise = require("bluebird");
const crypto = Promise.promisifyAll(require("crypto"));
const createError = require("create-error");
const RandomGenerationError = createError("RandomGenerationError", {
code: "RandomGenerationError"
});
function calculateParameters(range) {
/* This does the equivalent of:
*
* bitsNeeded = Math.ceil(Math.log2(range));
* bytesNeeded = Math.ceil(bitsNeeded / 8);
* mask = Math.pow(2, bitsNeeded) - 1;
*
* ... however, it implements it as bitwise operations, to sidestep any
* possible implementation errors regarding floating point numbers in
* JavaScript runtimes. This is an easier solution than assessing each
* runtime and architecture individually.
*/
let bitsNeeded = 0;
let bytesNeeded = 0;
let mask = 1;
while (range > 0) {
if (bitsNeeded % 8 === 0) {
bytesNeeded += 1;
}
bitsNeeded += 1;
mask = mask << 1 | 1; /* 0x00001111 -> 0x00011111 */
/* SECURITY PATCH (March 8, 2016):
* As it turns out, `>>` is not the right operator to use here, and
* using it would cause strange outputs, that wouldn't fall into
* the specified range. This was remedied by switching to `>>>`
* instead, and adding checks for input parameters being within the
* range of 'safe integers' in JavaScript.
*/
range = range >>> 1; /* 0x01000000 -> 0x00100000 */
}
return {bitsNeeded, bytesNeeded, mask};
}
module.exports = function secureRandomNumber(minimum, maximum, cb) {
return Promise.try(() => {
if (crypto == null || crypto.randomBytesAsync == null) {
throw new RandomGenerationError("No suitable random number generator available. Ensure that your runtime is linked against OpenSSL (or an equivalent) correctly.");
}
if (minimum == null) {
throw new RandomGenerationError("You must specify a minimum value.");
}
if (maximum == null) {
throw new RandomGenerationError("You must specify a maximum value.");
}
if (minimum % 1 !== 0) {
throw new RandomGenerationError("The minimum value must be an integer.");
}
if (maximum % 1 !== 0) {
throw new RandomGenerationError("The maximum value must be an integer.");
}
if (!(maximum > minimum)) {
throw new RandomGenerationError("The maximum value must be higher than the minimum value.")
}
/* We hardcode the values for the following:
*
* https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Number/MIN_SAFE_INTEGER
* https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER
*
* ... as Babel does not appear to transpile them, despite being ES6.
*/
if (minimum < -9007199254740991 || minimum > 9007199254740991) {
throw new RandomGenerationError("The minimum value must be inbetween MIN_SAFE_INTEGER and MAX_SAFE_INTEGER.");
}
if (maximum < -9007199254740991 || maximum > 9007199254740991) {
throw new RandomGenerationError("The maximum value must be inbetween MIN_SAFE_INTEGER and MAX_SAFE_INTEGER.");
}
let range = maximum - minimum;
if (range < -9007199254740991 || range > 9007199254740991) {
throw new RandomGenerationError("The range between the minimum and maximum value must be inbetween MIN_SAFE_INTEGER and MAX_SAFE_INTEGER.");
}
let {bitsNeeded, bytesNeeded, mask} = calculateParameters(range);
return Promise.try(() => {
return crypto.randomBytesAsync(bytesNeeded);
}).then((randomBytes) => {
var randomValue = 0;
/* Turn the random bytes into an integer, using bitwise operations. */
for (let i = 0; i < bytesNeeded; i++) {
randomValue |= (randomBytes[i] << (8 * i));
}
/* We apply the mask to reduce the amount of attempts we might need
* to make to get a number that is in range. This is somewhat like
* the commonly used 'modulo trick', but without the bias:
*
* "Let's say you invoke secure_rand(0, 60). When the other code
* generates a random integer, you might get 243. If you take
* (243 & 63)-- noting that the mask is 63-- you get 51. Since
* 51 is less than 60, we can return this without bias. If we
* got 255, then 255 & 63 is 63. 63 > 60, so we try again.
*
* The purpose of the mask is to reduce the number of random
* numbers discarded for the sake of ensuring an unbiased
* distribution. In the example above, 243 would discard, but
* (243 & 63) is in the range of 0 and 60."
*
* (Source: Scott Arciszewski)
*/
randomValue = randomValue & mask;
if (randomValue <= range) {
/* We've been working with 0 as a starting point, so we need to
* add the `minimum` here. */
return minimum + randomValue;
} else {
/* Outside of the acceptable range, throw it away and try again.
* We don't try any modulo tricks, as this would introduce bias. */
return secureRandomNumber(minimum, maximum);
}
});
}).nodeify(cb);
}
module.exports.RandomGenerationError = RandomGenerationError;