lib/on_flow/crypto.ex

defmodule OnFlow.Crypto do
  import Bitwise

  @doc """
  Generates a key pair.
  """
  @spec generate_keys() :: %{public_key: String.t(), private_key: String.t()}
  def generate_keys do
    {<<4>> <> public_key, private_key} = :crypto.generate_key(:ecdh, :secp256r1)
    public_key = Base.encode16(public_key, case: :lower)
    private_key = Base.encode16(private_key, case: :lower)

    %{public_key: public_key, private_key: private_key}
  end

  @doc """
  Signs the message with the given private key. Options are:

    * `:hash`, which defaults to `:sha3_256`
    * `:sign`, which defaults to `:secp256r1`
  """
  def sign(msg, private_key, opts \\ []) do
    msg
    |> signature(private_key, opts)
    |> rs_pair()
  end

  @doc false
  def signature(msg, private_key, opts) do
    hash = Keyword.get(opts, :hash, :sha3_256)
    sign = Keyword.get(opts, :sign, :secp256r1)

    :crypto.sign(:ecdsa, hash, msg, [private_key, sign])
  end

  @doc false
  def rs_pair(signature) do
    at = fn index ->
      <<n>> = binary_part(signature, index, 1)
      n
    end

    start_r = if (at.(1) &&& 0x80) == 1, do: 3, else: 2
    length_r = at.(start_r + 1)
    start_s = start_r + 2 + length_r
    length_s = at.(start_s + 1)

    r = binary_part(signature, start_r + 2, length_r)
    s = binary_part(signature, start_s + 2, length_s)

    # 256 >> 3
    n = 32
    final_signature = :binary.copy(<<0>>, n * 2)

    offset_r = max(n - byte_size(r), 0)
    start_r = max(0, byte_size(r) - n)
    final_signature = copy_into(final_signature, r, offset_r, start_r)

    offset_s = max(2 * n - byte_size(s), n)
    start_s = max(0, byte_size(s) - n)
    final_signature = copy_into(final_signature, s, offset_s, start_s)

    final_signature
  end

  @doc false
  def copy_into(destination, src, destination_offset \\ 0, start_index \\ 0) do
    prefix = :binary.part(destination, 0, destination_offset)
    src = :binary.part(src, start_index, byte_size(src) - start_index)
    suffix_length = byte_size(destination) - destination_offset - byte_size(src)
    suffix = :binary.part(destination, destination_offset + byte_size(src), suffix_length)

    prefix <> src <> suffix
  end
end