37

I understand you can generate a random number in JavaScript within a range using this function:

function getRandomInt (min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

Courtesy of Ionuț G. Stan here.

What I want to know is if you can generate a better random number in a range using crypto.getRandomValues() instead of Math.random(). I would like to be able to generate a number between 0 and 10 inclusive, or 0 - 1, or even 10 - 5000 inclusive.

You'll note Math.random() produces a number like: 0.8565239671015732.

The getRandomValues API might return something like:

  • 231 with Uint8Array(1)
  • 54328 with Uint16Array(1)
  • 355282741 with Uint32Array(1).

So how to translate that back to a decimal number so I can keep with the same range algorithm above? Or do I need a new algorithm?

Here's the code I tried but it doesn't work too well.

function getRandomInt(min, max) {       
    // Create byte array and fill with 1 random number
    var byteArray = new Uint8Array(1);
    window.crypto.getRandomValues(byteArray);

    // Convert to decimal
    var randomNum = '0.' + byteArray[0].toString();

    // Get number in range
    randomNum = Math.floor(randomNum * (max - min + 1)) + min;

    return randomNum;
}

At the low end (range 0 - 1) it returns more 0's than 1's. What's the best way to do it with getRandomValues()?

Many thanks

7 Answers 7

35

IMHO, the easiest way to generate a random number in a [min..max] range with window.crypto.getRandomValues() is described here.

An ECMAScript 2015-syntax code, in case the link is TL;TR:

function getRandomIntInclusive(min, max) {
    const randomBuffer = new Uint32Array(1);

    window.crypto.getRandomValues(randomBuffer);

    let randomNumber = randomBuffer[0] / (0xffffffff + 1);

    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(randomNumber * (max - min + 1)) + min;
}
Sign up to request clarification or add additional context in comments.

2 Comments

0xFFFFFFFF = uint32.MaxValue (+1 because Math.random is inclusive of 0, but not 1)
FYI: This implementation introduced a small bias. Probably negligible for non-cryptographic uses, but better alternatives exist.
26

The easiest way is probably by rejection sampling (see http://en.wikipedia.org/wiki/Rejection_sampling). For example, assuming that max - min is less than 256:

function getRandomInt(min, max) {       
    // Create byte array and fill with 1 random number
    var byteArray = new Uint8Array(1);
    window.crypto.getRandomValues(byteArray);

    var range = max - min + 1;
    var max_range = 256;
    if (byteArray[0] >= Math.floor(max_range / range) * range)
        return getRandomInt(min, max);
    return min + (byteArray[0] % range);
}

3 Comments

In the discussion here github.com/EFForg/OpenWireless/pull/195 a more general (int > 256) solution was devised. I've taken the liberty of editing the OP with this suggested revision. :)
See Diceware.prototype.random in github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js specifically.
Always throw the error RangeError: Maximum call stack size exceeded
10

Many of these answers are going to produce biased results.

Update 2025

Rejection sampling using safe integers (53-bit range)

This version is very fast but limited to a range (max - min) of 2^53-1, which is still better than the range of 2^48-1 supported by crypto.randomInt in Node.js.

const MAX_RANGE_SIZE = 2 ** 53
const buffer = new Uint32Array(1024)
let offset = buffer.length

/**
 * Returns a cryptographically secure random integer between min and max, inclusive.
 *
 * @param {number} min - the lowest integer in the desired range (inclusive)
 * @param {number} max - the highest integer in the desired range (inclusive)
 * @returns {number} Random number
 */

export function randomInt(min: number, max: number): number {
    if (!(Number.isSafeInteger(min) && Number.isSafeInteger(max))) {
        throw Error("min and max must be safe integers")
    }
    if (min > max) {
        throw Error("min must be less than or equal to max")
    }
    const rangeSize = max - min + 1
    if (rangeSize > MAX_RANGE_SIZE) {
        throw Error("(max - min) must be <= Number.MAX_SAFE_INTEGER")
    }
    const rejectionThreshold = MAX_RANGE_SIZE - (MAX_RANGE_SIZE % rangeSize)
    let result: number
    do {
        if ((offset + 1) >= buffer.length) {
            crypto.getRandomValues(buffer)
            offset = 0
        }
        result = (buffer[offset++] & 0x1f_ffff) * 0x1_0000_0000 + buffer[offset++]
    } while (result >= rejectionThreshold)
    return min + result % rangeSize
}

Rejection sampling using BigInt (64-bit range)

This version is slightly slower due to its use of bigints, but this relaxes the preconditions which previously restricted the valid range.

const MAX_RANGE_SIZE = 2n ** 64n
const buffer = new BigUint64Array(512)
let offset = buffer.length

/**
 * Returns a cryptographically secure random integer between min and max, inclusive.
 *
 * @param {number} min - the lowest integer in the desired range (inclusive)
 * @param {number} max - the highest integer in the desired range (inclusive)
 * @returns {number} Random number
 */

export function randomInt(min: number, max: number): number {
    if (!(Number.isSafeInteger(min) && Number.isSafeInteger(max))) {
        throw Error("min and max must be safe integers")
    }
    if (min > max) {
        throw Error("min must be less than or equal to max")
    }
    const bmin = BigInt(min)
    const rangeSize = BigInt(max) - bmin + 1n
    const rejectionThreshold = MAX_RANGE_SIZE - (MAX_RANGE_SIZE % rangeSize)
    let result: bigint
    do {
        if (offset >= buffer.length) {
            crypto.getRandomValues(buffer)
            offset = 0
        }
        result = buffer[offset++]
    } while (result >= rejectionThreshold)
    return Number(bmin + result % rangeSize)
}

Rejection sampling using fully BigInt, instance based approach

This version outputs random bigints of any size and caches some of the calculation in an object instance to make things more efficient when you are calculating many random numbers within the same range.

export class Random {
    private static readonly buffer = new BigUint64Array(512)
    private static offset: number
    private static refill() {
        crypto.getRandomValues(this.buffer)
        this.offset = 0
    }
    static { this.refill() }

    private readonly rangeSize: bigint
    private readonly wordsNeeded: number
    private readonly rejectionThreshold: bigint

    constructor(public readonly min: bigint, public readonly max: bigint) {
        if (min > max) {
            throw Error("min must be less than or equal to max")
        }
        this.rangeSize = max - min + 1n
        this.wordsNeeded = ((this.rangeSize - 1n).toString(2).length + 63) >>> 6
        const nextPowerOf64Bit = (1n << BigInt(this.wordsNeeded << 6))
        this.rejectionThreshold = nextPowerOf64Bit - (nextPowerOf64Bit % this.rangeSize)
    }

    public next(): bigint {
        let result: bigint
        do {
            result = 0n
            for (let i = 0; i < this.wordsNeeded; ++i) {
                if (Random.offset >= Random.buffer.length) {
                    Random.refill()
                }
                result = (result << 64n) | Random.buffer[Random.offset++]
            }
        } while (result >= this.rejectionThreshold)
        return this.min + result % this.rangeSize
    }

    public static next(min: bigint, max: bigint): bigint {
        return new Random(min, max).next()
    }
}

2 Comments

Won't this introduce a modulo bias?
@EricElliott the range specified by cutoff is evenly divisible by the range of max-min+1, and if the random value is greater than cutoff then continue to do ... while until you get one that isn't.
8

If you are using Node.js, it is safer to use the cryptographically secure pseudorandom crypto.randomInt. Don't go write this kind of sensitive methods if you don't know what you are doing and without peer review.

Official documentation

crypto.randomInt([min, ]max[, callback])

Added in: v14.10.0, v12.19.0

  • min <integer> Start of random range (inclusive). Default: 0.
  • max <integer> End of random range (exclusive).
  • callback <Function> function(err, n) {}.

Return a random integer n such that min <= n < max. This implementation avoids modulo bias.

The range (max - min) must be less than 2^48. min and max must be safe integers.

If the callback function is not provided, the random integer is generated synchronously.

// Asynchronous
crypto.randomInt(3, (err, n) => {
  if (err) throw err;
  console.log(`Random number chosen from (0, 1, 2): ${n}`);
});
// Synchronous
const n = crypto.randomInt(3);
console.log(`Random number chosen from (0, 1, 2): ${n}`);
// With `min` argument
const n = crypto.randomInt(1, 7);
console.log(`The dice rolled: ${n}`);

1 Comment

If OP is using window.crypto.getRandomValues chances are he's working with browsers, not Node.
5

Necromancing.
Well, this is easy to solve.

Consider random number in ranges without crypto-random:

// Returns a random number between min (inclusive) and max (exclusive)
function getRandomArbitrary(min, max) {
    return Math.random() * (max - min) + min;
}

/**
 * Returns a random integer between min (inclusive) and max (inclusive).
 * The value is no lower than min (or the next integer greater than min
 * if min isn't an integer) and no greater than max (or the next integer
 * lower than max if max isn't an integer).
 * Using Math.round() will give you a non-uniform distribution!
 */
function getRandomInt(min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

So all you need to do is replace Math.random with a random from crypt.

So what does Math.random do ?
According to MDN, the Math.random() function returns a floating-point, pseudo-random number in the range 0 to less than 1 (inclusive of 0, but not 1)

So we need a crypto-random number >= 0 and < 1 (not <=).

So, we need a non-negative (aka. UNSIGNED) integer from getRandomValues.
How do we do this?

Simple: Instead of getting an integer, and then doing Math.abs, we just get an UInt:

var randomBuffer = new Int8Array(4); // Int8Array = byte, 1 int = 4 byte = 32 bit 
window.crypto.getRandomValues(randomBuffer);
var dataView = new DataView(randomBuffer.buffer);
var uint = dataView.getUint32();

The shorthand version of which is

var randomBuffer = new Uint32Array(1);
(window.crypto || window.msCrypto).getRandomValues(randomBuffer);
var uint = randomBuffer[0];

Now all we need to do is divide uint by uint32.MaxValue (aka 0xFFFFFFFF) to get a floating-point number. And because we cannot have 1 in the result-set, we need to divide by (uint32.MaxValue+1) to ensure the result is < 1.
Dividing by (UInt32.MaxValue + 1) works because a JavaScript integer is a 64-bit floating-point number internally, so it is not limited at 32 bit.

function cryptoRand()
{
    var array = new Int8Array(4);
    (window.crypto || window.msCrypto).getRandomValues(array);
    var dataView = new DataView(array.buffer);

    var uint = dataView.getUint32();
    var f = uint / (0xffffffff + 1); // 0xFFFFFFFF = uint32.MaxValue (+1 because Math.random is inclusive of 0, but not 1) 

    return f;
}

the shorthand of which is

function cryptoRand()
{
    const randomBuffer = new Uint32Array(1);
    (window.crypto || window.msCrypto).getRandomValues(randomBuffer);
    return ( randomBuffer[0] / (0xffffffff + 1) );
}

Now all you need to do is replace Math.random() with cryptoRand() in the above functions.

Note that if crypto.getRandomValues uses the Windows-CryptoAPI on Windows to get the random bytes, you should not consider these values a truly cryptographically secure source of entropy.

1 Comment

Due to floating point precision issue, the result would be biased
2

Rando.js uses crypto.getRandomValues to basically do this for you

console.log(rando(5, 10));
<script src="https://randojs.com/2.0.0.js"></script>

This is carved out of the source code if you want to look behind the curtain:

var cryptoRandom = () => {
  try {
    var cryptoRandoms, cryptoRandomSlices = [],
      cryptoRandom;
    while ((cryptoRandom = "." + cryptoRandomSlices.join("")).length < 30) {
      cryptoRandoms = (window.crypto || window.msCrypto).getRandomValues(new Uint32Array(5));
      for (var i = 0; i < cryptoRandoms.length; i++) {
        var cryptoRandomSlice = cryptoRandoms[i].toString().slice(1, -1);
        if (cryptoRandomSlice.length > 0) cryptoRandomSlices[cryptoRandomSlices.length] = cryptoRandomSlice;
      }
    }
    return Number(cryptoRandom);
  } catch (e) {
    return Math.random();
  }
};

var min = 5;
var max = 10;
if (min > max) var temp = max, max = min, min = temp;
min = Math.floor(min), max = Math.floor(max);
console.log( Math.floor(cryptoRandom() * (max - min + 1) + min) );

Comments

1

Read this if you're concerned about the randomness of your number:

If you use a 6 sided dice to generate a random number 1 thru 5, what do you do when you land on 6? There's two strategies:

  1. Re-roll until you get a 1 thru 5. This maintains the randomness, but creates extra work.
  2. Map the 6 to one of the numbers you do want, like 5. This is less work, but now you skewed your distribution and are going to get extra 5s.

Strategy 1 is the "rejection sampling" mentioned by @arghbleargh and used in their answer and a few other answers. Strategy 2 is what @Chris_F is referring to as producing biased results.

So understand that all solutions to the original post's question require mapping from one pseudo-random distribution of numbers to another distribution with a different number of 'buckets'.

Strategy 2 is probably fine because:

  • With strategy 2, as long as you are taking the modulo then no resulting number will be more than 2x as likely as any other number. So it is not significantly easier to guess than strategy 1.
  • And as long as your source distribution is MUCH bigger than your target distribution, the skew in randomness will be negligible unless you're running a Monte Carlo simulation or something (which you probably wouldn't be doing in JavaScript to begin with, or at least you wouldn't be using the crypto library for that).
  • Math.random() uses strategy 2, maps from a ~52 bit number (2^52 unique numbers), though some environments use less precision (see here).

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.