Skip to main content

lib/bloccs/effects/random_system.ex

defmodule Bloccs.Effects.Random.System do
  @moduledoc """
  Pseudo / crypto random effect.

  `random = "pseudo"` draws from `:rand` (fast, not suitable for secrets);
  `random = "crypto"` draws from `:crypto.strong_rand_bytes/1` via unbiased
  rejection sampling, suitable for tokens and nonces.
  """

  @behaviour Bloccs.Effects.Random

  defstruct mode: :pseudo

  @impl true
  def new("pseudo"), do: %__MODULE__{mode: :pseudo}
  def new("crypto"), do: %__MODULE__{mode: :crypto}
  # An unrecognized mode is kept verbatim (no atom minted from manifest input)
  # and denied on first use with the original string in the message.
  def new(mode) when is_binary(mode), do: %__MODULE__{mode: mode}
  def new(_), do: %__MODULE__{mode: :pseudo}

  @impl true
  def int(%__MODULE__{mode: :pseudo}, n) when is_integer(n) and n > 0, do: :rand.uniform(n)

  def int(%__MODULE__{mode: :crypto}, n) when is_integer(n) and n > 0, do: crypto_uniform(n)

  def int(%__MODULE__{mode: mode}, _),
    do: Bloccs.Effects.deny!(:random, "mode #{inspect(mode)} unsupported")

  # Uniform 1..n from strong random bytes. A bare `rem(r, n)` is modulo-biased
  # whenever 256^bytes isn't a multiple of n, so values past the largest exact
  # multiple of n are rejected and redrawn.
  defp crypto_uniform(1), do: 1

  defp crypto_uniform(n) do
    bytes = byte_width(n)
    range = Integer.pow(256, bytes)
    threshold = range - rem(range, n)
    r = :crypto.bytes_to_integer(:crypto.strong_rand_bytes(bytes))

    if r < threshold, do: rem(r, n) + 1, else: crypto_uniform(n)
  end

  defp byte_width(n) do
    bits = (n - 1) |> Integer.digits(2) |> length()
    div(bits + 7, 8)
  end
end