lib/ctr_drbg.ex

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
    <<k::binary-size(16), v::binary-size(16)>> = output

    %{state | k: k, v: v}
  end

  defp increment(bin) do
    <<int_val::unit(8)-size(byte_size(bin))>> = bin
    <<int_val + 1::128>>
  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