6

It looks like Math.random() generates a 64-bit floating point number in the range [0,1) while the new crypto.getRandomValues() API only returns ints. What would be the ideal way to produce a number in [0,1) using this API?

This seems to work but seems suboptimal:

ints = new Uint32Array(2)
window.crypto.getRandomValues(ints)
return ints[0] / 0xffffffff * ints[1] / 0xffffffff

EDIT: To clarify, I am trying to produce better results than Math.random(). From my understanding of floating point, it should be possible to get a fully random fraction for 52 bits of randomness. (?)

EDIT 2: To give a little more background, I'm not trying to do anything cryptographically secure but there are a lot of anecdotal stories about Math.random() being implemented poorly (e.g. http://devoluk.com/google-chrome-math-random-issue.html) so where a better alternative is available I'd like to use it.

Juan Pablo Rinaldi
  • 3,394
  • 1
  • 20
  • 20
svachalek
  • 1,166
  • 8
  • 18
  • I tried your code in Chrome, and I also get values `> 1`, e.g. `2.5...`. – Šime Vidas Dec 04 '12 at 01:27
  • Why `Math.abs()`? From what I see in Chrome, the random numbers are positive. – Šime Vidas Dec 04 '12 at 01:30
  • @ŠimeVidas Indeed, they must necessarily be positive, because the OP is using a Uint (unsigned int) buffer view. `crypto.getRandomValues` just generates a random binary buffer that is contextualized into numbers by whatever type of view is used. – apsillers Dec 04 '12 at 01:32
  • @apsillers I see. I tried with `Int32Array` and got negative numbers indeed. – Šime Vidas Dec 04 '12 at 01:34
  • Why that expression? Wouldn't just `ints[0] / 0xffffffff` produce a random `[0,1)` value? – Šime Vidas Dec 04 '12 at 01:37
  • @apsillers Good point; I played around with a few different ways and managed to mix them up in my copy/paste! – svachalek Dec 04 '12 at 01:52
  • For anybody looking for a solution in Node.js, you can use [`crypto.randomInt(0,1)`](https://nodejs.org/api/crypto.html#crypto_crypto_randomint_min_max_callback) – adriaan Feb 11 '21 at 22:46

3 Answers3

7

Remember that floating point numbers are just a mantissa coefficient, multiplied by 2 raised to an exponent:

floating_point_value = mantissa * (2 ^ exponent)

With Math.random, you generate floating points that have a 32-bit random mantissa and always have an exponent of -32, so that the decimal place is bit shift to the left 32 places, so the mantissa never has any part to the left of the decimal place.

mantissa =         10011000111100111111101000110001 (some random 32-bit int)
mantissa * 2^-32 = 0.10011000111100111111101000110001

Try running Math.random().toString(2) a few times to verify that this is the case.

Solution: you can just generate a random 32-bit mantissa and multiply it by Math.pow(2,-32):

var arr = new Uint32Array(1);
crypto.getRandomValues(arr);
var result = arr[0] * Math.pow(2,-32);
// or just   arr[0] * (0xffffffff + 1);

Note that floating points do not have an even distribution (the possible values become sparser the larger the numbers become, due to a lack of precision in the mantissa), making them ill-suited for cryptographic applications or other domains which require very strong random numbers. For that, you should use the raw integer values provided to you by crypto.getRandomValues().

EDIT:

The mantissa in JavaScript is 52 bits, so you could get 52 bits of randomness:

var arr = new Uint32Array(2);
crypto.getRandomValues(arr);

// keep all 32 bits of the the first, top 20 of the second for 52 random bits
var mantissa = (arr[0] * Math.pow(2,20)) + (arr[1] >>> 12)

// shift all 52 bits to the right of the decimal point
var result = mantissa * Math.pow(2,-52);

So, all in all, no, this isn't ant shorter than your own solution, but I think it's the best you can hope to do. You must generate 52 random bits, which needs to be built from 32-bit blocks, and then it need to be shifted back down to below 1.

apsillers
  • 112,806
  • 17
  • 235
  • 239
  • 1
    That's equivalent to `var result = arr[0] / ( 0xffffffff + 1 );`, right? – Šime Vidas Dec 04 '12 at 01:47
  • I should clarify that I'm looking for the same interface as Math.random() but *better*, i.e. 52 bits of randomness. – svachalek Dec 04 '12 at 01:54
  • @ŠimeVidas Yes, and that's better than computing `Math.pow` each time, too. Thanks, added to my answer. – apsillers Dec 04 '12 at 01:59
  • @svachalek Edited with a solution, but it's not any shorter than your own, I'm afraid! – apsillers Dec 04 '12 at 02:24
  • I think it's better due to the lack of random bits multiplying with each other; I'm not much of a mathematician but that strikes me as reducing randomness somehow. I think it's probably faster using constants over Math.pow though: (ints[0] * 0x100000 + (ints[1] & 0xfffff)) / 0xfffffffffffff – svachalek Dec 04 '12 at 02:56
  • Shouldn't you be using the *unsigned* right shift, i.e. `arr[1] >>> 12`, as e.g. `0xffffffff >> 12 === -1`? (Or use `& 0xfffff` as above.) – MikeM Nov 02 '14 at 13:23
  • @MikeM Thanks, signed right-shift is indeed incorrect here; I changed it to `>>>`. – apsillers Nov 02 '14 at 18:14
0

Well, that is as optimal as it should be in case you really need the number in the range [0,1).

The problem with that code is that the odds for differents numbers are not the same anymore.

With that code is more likely for example to get an 0.5 (1*0.5,0.5*1,0.75*0.666) than a 1 (1*1).

Salchi13
  • 59
  • 3
0

The bit-twiddling version in this duplicate is also nice, since the spec says that numbers are IEEE 754. Also, consider the DataView comment for endianness.

Tom Palmer
  • 463
  • 5
  • 10