Last active
December 30, 2025 19:56
-
-
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().
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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]; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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