lib/crypto.ex

defmodule Tezex.Crypto do
  @moduledoc """
  A set of functions to check Tezos signed messages and verify a public key corresponds to a wallet address (public key hash).
  """
  alias Tezex.Crypto.{Base58Check, ECDSA}

  @doc """
  Verify that `address` is the public key hash of `pubkey` and that `signature` is a valid signature for `message` signed with the private key corresponding to public key `pubkey`.

  ## Examples
      iex> address = "tz2BC83pvEAag6r2ZV7kPghNAbjFoiqhCvZx"
      iex> address_b = "tz1burnburnburnburnburnburnburjAYjjX"
      iex> signature = "spsig1ZNQaUKNERZSiEiNviqa5EAPkcNASXhfkXtxRatZTDZAnUB4Ra2Jus8b1oEpFnPx8Z6g28pd8vK3R8nPK29JDU5FiSLH5T"
      iex> message = "05010000007154657a6f73205369676e6564204d6573736167653a207369676e206d6520696e20617320747a32424338337076454161673672325a56376b5067684e41626a466f69716843765a78206f6e206f626a6b742e636f6d20617420323032312d31302d30345431383a35393a31332e3939305a"
      iex> public_key = "sppk7aBerAEA6tv4wzg6FnK7i5YrGtEGFVvNjWhc2QX8bhzpouBVFSW"
      iex> Tezex.Crypto.check_signature(address, signature, message, public_key)
      true
      iex> Tezex.Crypto.check_signature(address_b, signature, message, public_key)
      false
      iex> Tezex.Crypto.check_signature(address, signature, "", public_key)
      false
  """
  @spec check_signature(binary, binary, binary, binary) :: boolean
  def check_signature("tz" <> _ = address, signature, message, pubkey) do
    with :ok <- check_address(address, pubkey),
         true <- verify_signature(signature, message, pubkey) do
      true
    else
      _ -> false
    end
  end

  defp decode_signature(data) do
    data
    |> decode_base58()
    |> binary_part(5, 64)
  end

  defp decode_base58(data) do
    Base58Check.decode58!(data)
  end

  defp hash_message(message) do
    try do
      iodata = :binary.decode_hex(message)
      :enacl.generichash(32, iodata)
    rescue
      ArgumentError -> ""
    end
  end

  @doc """
  Verify that `signature` is a valid signature for `message` signed with the private key corresponding to public key `pubkey`
  """
  @spec verify_signature(binary, binary, binary) :: boolean
  def verify_signature("ed" <> _sig = signature, message, pubkey) do
    # tz1…
    message_hash = hash_message(message)
    signature = decode_signature(signature)

    try do
      pubkey =
        pubkey
        |> decode_base58()
        |> binary_part(4, 32)

      :enacl.sign_verify_detached(signature, message_hash, pubkey)
    rescue
      ArgumentError -> false
    end
  end

  def verify_signature("sp" <> _sig = signature, message, pubkey) do
    # tz2…
    message_hash = hash_message(message)
    signature = decode_signature(signature)

    pubkey =
      pubkey
      |> decode_base58()
      |> binary_part(4, 33)

    :ok == :libsecp256k1.ecdsa_verify_compact(message_hash, signature, pubkey)
  end

  def verify_signature("p2" <> _sig = signature, msg, pubkey) do
    # tz3…
    <<54, 240, 44, 52>> <> <<sig::binary-size(64)>> <> _ = decode_base58(signature)
    <<r::unsigned-integer-size(256), s::unsigned-integer-size(256)>> = sig
    signature = %EllipticCurve.Signature{r: r, s: s}

    message = :binary.decode_hex(msg)

    <<0x03, 0xB2, 0x8B, 0x7F>> <> <<public_key::binary-size(33)>> <> _ = decode_base58(pubkey)

    public_key = ECDSA.decode_public_key(public_key, :prime256v1)

    ECDSA.verify?(message, signature, public_key,
      hashfunc: fn msg -> :enacl.generichash(32, msg) end
    )
  end

  @doc """
  Verify that `address` is the public key hash of `pubkey`.

  ## Examples
      iex> pubkey = "sppk7aBerAEA6tv4wzg6FnK7i5YrGtEGFVvNjWhc2QX8bhzpouBVFSW"
      iex> Tezex.Crypto.check_address("tz2BC83pvEAag6r2ZV7kPghNAbjFoiqhCvZx", pubkey)
      :ok
      iex> Tezex.Crypto.check_address("tz1burnburnburnburnburnburnburjAYjjX", pubkey)
      {:error, :mismatch}
  """
  @spec check_address(any, any) :: :ok | {:error, :mismatch | :unknown_pubkey_format}
  def check_address(address, pubkey) do
    case derive_address(pubkey) do
      {:ok, ^address} ->
        :ok

      {:ok, _derived} ->
        {:error, :mismatch}

      err ->
        err
    end
  end

  @doc """
  Derive public key hash (Tezos wallet address) from public key

  ## Examples
      iex> Tezex.Crypto.derive_address("edpktsPhZ8weLEXqf4Fo5FS9Qx8ZuX4QpEBEwe63L747G8iDjTAF6w")
      {:ok, "tz1LKpeN8ZSSFNyTWiBNaE4u4sjaq7J1Vz2z"}
      iex> Tezex.Crypto.derive_address("sppk7aBerAEA6tv4wzg6FnK7i5YrGtEGFVvNjWhc2QX8bhzpouBVFSW")
      {:ok, "tz2BC83pvEAag6r2ZV7kPghNAbjFoiqhCvZx"}
      iex> Tezex.Crypto.derive_address("p2pk65yRxCX65k6qRPrbqGWvfW5JnLB1p3dn1oM5o9cyqLKPPhJaBMa")
      {:ok, "tz3bPFa6mGv8m4Ppn7w5KSDyAbEPwbJNpC9p"}
  """
  @spec derive_address(binary) :: {:error, :unknown_pubkey_format} | {:ok, binary}
  def derive_address("edpk" <> _ = pubkey) do
    # tz1…
    pkh = <<6, 161, 159>>
    <<13, 15, 37, 217>> <> <<public_key::binary-size(32)>> <> _ = decode_base58(pubkey)

    derive_address(public_key, pkh)
  end

  def derive_address("sppk" <> _ = pubkey) do
    # tz2…
    pkh = <<6, 161, 161>>
    <<3, 254, 226, 86>> <> <<public_key::binary-size(33)>> <> _ = decode_base58(pubkey)

    derive_address(public_key, pkh)
  end

  def derive_address("p2pk" <> _ = pubkey) do
    # tz3…
    pkh = <<6, 161, 164>>
    <<3, 178, 139, 127>> <> <<public_key::binary-size(33)>> <> _ = decode_base58(pubkey)

    derive_address(public_key, pkh)
  end

  def derive_address(_) do
    {:error, :unknown_pubkey_format}
  end

  defp derive_address(pubkey, pkh) do
    derived =
      :enacl.generichash(20, pubkey)
      |> Tezex.Crypto.Base58Check.encode(pkh)

    {:ok, derived}
  end
end