Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use window.crypto.getRandomValues to get random values in a specific range

We had been using Math.random to get random numbers between 4000-64000.:

Math.floor(Math.random() * 60000 + 4000);

We have to now replace this with a more cryptographically secure random number generator. After searching on this issue we have decided to go with window.crypto.getRandomValues. I am not able to figure out how to use this to get a random number between a particular range. Can someone please help out.

like image 212
Sid Avatar asked Dec 24 '22 22:12

Sid


2 Answers

For a given min and max, the formula u \cdot \left ( 1 - {2^u \boldsymbol{\textup{mod}} (max-min) \over 2^u} \right ) \sum_{i=0}^{\infty} \left( 2^u \boldsymbol{\textup{mod}} (max-min) \over 2^u \right )^i (i + 1) describes how many bits you'll use on average if you request u bits at once and retry if returning the result would introduce bias.

Fortunately, the optimal strategy is simply requesting ceil(log2(max - min + 1)) bits at once. We can only get full bytes with crypto.getRandomValues anyways, so if we have one call of crypto.getRandomValues per function call, the best we can do is:

// Generate a random integer r with equal chance in  min <= r < max.
function randrange(min, max) {
    var range = max - min;
    if (range <= 0) {
        throw new Exception('max must be larger than min');
    }
    var requestBytes = Math.ceil(Math.log2(range) / 8);
    if (!requestBytes) { // No randomness required
        return min;
    }
    var maxNum = Math.pow(256, requestBytes);
    var ar = new Uint8Array(requestBytes);

    while (true) {
        window.crypto.getRandomValues(ar);

        var val = 0;
        for (var i = 0;i < requestBytes;i++) {
            val = (val << 8) + ar[i];
        }

        if (val < maxNum - maxNum % range) {
            return min + (val % range);
        }
    }
}

If you generate many values, you may consider some optimizations, namely requesting more bytes (i.e. a larger array) in advance. If your range becomes smaller (say you want to flip a coin), than it may also be beneficial to work in a bit-based manner, i.e. request many bits upfront and then only use up the random bits you really need.

like image 150
phihag Avatar answered Dec 26 '22 10:12

phihag


A simple replacement for Math.random might look like this:

/**
 * Return values in the range of [0, 1)
 */
const randomFloat = function () {
  const int = window.crypto.getRandomValues(new Uint32Array(1))[0]
  return int / 2**32
}

To extend this to integers:


/**
 * Return integers in the range of [min, max)
 *
 * @todo check that min is <= max.
 */
const randomInt = function (min, max) {
  const range = max - min
  return Math.floor(randomFloat() * range + min)
}

To extend this to arrays of integers:


/**
 * Generate an array of integers in the range of [min, max).
 */
const randomIntArray = function (length, min, max) {
  return new Array(length).fill(0).map(() => randomInt(min, max))
}

Generate an array of ten integers from 0 to 2 inclusive:

randomIntArray(10, 0, 3)
[0, 2, 1, 2, 0, 0, 1, 0, 1, 0]
like image 36
t.888 Avatar answered Dec 26 '22 12:12

t.888