1

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?
  • Are you aware that the code you copied & pasted without credit assumes, as a premise, that the outcomes of uniform32() are uniformly distributed, and that it returns the bits of two calls to uniform32() 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:15
  • I used the function to achieve a uniform approach since the SHAKE value was too big to work with in JS. If I had a 1000+ digit number string, that would be useless, so I cut it down with bigInt and then applied the uniform function. – Display Name Aug 30 '19 at 02:17
  • Also, I will go credit the source (gonna go find the author and edit the post) – Display Name Aug 30 '19 at 02:19
  • Are you just trying to make the original uniform_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 change uniform32 to call a generic CSPRNG instead of window.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:19
  • Isn't that what I did with the SCKE variable? The original SCKE input would be uniform(shake256("txt", 1000).replace(/([a-z])/gm, "").toString()) – Display Name Aug 30 '19 at 02:21
  • Alright I just read your edits, I used the srt - u*2**-e to remove the extra digits and convert it to a proper float – Display Name Aug 30 '19 at 02:24
  • What I'm getting at is that there are three separate concerns here: 1. combining nonuniform random sources into a short uniform random seed (which SHAKE128 can do); 2. expanding a short uniform random seed into many uniform random bits (which SHAKE128 can also do); 3. mapping a uniform random bit string into a uniform random floating-point number in [0,1], which is what uniform_01 does given access to a uniform random bit string in uniform32. 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:40
  • Without that, when I input the string "291376178..." it would return +1.23817... or something similar. I however just thought of a simpler solution. Would just using SHAKE, extracting the numbers as usual, and then splitting that into values that uniform01 can be seeded with and uniform32() can return, and then feeding that data stream into a nice array, so that we can throw away all the flawed code I made? I see that the code I modified is highly flawed, so would this implementation work better? – Display Name Aug 30 '19 at 02:46
  • Just structure it as function 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 as prng.uniform32() returns a 32-bit unsigned integer. (The crazy results you got probably happened because your uniform32 did not do that.) For example, if key erasure (‘forward secrecy’, ‘backtracking resistance’) is not important, you could use the first 32 bits of SHAKE128(seed + i) where i is a count of the number of requests to the PRNG so far. – Squeamish Ossifrage Aug 30 '19 at 02:52
  • Alright so would this be a viable solution: collect entropy, hash together with SHAKE, remove letters from the hash (or replace doesn't matter, does it?), then break the hash into a bunch of strings which are 32-bit unsigned integers, then pass all of the numbers into the uniform function, then format the floats into an array, then we've got a nice array with floats? – Display Name Aug 30 '19 at 02:58
  • Don't do anything with letters or digits. Don't parse or interpret the output of SHAKE128 at all. Just store it in an unsigned integer array like window.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
  • Alright got it. I'm going to try to code it now – Display Name Aug 30 '19 at 03:17
  • The techie code and crypto comments above aren't particularly relevant to the crux of your issue which is - "...if an attacker can break a (CS)PRNG, they can break SSL. (TLS)". SSL can be forcibly turned off/bypassed and many consider it broken anyway. That leaves "generate billions of bits of entropy locally" as mitigation. So make megabits locally and don't bother with weird randomness beacons. They're pretty useless anyway. But you've not stated how you'll do this locally if you're looking for Kolmogorov random entropy. So, how billions? – Paul Uszak Aug 30 '19 at 22:14

0 Answers0