In order to produce a cryptographically secure pseudorandom number generator (CSPRNG), the output has to pass the next-bit test. Roughly, that means that if an attacker has a given amount of output of that generator, they cannot guess the next bit with a better probability than 0.5 (random chance). Due to Kerckhoff's principle, that means that the attacker can poke, prod, and analyze the algorithm at will to determine that information.
To determine if this generator meets that test, let's look at the internals. First, let's skip the entropy generation part with time_ns
and always specify a seed. This will make our output deterministic and easier to analyze, and, since we assume in a CSPRNG that the seed or entropy input is the only private data, we're no worse off.
We can also note that fair_bit
is effectively output whitening. To aid in analysis, we'll simply call _bit
in octabit_number
, which will help us better understand the output of the raw generator.
If we break down defloat
into a multiplication and the rest (which we can call semi_defloat
), we get a definition like this: semi_defloat = lambda x: (10^(len(str(x))-(str(x).find(".")+1)))
. We can observer that the result of semi_defloat
is typically a small integer, usually 4, 5, or 7. Other values are possible, but, for analysis's sake, we'll want to determine the most likely internal state to see if we can more effectively cryptanalyze it. Thus, defloat
is effectively multiplying its input by a small integer.
I noted that it may have been the intention to perform an exponentiation here, but that is not what ^
does in Python (it performs an XOR). I tested with an exponentiation and that leads to the generator devolving into producing entirely zero bits as the values become very large and consistently even, so that is not an improvement in design.
One thing I noted is that if the input state is all zeros, then the output is also zeros. In both this case and my test exponentiation above, the real generator simply hangs since the whitening step fails. While not insecure, this is practically inconvenient, and as such would be considered a weak state.
Thus, the core of the generator consists of two values, which it updates with a square root, an addition, and some multiplications by small integers.
We should note that the fractional part of the square root operation is very important because if we use int
on the sqrt
, the generator tends to devolve into a fixed state (usually all-ones) very quickly. Similarly, if we change the call to Random.defloat
with a call to multiply 4, 5, or 7, then we also devolve into a fixed state (either odd or even, depending on the integer) quite rapidly, so the exact semantics of Random.defloat
are very important.
A final thing I noted examining this is the internal state values tend to end up hovering around having 2 or 3 digits before the decimal point. If we assume that we're using a normal double-precision float here, that means that we're expressing a tiny fraction of the valid 64-bit values we can. Thus, the internal state here is substantially smaller than 128 bits, and thus this is probably subject to brute force.
I don't have a huge amount of time to spend on analyzing this further, but due to the smaller-than-expected effective internal state and the absolute dependence on the variation in the small integer of Random.defloat
to not devolve into a fixed state, I'm not very confident in this design. It's my expectation that a real cryptographer (which I am not) with more experience would probably be successful in attacking this without too much difficulty.