Fast and efficient cryptographically strong versions of several Enum functions that rely on :rand
uniform functions for randomness.
By default, the :rand
functions operate in pseudo-random number generator (PRNG) mode, and hence are not cryptographically secure. To achieve cryptographically strong pseudo-random number generation (CSPRNG) versions, :crypto.rand_seed
can be used to force :rand
functions into a CSPRNG mode; however, the performance of the Enum
functions under CSPRNG is approximately 10 times slower than under PRNG. This module provides functions that are faster than corresponding Enum
functions when operating in CSPRNG mode.
Add crypto_rand
to mix.exs
dependencies:
def deps, do:
[ {:crypto_rand, "~> 1.0.0"} ]
The source code for the :rand
module indicates that at least 55 random bits are required to generate values using :rand
functions. So, for example, the call :rand.uniform(2)
, which returns a value of either 1
or 2
, consumes 55 bits of randomness, even though only 1 bit is actually needed to chose between two values.
PRNG methods are sufficiently fast that this inefficient use of randomness is negligible. CSPRNG methods, however, are much slower, and the inefficiency is easily detected. For example, the time (in seconds) required to generate 1 million random integers using :rand.uniform(8)
with PRNG vs CSPRNG yields:
PRNG: 0.218
CSPRNG: 2.566
The timing is, of course, system dependent. But it's the relative timing that is of interest. Running :rand.uniform/1
under CSPRNG is much slower than PRNG.
Consider the inefficient use of randomness in the above example. At least 53.8 MB of randomness was used to generate the one million integers in the range 1..8; yet since each integer in theory only needs 3 bits to be determined, only 2.9 MB of randomness was actually needed. The remaining 50.9 MB were effectively wasted. That's a usage efficiency ratio of around 5%.
CryptoRand
implements a strategy that maximizes the effective use of CSRPNG generated bytes. For example, to generate random integers in the range 1..8, the function CryptoRand.uniform(8)
uses 3 bits of randomness per call. The time (in seconds) required to generate 1 million CSPRNG random integers as above is:
CryptoRand: 0.647
Though slower than :rand.uniform/1
under PRNG, CryptoRand.uniform/1
is significantly faster than the equivalent use of :rand.uniform/1
under CSPRNG.
All CryptoRand
functions take a final, rand_bytes
argument to specify a function of the form (non_negative_integer) -> binary()
that returns an input integer number of bytes as a binary
. The optional argument defaults to :cryto.strong_rand_bytes/1
and can omitted. However, any suitable function can be substituted for generating bytes, allowing for deterministic testings as well as substituting any random byte generating function of choice.
For efficiency, CryptoRand
stores values in the process dictionary. This is consistent with the default behavior of :rand
. CryptoRand
stores unprocessed bytes generated using rand_bytes
under the key :crypto_rand_bytes
and parameters needed for generating uniform values in the range 1..max
under the key :crypto_rand_max
. These entries can be removed using CryptoRand.clear/0
.
The test suite contains deterministic tests of correctness, non-deterministic histogram tests for validating CryptoRand
implementation in the face of randomness, and timing tests for rough comparison to existing solutions.
To run the deterministic tests:
> mix test --trace
The non-deterministic tests use CryptoRand
functions to create histograms which should exhibit uniform behaviour. The histograms are validated using a simple chi-square test. However, the very nature of chi-square testing means these tests are subject to potential failure even if the underlying implementation is correct. So an occasional failure of a histogram chi-square tests does not mean the implementation is necessarily wrong. For this reason, these tests are put in a separate file that does not automatically get run with the standard mix test
command.
To run the histogram tests:
> mix test --trace test/histogram.exs
The histogram tests include module tags for testing particular CryptoRand
functions. The tags are random
, shuffle
, take_random
, and uniform
. For example, to run the CryptoRand.shuffle/1
histogram tests:
> mix test --trace test/histogram.exs --only shuffle
The timing tests compare CryptoRand
implementation to various existing implementations. These tests are, of course, system dependent; but it is the resulting relative timings that are of interest. Also note these timing tests do not assert expected behavior, and as such don't either pass or fail. For this reason, these tests are put in a separate file that does not automatically get run with the standard mix test command.
To run the timing tests:
> mix test test/timing.exs
The timing tests include module tags for timing particular CryptoRand
functions. The tags are random
, shuffle
, take_random
, uniform
and uniform_list
. For example, to run the CryptoRand.shuffle/1
timing tests:
> mix test test/timing.exs --only shuffle
The following provides example timings.
250000 random elements from enumerable 1..12
rand.uniform (PRNG) : 0.111963
rand.uniform (CSPRNG) : 0.735606
CryptoRand : 0.34594
250000 random elements from list len 12
rand.uniform (PRNG) : 0.736724
rand.uniform (CSPRNG) : 7.786567
CryptoRand : 0.297439
250000 shuffles of enumerable 1..12
rand.uniform (PRNG) : 0.827411
rand.uniform (CSPRNG) : 9.516535
CryptoRand : 2.285146
250000 shuffles of list len 12
rand.uniform (PRNG) : 0.796975
rand.uniform (CSPRNG) : 9.318896
CryptoRand : 2.121225
100000 trials of take_random 5 elements from enumerable 1..26
rand.uniform (PRNG) : 0.711783
rand.uniform (CSPRNG) : 7.060916
CryptoRand : 0.680446
100000 trials of take_random 5 elements from list len 26
rand.uniform (PRNG) : 0.734776
rand.uniform (CSPRNG) : 7.020736
CryptoRand : 0.572719
1000000 uniform ints in range 1..25
rand.uniform (PRNG) : 0.22094
rand.uniform (CSPRNG) : 2.583177
CryptoRand : 1.056496
100000 lists of size 20 with uniform ints from 1..21
rand.uniform (PRNG) : 0.384218
rand.uniform (CSPRNG) : 5.614058
CryptoRand : 1.026913
Unlike Enum
, CryptoRand
isn't contextually constrained to enumerables. Each of the functions CryptoRand.random/1
, CryptoRand.shuffle/1
and CryptoRand.take_random/1
operate on Elixir String.t()
.
The development of CryptoRand
was initially in support of functionality needed for the Puid and RandomPassword libraries. Neither of these libraries required operating on enumerables with a large number of elements. Ensure suitability for your needs before adopting CryptoRand
into your projects.