0

I've been goofing around with random numbers a lot recently and have usually just used Math.Random as I've had no need to use something more secure, however I figured it would be useful to learn stuff like this and I was wondering how secure / practical this function is and some improvements I could implement.

function random_number(max) {
    let buffer=new ArrayBuffer(8);
    let ints=new Int8Array(buffer);
    window.crypto.getRandomValues(ints);
    ints[7]=64-1;
    ints[6]|=0xf0;
    let float=new DataView(buffer).getFloat64(0,true)-1;
    return Math.floor(float*Math.floor(max+1));
}
fgorov
  • 3
  • 1

1 Answers1

0

No, it isn't.

What you're effectively doing (via a cast of binary data to a floating point value) is calculating floor(max * rand() / (RAND_MAX + 1.0)) (with RAND_MAX equal to 252−1). This will always result in a skewed distribution unless max is a factor of RAND_MAX+1, as explained here.

This is quite easy to demonstrate:

function random_number(max) {
    let buffer=new ArrayBuffer(8);
    let ints=new Int8Array(buffer);
    window.crypto.getRandomValues(ints);
    ints[7]=64-1;
    ints[6]|=0xf0;
    let float=new DataView(buffer).getFloat64(0,true)-1;
    return Math.floor(float*Math.floor(max+1));
}

function check_skew() {
    var m = Math.floor(Math.pow(2,52) * 2 / 3);
    var o = [0,0];
    var ns = 100000;
    for (i=0; i<ns; i++) o[random_number(m)&1]++; o;
    console.log("Out of "+ns+" random numbers, "+o[0]*100/ns+
                "% were even and "+o[1]*100/ns+"% were odd.");
}
<button onclick="check_skew()">Click this button a few times and check the results</button>

The correct way to obtain a random integer over a specified range is to start with a uniform random number whose bit length is at least as long as that of max. Discard the higher bits, and return the result if it is less than or equal to max. Otherwise repeat the process.

Something like this, perhaps:

function rand_int(max) {
    // Returns a uniform random integer from 0 to max (inclusive)
    var mask = 1;
    var crypto = window.crypto;
    max = Math.floor(max);

    if (!crypto) throw "window.crypto undefined";
    if (max < 1) throw "max value too small";
    if (max > 0xffffffff) throw "max value too large";

    // Generate binary mask (all 1)
    while (mask < max) mask = (mask << 1) | 1;

    // Now generate random values until one is within range
    var r = new Int32Array(1);
    do {
        crypto.getRandomValues(r);
        r[0] &= mask;
    } while (r[0] > max);
    return r[0];
}
r3mainer
  • 23,981
  • 3
  • 51
  • 88