1.5 KiB · text History 4b6fe3b
defmodule OhMyExOhIrcd.IRC.Opers do
  @moduledoc """
  IRC operator authentication.

  Opers are configured statically in `config.exs` (or runtime config)
  under `:opers`, keyed by login name. Passwords are stored as bcrypt
  hashes — generate one with `Bcrypt.hash_pwd_salt("yourpassword")`.

  Example:

      config :oh_my_ex_oh_ircd, :opers, %{
        "admin" => %{
          password_hash: "$2b$12$...",
          host_mask: "*@*"          # restrict where this oper can log in from
        }
      }

  `host_mask` is an IRC glob (`*`, `?`) applied to the connection's
  `user@host` part. `"*@*"` means anywhere.
  """

  @spec verify(String.t(), String.t(), String.t()) ::
          :ok | {:error, :no_such_oper | :bad_password | :bad_host}
  def verify(name, password, user_at_host) do
    case lookup(name) do
      nil ->
        # Avoid timing-leak: still run a bcrypt verify against a dummy hash.
        _ = Bcrypt.no_user_verify()
        {:error, :no_such_oper}

      %{password_hash: hash, host_mask: mask} ->
        cond do
          not match_glob?(user_at_host, mask) -> {:error, :bad_host}
          not Bcrypt.verify_pass(password, hash) -> {:error, :bad_password}
          true -> :ok
        end
    end
  end

  defp lookup(name) when is_binary(name) do
    Application.get_env(:oh_my_ex_oh_ircd, :opers, %{})
    |> Map.get(name)
  end

  defp match_glob?(string, mask) do
    pattern =
      mask
      |> Regex.escape()
      |> String.replace("\\*", ".*")
      |> String.replace("\\?", ".")

    Regex.match?(~r/\A#{pattern}\z/i, string)
  end
end