Skip to content

Instantly share code, notes, and snippets.

@lichrot
Last active December 30, 2025 19:56
Show Gist options
  • Select an option

  • Save lichrot/a01f6c7bc6b6334ff823290c3aadf15c to your computer and use it in GitHub Desktop.

Select an option

Save lichrot/a01f6c7bc6b6334ff823290c3aadf15c to your computer and use it in GitHub Desktop.
Cryptographically secure randomness in JavaScript/TypeScript. All implementations perform ~17x slower than regular Math.random().
const U32_OFFSET = 12;
const F64_EXP = 1072693248;
const RANGE_OFFSET = 1;
const BYTES = new ArrayBuffer(8);
const U32_ARRAY = new Uint32Array(BYTES);
const F64_ARRAY = new Float64Array(BYTES);
/**
* Same as {@link Math.random}, but cryptographically secure.
*
* A bit (no pun intended) more involved then the math one, since it tries to model
* [the union reinterpretation "hack" in C](https://blog.bithole.dev/blogposts/random-float/).
*/
function random(): number {
crypto.getRandomValues(U32_ARRAY);
U32_ARRAY[0] >>>= 0;
U32_ARRAY[1] = (U32_ARRAY[1] >>> U32_OFFSET) | F64_EXP;
return F64_ARRAY[0] - RANGE_OFFSET;
}
const EXP = 2 ** 10 - 2;
const U32_OFFSET = 12;
const BIT_OFFSET = 20;
const BYTES = new ArrayBuffer(8);
const U32_ARRAY = new Uint32Array(BYTES);
const F64_ARRAY = new Float64Array(BYTES);
/**
* Same as {@link Math.random}, but cryptographically secure.
*
* This algorithm improves upon the bitmask approach by making the distribution more uniform.
*
* See [Downey, A. B. (2007). Generating Pseudo-random Floating-Point Values.](https://allendowney.com/research/rand/downey07randfloat.pdf)
*/
function random(): number {
crypto.getRandomValues(U32_ARRAY);
const exp = EXP - Math.clz32(U32_ARRAY[0]);
if (exp < 1) return 0;
U32_ARRAY[1] = (exp << BIT_OFFSET) | (U32_ARRAY[1] >>> U32_OFFSET);
U32_ARRAY[0] = U32_ARRAY[1] << BIT_OFFSET;
return F64_ARRAY[0];
}
const U32_OFFSET = 11;
const U32_INT_MAX = 2 ** 32;
const F64_INT_MAX = 2 ** 53;
const U32_ARRAY = new Uint32Array(2);
/**
* Same as {@link Math.random}, but cryptographically secure.
*
* The algorithm is as follows:
* 1) Generate two 32-bit unsigned integers (using {@link crypto.getRandomValues} in this case)
* 2) Since 64-bit float (standard number in JS) has 53 bits to hold the mantissa (significand),
* we need to trim the high 32-bit integer down to `53 - 32 = 21` bits, which is an offset of `32 - 21 = 11`
* 3) Since we can't go over the limit of 32 bits (bitwise operations convert their operands to that),
* we have to mathematically shift high 21 bits to the left by 32 bits to fit the low 32 bits,
* and the formula to combine the two is `h * 2 ** b + l`, where b is the number of bits (`h * 2 ** 32 + l` in this case)
* 4) Finally, divide the resulting float (which holds the giant integer) by the maximum possible integer of `2 ** 53`
*/
function random(): number {
crypto.getRandomValues(U32_ARRAY);
return ((U32_ARRAY[1] >>> U32_OFFSET) * U32_INT_MAX + (U32_ARRAY[0] >>> 0)) / F64_INT_MAX;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment