Background
I do not recommend trying to roll your own crypto. This is just a for-fun PoC, which won't be used in any public or private scenario.
I am creating a PoC nearly-true random number generator, but this won't be used in real applications. I know the rule of "Don't roll your own crypto", but this is just a fun PoC for nearly-true random numbers. Also, time and convenience do not matter in this case. Also also, I did use the web cryptography API combined with a lot of other CSPRNGs for entropy. Read below...
How it works
The "point" of this is to generate true random numbers without a hardware TRNG in the browser/site with JavaScript.
Of course, if an attacker can break a (CS)PRNG, they can break SSL. (TLS)
The mitigation would be to generate billions of bits of entropy locally, while mixing that with several "randomness beacons"
The (CS/T)PRNG works like this: generate as much local entropy, (into arrays), then get a bunch of numbers from randomness beacons, then hash them together. This is where things get baaaaadddd.
Problem
First, if the values aren't uniform, the values can be somewhat predictable.
Second, the hash function can make the random number lose entropy. A primitive caveman mitigation would be to just add a lot more entropy, but I want to fix this correctly.
So my solution for now is to take the hash (which is several thousand bits long; shake256 is used), re-hash it with sha512, and remove all the letters, and pass that massive number through a library which manages large integers, and then pass it through a uniformity function. This is a modified version of another function I found on SO here:
function uniform(SCKE) { //SCKE must be a string of a hash without a-z
function uniform32 () {
if (SCKE.length <= 23) {
return SCKE;
} else {
return bigInt(SCKE).divide(10**(SCKE.length - 24)).toJSNumber();
};
}
// sample e from geometric(1/2) by counting zeros in uniform bits
// but stop if we have too many zeros to be sensible
let e = 0, x
while ((x = uniform32()) == 0) {
if ((e += 32) >= 1075) {
return 0
}
}
// count the remaining leading zeros in x
e += Math.clz32(x)
// sample s' = sl + sh*2^32 from odd integers in (2^63, 2^64)
// (beware javascript signedness)
let sl = (uniform32() | 0x00000001) >>> 0
let sh = (uniform32() | 0x80000000) >>> 0
// round s' to floating-point number
let s = sl + sh*Math.pow(2, 32)
// scale into [1/2, 1]
let u = s * Math.pow(2, -64)
var srt = Math.trunc(u);
return Math.abs(srt - (u * Math.pow(2, -e))); //MUST RETURN A FLOAT
};
Question(s)
- That is my best mitigation against entropy loss and skewing, but it probably is skewed and has many holes. How can I fix this while keeping uniformity and entropy?
- Is there anything else that could be a problem?
uniform32()
are uniformly distributed, and that it returns the bits of two calls touniform32()
nearly verbatim as the significand of $u$? What are you trying to accomplish with this construction that SHAKE256 doesn't already do—what do you expect to come out of using this code instead of just returning the SHAKE256 output directly? (And what is removing letters and passing it through a bignum supposed to do?) – Squeamish Ossifrage Aug 30 '19 at 02:15uniform_01
code work reproducibly with a seed, to sample floating-point numbers in [0,1] uniformly at random and reproducibly? There's a simpler way to do that—pass in a seed and changeuniform32
to call a generic CSPRNG instead ofwindow.getRandomValues
. (But I don't understand what the $\left|\mathit{srt} - u\cdot 2^{-e}\right|$ business at the end is supposed to do; the result is certainly not the uniform distribution on [0,1].) – Squeamish Ossifrage Aug 30 '19 at 02:19uniform(shake256("txt", 1000).replace(/([a-z])/gm, "").toString())
– Display Name Aug 30 '19 at 02:21srt - u*2**-e
to remove the extra digits and convert it to a proper float – Display Name Aug 30 '19 at 02:24uniform_01
does given access to a uniform random bit string inuniform32
. But it is not clear to me what $\left|\lfloor u\rfloor - u \cdot 2^{-e}\right|$ is supposed to do—it's not (1), (2), or (3). – Squeamish Ossifrage Aug 30 '19 at 02:40function uniform_01(seed) { let prng = new prng(seed); function uniform32() { return prng.uniform32() }; ... }
with the ellipsis filled in as in the original code. Then use whatever PRNG you want, as long asprng.uniform32()
returns a 32-bit unsigned integer. (The crazy results you got probably happened because youruniform32
did not do that.) For example, if key erasure (‘forward secrecy’, ‘backtracking resistance’) is not important, you could use the first 32 bits ofSHAKE128(seed + i)
wherei
is a count of the number of requests to the PRNG so far. – Squeamish Ossifrage Aug 30 '19 at 02:52window.crypto.getRandomValues
does. You could safely just generate 256 bits (32 bytes, or eight 32-bit unsigned integers in a UInt32Array) from SHAKE128 and then read them out sequentially; it won't ever return more than 128 leading zero bits, in practical terms, so the loop will halt before then. – Squeamish Ossifrage Aug 30 '19 at 03:10