lib/charon/utils/key_generator.ex

defmodule Charon.Utils.KeyGenerator do
  @moduledoc """
  Derive a key from a base secret using PBKDF2.
  """

  @type opts :: [
          length: pos_integer(),
          iterations: pos_integer(),
          digest: :sha | :sha224 | :sha256 | :sha384 | :sha512
        ]

  @doc """
  Derive a new key from `base_secret` using `salt`.
  The result is cached using `FastGlobal` with key `#{__MODULE__}`.

  ## Options

    - `:length` key length in bytes, default 32 (256 bits)
    - `:iterations` hash iterations to derive new key, default 250_000
    - `:digest` hashing algorithm used as pseudo-random function, default `:sha256`


  ## Doctests

      iex> derive_key("secret", "salt", length: 5, iterations: 1)
      <<56, 223, 66, 139, 48>>

      # key is returned from cache based on function args
      iex> FastGlobal.put(KeyGenerator, %{{"secret", "salt", [length: 5, iterations: 1]} => "supersecret"})
      iex> derive_key("secret", "salt", length: 5, iterations: 1)
      "supersecret"
  """
  @spec derive_key(binary, binary, opts) :: binary()
  def derive_key(base_secret, salt, opts \\ []) do
    cache = FastGlobal.get(__MODULE__) || %{}

    if cached = Map.get(cache, {base_secret, salt, opts}) do
      cached
    else
      length = opts[:length] || 32
      digest = opts[:digest] || :sha256
      iterations = opts[:iterations] || 250_000

      :crypto.pbkdf2_hmac(digest, base_secret, salt, iterations, length)
      |> tap(&FastGlobal.put(__MODULE__, Map.put(cache, {base_secret, salt, opts}, &1)))
    end
  end
end