I tried to make a cryptographically secure random int in range generator for javascript, practically the javascript equivalent of php's random_int(min,max):
function maybe_cryptographically_secure_random_int(min, max) {
if (!Number.isInteger(min)) {
throw new Error("min must be an integer");
}
if (!Number.isInteger(max)) {
throw new Error("max must be an integer");
}
if (min < Number.MIN_SAFE_INTEGER) {
throw new Error("min must be >= Number.MIN_SAFE_INTEGER (" + Number.MIN_SAFE_INTEGER + ")");
}
if (max > Number.MAX_SAFE_INTEGER) {
throw new Error("max must be <= Number.MAX_SAFE_INTEGER (" + Number.MAX_SAFE_INTEGER + ")");
}
if (min === max) {
return min;
}
if (min > max) {
throw new Error("min must be <= max");
}
const range = max - min + 1;
if (range >= Number.MAX_SAFE_INTEGER) {
// hmm, i wonder if this is a problem or not
}
const bits_needed = Math.ceil(Math.log2(range));
const bytes_needed = Math.ceil(bits_needed / 8);
const max_value = Math.pow(256, bytes_needed);
if (max_value > Number.MAX_SAFE_INTEGER) {
// hmm, i wonder if this is a problem or not
}
const max_acceptable = Math.floor(max_value / range) * range - 1;
if (max_acceptable > Number.MAX_SAFE_INTEGER) {
// hmm, i wonder if this is a problem or not
}
const random_bytes = new Uint8Array(bytes_needed);
let random_value;
const max_attempts = 9999; // never seen it take more than 2 attempts...
let attempts = 0;
do {
++attempts;
if (attempts > max_attempts) {
throw new Error("Failed after " + max_attempts + " attempts");
}
crypto.getRandomValues(random_bytes);
random_value = 0;
for (let i = 0; i < random_bytes.length; i++) {
random_value = random_value * 256 + random_bytes[i];
}
} while (random_value > max_acceptable);
random_value = random_value % range;
const ret = min + random_value;
if (ret < min || ret > max) {
// should be impossible...
throw new Error("Generated value " + ret + " is out of range [" + min + ", " + max + "]");
}
// console.log("Attempts: " + attempts);
return ret;
}
some simple test code:
o={"-1":0,"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0};
for(let i=0;i<1_000_000;++i){
++o[maybe_cryptographically_secure_random_int(-1, 10)];
}
o;
yields something like
{
0: 82700,
1: 83786,
2: 83459,
3: 83454,
4: 82887,
5: 83546,
6: 83341,
7: 83035,
8: 83637,
9: 83378,
10: 83417,
-1: 83360
}