defmodule CtrDrbg do @moduledoc """ Pure Elixir implementation of the CTR_DRBG PRNG algorithm. ## Examples iex> state = CtrDrbg.init(:crypto.strong_rand_bytes(16)) iex> {state, random_bytes} = CtrDrbg.generate(state, 32) {#CtrDrbg<...>, <<...>>} """ @typedoc "The current PRNG state." @opaque t :: %__MODULE__{} @derive {Inspect, only: []} @enforce_keys [:k, :v] defstruct [:k, :v] @block_size 16 @seed_size 32 @doc """ Initialize and seed the generator. """ @spec init(binary(), binary()) :: t() def init(entropy, pstring \\ <<>>) do seed = exor(entropy, pstring) reseed( %__MODULE__{v: null_pad(16), k: null_pad(16)}, seed ) end @doc """ Reseed the generator. """ @spec reseed(t(), binary(), binary()) :: t() def reseed(state, seed, additional_entropy_input \\ <<>>) do seed = exor(seed, additional_entropy_input) update(state, seed) end @doc """ Generate an arbitrary number of random bytes. """ @spec generate(t(), pos_integer(), binary()) :: {t(), binary()} def generate(state, length \\ 16, additional_entropy_input \\ <<>>) do state = if byte_size(additional_entropy_input) > 0 do update(state, additional_entropy_input) else state end output_blocks = ceil(length / @block_size) {v, output} = next(output_blocks, state.k, state.v) state = %{state | v: v} {update(state, additional_entropy_input), binary_slice(output, 0..(length - 1))} end defp update(state, seed) do # for each 16-byte block of seed: # increment V # encrypt V using K # pass V to the next iteration and append the output to the output block {_v, output} = next(ceil(@seed_size / @block_size), state.k, state.v) # XOR output with seed output = exor(output, seed) # K = first half of output # V = second half of output <> = output %{state | k: k, v: v} end defp increment(bin) do <> = bin <> end defp next(blocks, k, v, acc \\ <<>>) defp next(0, _k, v, acc) do {v, acc} end defp next(blocks, k, v, acc) do v = increment(v) block = :crypto.crypto_one_time(:aes_128_ecb, k, v, true) next(blocks - 1, k, v, acc <> block) end defp null_pad(binary \\ <<>>, length, direction \\ :leading) defp null_pad(binary, length, _direction) when byte_size(binary) >= length, do: binary defp null_pad(binary, length, direction) do filler = :binary.copy(<<0>>, length - byte_size(binary)) case direction do :leading -> filler <> binary :trailing -> binary <> filler end end # Perform a bitwise exor on bin1 and bin2. If the two binaries are not the same # size, pad the smaller one with trailing null bytes (X ^ 0 == X). If either binary # is empty, return the other. defp exor(bin1, bin2) when byte_size(bin1) == 0, do: bin2 defp exor(bin1, bin2) when byte_size(bin2) == 0, do: bin1 defp exor(bin1, bin2) when byte_size(bin1) < byte_size(bin2) do bin1 = null_pad(bin1, byte_size(bin2), :trailing) exor(bin1, bin2) end defp exor(bin1, bin2) when byte_size(bin1) > byte_size(bin2) do bin2 = null_pad(bin2, byte_size(bin1), :trailing) exor(bin1, bin2) end defp exor(bin1, bin2) when byte_size(bin1) == byte_size(bin2) do :crypto.exor(bin1, bin2) end end