Skip to content

Instantly share code, notes, and snippets.

@James-E-A
Last active September 6, 2025 19:59
Show Gist options
  • Save James-E-A/da3789750d5da196acecdd05b146174b to your computer and use it in GitHub Desktop.
Save James-E-A/da3789750d5da196acecdd05b146174b to your computer and use it in GitHub Desktop.
Elixir simple seeded AES-CTR rng
defmodule Foo.SimpleRNG do
@moduledoc """
A simple glue layer to use CTR-mode `:crypto` algorithms with the `:rand` module.
iex> new(:aes_128_ctr, <<0::size(128)>>)
...> |> :rand.seed()
iex> :rand.uniform(10**10)
3992083247
iex> :rand.uniform(10**10)
4813258075
"""
defp do_alg(crypto_cipher, options) do
import Bitwise
# The size of an underlying "block" after which the counter is incremented.
block_size = Keyword.fetch!(options, :block_size)
# null plaintext to convert stream cipher back into a CSPRNG
null_plaintext = <<0::size(block_size)>>
# The TOTAL size of the underlying IV/Nonce parameter, including the counter
iv_size = Keyword.get(options, :iv_size, block_size)
ctr_mask = 2 ** iv_size - 1
# The portion of the IV/Nonce which is NOMINALLY considered the "counter"
jump_size = Keyword.get(options, :jump_size, div(iv_size, 2))
if jump_size > iv_size * 0.625 do
raise ArgumentError, "jump too large compared to iv"
end
jump = 2 ** jump_size
%{
type: crypto_cipher,
bits: block_size,
next: fn {crypto_key, ctr} ->
crypto_iv = <<ctr::size(iv_size)>>
crypto_result = :crypto.crypto_one_time(crypto_cipher, crypto_key, crypto_iv, null_plaintext, true)
rand_result = :binary.decode_unsigned(crypto_result, :big)
{rand_result, {crypto_key, (ctr + 1) &&& ctr_mask}}
end,
#uniform_n: &uniform_n/2,
jump: fn {handler, {crypto_key, ctr}} ->
{handler, {crypto_key, (ctr + jump) &&& ctr_mask}}
end
}
end
@spec new() :: :rand.state()
def new(), do: new(:crypto.strong_rand_bytes(16), 0)
@spec new(cipher :: atom()) :: :rand.state()
@spec new(key :: binary()) :: :rand.state()
def new(:aes_128_ctr), do: new(:aes_128_ctr, :crypto.strong_rand_bytes(16), 0)
def new(:aes_256_ctr), do: new(:aes_128_ctr, :crypto.strong_rand_bytes(32), 0)
def new(:chacha20), do: new(:aes_128_ctr, :crypto.strong_rand_bytes(32), 0)
def new(:sm4_ctr), do: new(:aes_128_ctr, :crypto.strong_rand_bytes(16), 0)
def new(<<key::binary-size(16)>>), do: new(:aes_128_ctr, key, 0)
def new(<<key::binary-size(32)>>), do: new(:aes_256_ctr, key, 0)
@spec new(cipher :: atom(), key :: binary()) :: :rand.state()
@spec new(key :: binary(), ctr_start :: non_neg_integer()) :: :rand.state()
@spec new(cipher :: atom(), ctr_start :: non_neg_integer()) :: :rand.state()
def new(cipher, <<key::binary>>) when is_atom(cipher), do: new(cipher, key, 0)
def new(<<key::binary-size(16)>>, ctr) when is_integer(ctr), do: new(:aes_128_ctr, key, ctr)
def new(<<key::binary-size(32)>>, ctr) when is_integer(ctr), do: new(:aes_256_ctr, key, ctr)
def new(:aes_128_ctr, ctr) when is_integer(ctr), do: new(:aes_128_ctr, :crypto.strong_rand_bytes(16), ctr)
def new(:aes_256_ctr, ctr) when is_integer(ctr), do: new(:aes_128_ctr, :crypto.strong_rand_bytes(32), ctr)
def new(:chacha20, ctr) when is_integer(ctr), do: new(:aes_128_ctr, :crypto.strong_rand_bytes(32), ctr)
def new(:sm4_ctr, ctr) when is_integer(ctr), do: new(:aes_128_ctr, :crypto.strong_rand_bytes(16), ctr)
@spec new(cipher :: atom(), key :: binary(), ctr_start :: non_neg_integer()) :: :rand.state()
def new(cipher, key, ctr)
def new(:aes_128_ctr, <<key::binary-size(16)>>, ctr) when is_integer(ctr) do
{do_alg(:aes_128_ctr, block_size: 128), {key, ctr}}
end
def new(:aes_256_ctr, <<key::binary-size(32)>>, ctr) when is_integer(ctr) do
{do_alg(:aes_256_ctr, block_size: 128), {key, ctr}}
end
def new(:chacha20, <<key::binary-size(32)>>, ctr) when is_integer(ctr) do
{do_alg(:chacha20, block_size: 512, iv_size: 128, jump_size: 32), {key, ctr}}
end
def new(:sm4_ctr, <<key::binary-size(16)>>, ctr) when is_integer(ctr) do
{do_alg(:sm4_ctr, block_size: 128), {key, ctr}}
end
end
@James-E-A
Copy link
Author

James-E-A commented Sep 5, 2025

Terse version

import Bitwise

{
  %{ # https://gist.github.com/da3789750d5da196acecdd05b146174b
    type: :aes_128_ctr,
    bits: 128,
    next: fn {key, ctr} ->
      {:binary.decode_unsigned(:crypto.crypto_one_time(:aes_128_ctr, key, <<ctr::size(128)>>, <<0::size(128)>>, [])),
       {key, (ctr + 1) &&& 0xffffffffffffffffffffffffffffffff}}
    end,
    jump: fn {alg, {key, ctr}} ->
      {alg, {key, (ctr + 0x10000000000000000) &&& 0xffffffffffffffffffffffffffffffff}}
    end
  },
  {:crypto.strong_rand_bytes(16), 0}
}
|> :rand.seed()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment