'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 ) {
/ * T h i s d o e s t h e e q u i v a l e n t o f :
*
* 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 */
/ * S E C U R I T Y P A T C H ( M a r c h 8 , 2 0 1 6 ) :
* 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." )
}
/ * W e h a r d c o d e t h e v a l u e s f o r t h e f o l l o w i n g :
*
* 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 ) ) ;
}
/ * W e a p p l y t h e m a s k t o r e d u c e t h e a m o u n t o f a t t e m p t s w e m i g h t n e e d
* 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 ) {
/ * W e ' v e b e e n w o r k i n g w i t h 0 a s a s t a r t i n g p o i n t , s o w e n e e d t o
* add the ` minimum ` here . * /
return minimum + randomValue ;
} else {
/ * O u t s i d e o f t h e a c c e p t a b l e r a n g e , t h r o w i t a w a y a n d t r y a g a i n .
* We don ' t try any modulo tricks , as this would introduce bias . * /
return secureRandomNumber ( minimum , maximum ) ;
}
} ) ;
} ) . nodeify ( cb ) ;
}
module . exports . RandomGenerationError = RandomGenerationError ;