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.
116 lines
3.8 KiB
JavaScript
116 lines
3.8 KiB
JavaScript
8 years ago
|
'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 */
|
||
|
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.")
|
||
|
}
|
||
|
|
||
|
let range = maximum - minimum;
|
||
|
let {bitsNeeded, bytesNeeded, mask} = calculateParameters(range);
|
||
|
|
||
|
if (bitsNeeded > 53) {
|
||
|
throw new RandomGenerationError("Cannot generate numbers larger than 53 bits.");
|
||
|
}
|
||
|
|
||
|
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;
|