lib/bbox/votes.ex

defmodule Bbox.Votes do
  import Ecto.Query
  alias Bbox.Repo
  alias Bbox.Votes.Vote
  alias Bbox.Candidates.Candidate
  alias Bbox.Voters
  alias Bbox.Voters.Voter

  @doc """
  Insert new vote.
  """
  @spec insert(binary(), %Candidate{}) :: {:ok, %Vote{}}
  def insert(private_key, candidate) do
    with %Voter{} = voter <- Voters.get_by_private_key!(private_key) do
      private_key =
        private_key
        |> Base.decode32!()
        |> X509.PrivateKey.from_pem!()

      signature =
        candidate.code
        |> :public_key.sign(:sha256, private_key)
        |> Base.encode32()

      vote = %Vote{}
      vote = Ecto.build_assoc(voter, :votes, vote)
      vote = Ecto.build_assoc(candidate, :votes, vote)
      vote = Vote.changeset(vote, %{signature: signature})
      {:ok, vote} = Repo.insert(vote)

      :telemetry.execute([:bbox, :votes, :insert], %{vote: vote})

      {:ok, vote}
    end
  end

  @doc """
  Validate vote signature.
  """
  @spec is_valid?(binary(), %Vote{}) :: {:ok, %Vote{}} | {:error, charlist()}
  def is_valid?(private_key, vote) do
    public_key =
      private_key
      |> Base.decode32!()
      |> X509.PrivateKey.from_pem!()
      |> X509.PublicKey.derive()

    vote = Repo.preload(vote, [:voter, :candidate])
    signature = Base.decode32!(vote.signature)

    is_valid = :public_key.verify(vote.candidate.code, :sha256, signature, public_key)

    :telemetry.execute([:bbox, :votes, :is_valid?], %{
      is_valid: is_valid,
      signature: signature,
      public_key: public_key,
      vote: vote
    })

    is_valid
  end

  @doc """
  List votes.
  """
  @spec list!() :: [%Vote{}]
  def list!() do
    votes =
      Vote
      |> Repo.all()

    :telemetry.execute([:bbox, :votes, :list!], %{votes: votes})

    votes
  end
end