I'm trying to generate secure randomness in Node as a personal challenge. I was wondering if the following implementations are correctly random or they have some flaws.
const crypto = require('crypto');
const secureRandomFloat = (bytes = 6) => {
const size = Math.pow(2, bytes * 8);
const hexString = crypto.randomBytes(bytes).toString('hex');
const randInt = parseInt(hexString, 16);
return randInt / size;
}
export const secureRandomIntRange = (min, max) => {
const range = max - min; // max is the first not included integer [min, max)
if (range > Number.MAX_SAFE_INTEGER) return null;
// use an amount of bytes that will cover the range we need
const bytes = Math.ceil(Math.log(range) / Math.log(256));
const size = Math.pow(2, bytes * 8);
// this is the "tail" of possible numbers. Using them that would
// give us more chances to get lower values when using modulo
const excess = size % range;
let validRand = false;
let hexString;
let randInt;
while (!validRand) {
hexString = crypto.randomBytes(bytes).toString('hex');
randInt = parseInt(hexString, 16);
validRand = randInt + excess < size;
}
return min + randInt % range;
}
const secureRandomChoice = arr => {
// This is to check that the "arr" is iterable, like an array
// or a string, etc.
if (arr == null) return null;
if (typeof arr[Symbol.iterator] !== 'function') return null;
if (!arr.hasOwnProperty('length')) return null;
// use an amount of bytes that will cover the range we need
const bytes = Math.ceil(Math.log(arr.length) / Math.log(256));
const randIdx = secureRandomIntRange(0, arr.length);
if (randIdx === null) return null;
return arr[randIdx];
}
I've done some testing with a million iterations and results seem to be random, but I don't know if I'm missing something.
Are these implementations going to give me pseudo-randomness?
Culling
@Blindman67 has commented that culling, which I do in the while loop at secureRandomIntRange
, is not going to give me random values. I don't have a strong argument to say that culling will give me random numbers, but I don't see why it won't.
I used culling because I remembered once I needed to generate random points in a sphere. To do that I took a random point in a cube (much simpler), and discarded the point if it was out of the sphere. I think it gave me a random distribution of points in the sphere.
I tried to apply this same idea to this problem. If I have 1 byte I can generate 256 possible combinations at random, say from number 0 to 255. If I wanted to select an integer from 0 to 99 (100 possible numbers), the naive way using modulo would give me the following relations:
randomByte: 0 1 2 ... 99 100 101 ... 199 200 201 ... 254 255
secureRandInt: 0 1 2 ... 99 0 1 ... 99 0 1 ... 54 55
But that way all numbers from 0-55 can be drawn from 3 different bytes, whereas numbers from 56-99 can only be drawn from 2 different bytes. Then, the way I thought could give me a random distribution in the range was assigning the numbers as follows:
randomByte: 0 1 2 ... 99 100 101 ... 199 200 201 ... 254 255
secureRandInt: 0 1 2 ... 99 0 1 ... 99 x x ... x x
Where x
means that the byte is not valid (!validRand
) and I have to discard it and get a new byte at random.
In this example, the values for the different variables would be as follows:
range = 100
size = 256
excess = 56
isValid = randInt + excess < size
// isValid = randInt + 56 < 256 --> isValid = randInt < 200
// T for true and F for false
randomByte: 0 1 2 ... 99 100 101 ... 199 200 201 ... 254 255
isValid: T T T ... T T T ... T F F ... F F
Do you think that approach is invalid to get random numbers?
secureRandomIntRange
just takes min and max, and you also pass bytes insecureRandomChoice
\$\endgroup\$secureRandomFloat(bytes)
forbytes > 6
creates integer values greater thanNumber.MAX_SAFE_INTEGER
so no longer has even distribution . Also the while loop insecureRandomIntRange
is selectively culling values. That is not random \$\endgroup\$bytes
as an argument instead of computing it insecureRandomIntRange
. I didn't realize the mistake because I wasn't using that argument anymore, so no errors were thrown. I'll edit the code. Thanks for the catch! \$\endgroup\$Number.MAX_SAFE_INTEGER
and return an error. Regarding the culling, can you elaborate why that cannot be random? I'll update the question with my reasoning for doing culling. I'll add the check for range size. I'll also decrease the range size by 1, returning values from min to max, not including max –that seems to be the standard–. Thanks for the comment! \$\endgroup\$