lib/random_password.ex

defmodule RandomPassword do
  @moduledoc """
  Random Password generator.

  `RandomPassword` creates a module for randomly generating strings with a specified number of
  alpha, decimal and symbol characters. Symbols can be optionally specified.

  """

  alias Puid.Chars
  alias RandomPassword.Util

  @doc """

  Bits of entropy for password with `alpha` alpha chars, `decimal` decimal digits and `symbol`
  symbol chars.

  This function provides calculation of entropy bits without having to create a module.

  The characters to be used for `alphas`, `decimals` and `symbols` can be specified as options;
  o/w defaults are used.

  ## Example

      iex> RandomPassword.entropy_bits(12, 4, 2) |> Float.round(2)
      91.31

      iex> RandomPassword.entropy_bits(12, 4, 2, symbols: "!@#$%&") |> Float.round(2)
      86.86

  """

  @spec entropy_bits(non_neg_integer, non_neg_integer, non_neg_integer, map()) :: float()
  def entropy_bits(alpha, decimal, symbol, options \\ %{}) do
    alphas = options[:alphas] || Util.chars_string(:alpha)
    decimals = options[:decimals] || Util.chars_string(:decimal)
    symbols = options[:symbols] || Util.chars_string(:symbol)

    {alpha_bits, decimal_bits, symbol_bits} =
      entropy_bits(alpha, alphas, decimal, decimals, symbol, symbols)

    alpha_bits + decimal_bits + symbol_bits
  end

  @doc false
  def entropy_bits(alpha, alphas, decimal, decimals, symbol, symbols) do
    Util.validate_alpha(alphas)
    Util.validate_decimals(decimals)
    Util.validate_symbol(symbols)

    Util.validate_n_chars(alpha, alphas)
    Util.validate_n_chars(decimal, decimals)
    Util.validate_n_chars(symbol, symbols)

    alpha_bits = Util.bits(alpha, alphas)
    decimal_bits = Util.bits(decimal, decimals)
    symbol_bits = Util.bits(symbol, symbols)

    {alpha_bits, decimal_bits, symbol_bits}
  end

  defmacro __using__(opts) do
    quote do
      default_alphas = Chars.charlist!(:alpha)
      default_decimals = Chars.charlist!(:decimal)
      default_symbols = Chars.charlist!(:symbol)

      {alpha, decimal, symbol} =
        Util.default_n(
          unquote(opts)[:alpha],
          unquote(opts)[:decimal],
          unquote(opts)[:symbol]
        )

      alphas = unquote(opts)[:alphas] || default_alphas |> to_string()
      decimals = unquote(opts)[:decimals] || Chars.charlist!(:decimal) |> to_string()
      symbols = unquote(opts)[:symbols] || default_symbols |> to_string()

      Util.validate_alpha(alphas)
      Util.validate_decimals(decimals)
      Util.validate_symbol(symbols)

      Util.validate_n_chars(alpha, alphas)
      Util.validate_n_chars(decimal, decimals)
      Util.validate_n_chars(symbol, symbols)

      rand_bytes = unquote(opts[:rand_bytes])

      {alpha_bits, decimal_bits, symbol_bits} =
        RandomPassword.entropy_bits(
          alpha,
          alphas,
          decimal,
          decimals,
          symbol,
          symbols
        )

      defmodule __MODULE__.Empty do
        def generate, do: ""
        def info, do: "Empty string module"
      end

      def_mod = fn mod_name, bits, chars, rand_bytes ->
        if 0 < bits do
          defmodule mod_name, do: use(Puid, bits: bits, chars: chars, rand_bytes: rand_bytes)
        else
          defmodule mod_name do
            def generate, do: ""
            def info, do: %Puid.Info{characters: ""}
          end
        end
      end

      def_mod.(__MODULE__.Alpha, alpha_bits, alphas, rand_bytes)

      def_mod.(__MODULE__.Decimal, decimal_bits, decimals, rand_bytes)

      def_mod.(__MODULE__.Symbol, symbol_bits, symbols, rand_bytes)

      @doc """
      Generate random password

      ## Example
          defmodule(Passwd, do: use(RandomPassword, alpha: 16, decimal: 4, symbol: 2))

          Passwd.generate()
          "vwt8FauEN+spr5{m1Rhso7"
      """

      def generate do
        alpha = __MODULE__.Alpha.generate()
        decimal = __MODULE__.Decimal.generate()
        symbol = __MODULE__.Symbol.generate()

        (alpha <> decimal <> symbol)
        |> to_charlist()
        |> Enum.shuffle()
        |> to_string()
      end

      mod_info = %RandomPassword.Info{
        entropy_bits: (alpha_bits + decimal_bits + symbol_bits) |> Float.round(2),
        alpha: alpha,
        alphas: __MODULE__.Alpha.info().characters(),
        decimal: decimal,
        decimals: __MODULE__.Decimal.info().characters(),
        symbol: symbol,
        symbols: __MODULE__.Symbol.info().characters(),
        length: alpha + decimal + symbol
      }

      @random_password_mod_info mod_info

      @doc """
      `RandomPassword` generated module info.

      ## Example
          defmodule(Password, do: use(RandomPassword, alpha: 16, decimal: 2, symbol: 1))

          Password.info()
              %RandomPassword.Info{
                alpha: 16,
                decimal: 2,
                entropy_bits: 102.66,
                symbol: 1,
                symbols: "!#$%&()*+,-./:;<=>?@[]^_{|}~"
              }
      """
      def info, do: @random_password_mod_info
    end
  end
end