22

How can I generate random numbers in a specific range using crypto.randomBytes?

I want to be able to generate a random number like this:

console.log(random(55, 956)); // where 55 is minimum and 956 is maximum

and I'm limited to use crypto.randomBytes only inside random function to generate random number for this range.

I know how to convert generated bytes from randomBytes to hex or decimal but I can't figure out how to get a random number in a specific range from random bytes mathematically.

3
  • Should the output be an integer, or a float? Commented Nov 10, 2015 at 9:08
  • @CodesInChaos integer. Commented Nov 10, 2015 at 11:20
  • Related: github.com/paragonie/random_compat It's PHP, but due to PHP's treatment of integers and floats in ran into pretty similar issues you'd run into if you wanted to properly implement this. Commented Nov 10, 2015 at 13:04

5 Answers 5

32

To generate random number in a certain range you can use the following equation

Math.random() * (high - low) + low

But you want to use crypto.randomBytes instead of Math.random() this function returns a buffer with randomly generated bytes. In turn, you need to convert the result of this function from bytes to decimal. this can be done using biguint-format package. To install this package simply use the following command:

npm install biguint-format --save

Now you need to convert the result of crypto.randomBytes to decimal, you can do that as follow:

var x= crypto.randomBytes(1);
return format(x, 'dec');

Now you can create your random function which will be as follow:

var crypto = require('crypto'),
    format = require('biguint-format');

function randomC (qty) {
    var x= crypto.randomBytes(qty);
    return format(x, 'dec');
}
function random (low, high) {
    return randomC(4)/Math.pow(2,4*8-1) * (high - low) + low;
}
console.log(random(50,1000));
Sign up to request clarification or add additional context in comments.

12 Comments

@CodesInChaos: your point is valid, but note that the OP only said he wanted to use crypto.randomBytes, not that he wanted the result to be used in cryptography. Also, what is the 'censor script' you're referring to?
@Mustafamg thank you! This is exactly what I wanted. You should add Math.round front of randomC and that's it. return Math.round(randomC(1)/256 * (high - low) + low);
@Mustafamg also if you generate only few bytes, there is no need to use biguint-format npm. Instead of format(x, 'dec') we can use parseInt(x.toString('hex'), 16) and avoid adding additional module.
@Mustafamg and one more thing... you can't get all available numbers between 50 and 1000 from only one byte. randomC(1)/256 is wrong if high - low > 256. You should use randomC(2)/65536 if high - low <= 65536 and randomC(3)/16777216 if high - low <= 16777216 and etc... You can't get all possible numbers from range if high - low is more than decimal value of generated bytes.
@Mustafamg 1) Different outputs are chosen with different probability, i.e. they're biased. If the range is longer than 256, many possible results won't be chosen at all. 2) The max will be among the impossible numbers if the range is big, since you divide by 256 not 255. 3) Stackexchange prevents posting comments starting with -1, because they apparently prefer people downvoting without giving a reason.
|
9

The crypto package now has a randomInt() function. It was added in v14.10.0 and v12.19.0.

console.log(crypto.randomInt(55, 957)); // where 55 is minimum and 956 is maximum

The upper bound is exclusive.

Here is the (abridged) implementation:

// Largest integer we can read from a buffer.
// e.g.: Buffer.from("ff".repeat(6), "hex").readUIntBE(0, 6);
const RAND_MAX = 0xFFFF_FFFF_FFFF;

const range = max - min;

const excess = RAND_MAX % range;
const randLimit = RAND_MAX - excess;

while (true) {
  const x = randomBytes(6).readUIntBE(0, 6);
  // If x > (maxVal - (maxVal % range)), we will get "modulo bias"
  if (x > randLimit) {
    // Try again
    continue;
  }
  const n = (x % range) + min;
  return n;
}

See the full source and the official docs for more information.

Comments

5

Thanks to answer from @Mustafamg and huge help from @CodesInChaos I managed to resolve this issue. I made some tweaks and increase range to maximum 256^6-1 or 281,474,976,710,655. Range can be increased more but you need to use additional library for big integers, because 256^7-1 is out of Number.MAX_SAFE_INTEGER limits.

If anyone have same problem feel free to use it.

var crypto = require('crypto');

/*
Generating random numbers in specific range using crypto.randomBytes from crypto library
Maximum available range is 281474976710655 or 256^6-1
Maximum number for range must be equal or less than Number.MAX_SAFE_INTEGER (usually 9007199254740991)
Usage examples:
cryptoRandomNumber(0, 350);
cryptoRandomNumber(556, 1250425);
cryptoRandomNumber(0, 281474976710655);
cryptoRandomNumber((Number.MAX_SAFE_INTEGER-281474976710655), Number.MAX_SAFE_INTEGER);

Tested and working on 64bit Windows and Unix operation systems.
*/

function cryptoRandomNumber(minimum, maximum){
	var distance = maximum-minimum;
	
	if(minimum>=maximum){
		console.log('Minimum number should be less than maximum');
		return false;
	} else if(distance>281474976710655){
		console.log('You can not get all possible random numbers if range is greater than 256^6-1');
		return false;
	} else if(maximum>Number.MAX_SAFE_INTEGER){
		console.log('Maximum number should be safe integer limit');
		return false;
	} else {
		var maxBytes = 6;
		var maxDec = 281474976710656;
		
		// To avoid huge mathematical operations and increase function performance for small ranges, you can uncomment following script
		/*
		if(distance<256){
			maxBytes = 1;
			maxDec = 256;
		} else if(distance<65536){
			maxBytes = 2;
			maxDec = 65536;
		} else if(distance<16777216){
			maxBytes = 3;
			maxDec = 16777216;
		} else if(distance<4294967296){
			maxBytes = 4;
			maxDec = 4294967296;
		} else if(distance<1099511627776){
			maxBytes = 4;
			maxDec = 1099511627776;
		}
		*/
		
		var randbytes = parseInt(crypto.randomBytes(maxBytes).toString('hex'), 16);
		var result = Math.floor(randbytes/maxDec*(maximum-minimum+1)+minimum);
		
		if(result>maximum){
			result = maximum;
		}
		return result;
	}
}

So far it works fine and you can use it as really good random number generator, but I strictly not recommending using this function for any cryptographic services. If you will, use it on your own risk.

All comments, recommendations and critics are welcome!

8 Comments

Try cryptoRandomNumber(-1,+1) and check of common each result is. I expect it to be 1/4 | 1/2 | 1/4 instead of the uniform 1/3 you might expect.
@CodesInChaos Thanks for suggestion, you are right. This is results from 1 million request: -1x249836, 0x501146, 1x249018. And this is for all combinations I tried, not only from -1 to 1 range :( Any idea how can I fix it?
If you multiply with max-min+1 and truncate the result instead of rounding it would be correct mathematically, but ensuring that floating point rounding doesn't sometimes (very rarely) return max+1 might be a bit annoying. Since your code doesn't use the full 53 bits of a double, that shouldn't be possible, but personally I'd rather write the code using integer arithmetic only.
@CodesInChaos Thank you so much! It works! After few millions of test requests I never had any issue with return max+1, but I added extra code to return false if it happens and I can detect and try again. All potential numbers are ~equally common now.
I'd rather add something like if(result>max) result=max instead of forcing the caller to handle it.
|
4

To generate numbers in the range [55 .. 956], you first generate a random number in the range [0 .. 901] where 901 = 956 - 55. Then add 55 to the number you just generated.

To generate a number in the range [0 .. 901], pick off two random bytes and mask off 6 bits. That will give you a 10 bit random number in the range [0 .. 1023]. If that number is <= 901 then you are finished. If it is bigger than 901, discard it and get two more random bytes. Do not attempt to use MOD, to get the number into the right range, that will distort the output making it non-random.

ETA: To reduce the chance of having to discard a generated number.

Since we are taking two bytes from the RNG, we get a number in the range [0 .. 65535]. Now 65535 MOD 902 is 591. Hence, if our two-byte random number is less than (65535 - 591), that is, less than 64944, we can safely use the MOD operator, since each number in the range [0 .. 901] is now equally likely. Any two-byte number >= 64944 will still have to be thrown away, as using it would distort the output away from random. Before, the chances of having to reject a number were (1024 - 901) / 1024 = 12%. Now the chances of a rejection are (65535 - 64944) / 65535 = 1%. We are far less likely to have to reject the randomly generated number.

running <- true
while running
  num <- two byte random
  if (num < 64944)
    result <- num MOD 902
    running <- false
  endif
endwhile
return result + 55

7 Comments

just adding that to convert bytes to an integer, you could check the answer given in stackoverflow.com/questions/15821447/…
Do you mean the modulo operation by "MOD"?
Yes, don't use it unless you know exactly what you are doing. It is easy to get a non-random result after a MOD.
@rossum Thanks for answer, but this method is not ready for production level script. There is always probability to get number more than 901 few times in a row... Well, according to probability theory it's even possible to get number more than 901, million or more times in a row. I know it's unlikely but even if it's 3 times in a row it can affect server performance in long term.
Have you tried running it in a test environment and seen how often the problem actually happens and the actual impact it has? Premature optimisation can cause more problems than it solves.
|
1

So the issue with most other solutions are that they distort the distribution (which you probably would like to be uniform).

The pseudocode from @rossum lacks generalization. (But he proposed the right solution in the text)

// Generates a random integer in range [min, max]
function randomRange(min, max) {
    const diff = max - min + 1;

    // finds the minimum number of bit required to represent the diff
    const numberBit = Math.ceil(Math.log2(diff));
    // as we are limited to draw bytes, minimum number of bytes
    const numberBytes = Math.ceil(numberBit / 4);

    // as we might draw more bits than required, we look only at what we need (discard the rest)
    const mask = (1 << numberBit) - 1;

    let randomNumber;

    do {
        randomNumber = crypto.randomBytes(numberBytes).readUIntBE(0, numberBytes);
        randomNumber = randomNumber & mask;
    // number of bit might represent a numbers bigger than the diff, in that case try again
    } while (randomNumber >= diff);

    return randomNumber + min;
}

About performance concerns, basically the number is in the right range between 50% - 100% of the time (depending on the parameters). That is in the worst case scenario the loop is executed more than 7 times with less than 1% chance and practically, most of the time the loop is executed one or two times.

The random-js library acknowledges that most solution out there don't provide random numbers with uniform distributions and provides a more complete solution

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.