lib/ed25519.ex

defmodule Ed25519 do
  import Bitwise

  @moduledoc """
  Ed25519 signature functions

  This is mostly suitable as part of a pure Elixir solution.

  ## Configuration

  *No configuration is needed* in most cases. However, if needed, a custom hash
  function can be configured. As per the specification - `sha512` is the default.

  `config/config.exs`

      import Config

      # The hash function will be invoked as 'Blake2.hash2b(payload, 16)'
      config :ed25519,
        hash_fn: {Blake2, :hash2b, [], [16]}

      # The hash function will be invoked as ':crypto.hash(:sha256, payload)'
      config :ed25519,
        hash_fn: {:crypto, :hash, [:sha256], []}

  """
  @typedoc """
  public or secret key
  """
  @type key :: binary

  @typedoc """
  computed signature
  """
  @type signature :: binary

  @p 57_896_044_618_658_097_711_785_492_504_343_953_926_634_992_332_820_282_019_728_792_003_956_564_819_949
  @l 7_237_005_577_332_262_213_973_186_563_042_994_240_857_116_359_379_907_606_001_950_938_285_454_250_989
  @d -4_513_249_062_541_557_337_682_894_930_092_624_173_785_641_285_191_125_241_628_941_591_882_900_924_598_840_740
  @i 19_681_161_376_707_505_956_807_079_304_988_542_015_446_066_515_923_890_162_744_021_073_123_829_784_752
  @t254 28_948_022_309_329_048_855_892_746_252_171_976_963_317_496_166_410_141_009_864_396_001_978_282_409_984
  @base {15_112_221_349_535_400_772_501_151_409_588_531_511_454_012_693_041_857_206_046_113_283_949_847_762_202,
         46_316_835_694_926_478_169_428_394_003_475_163_141_307_993_866_256_225_615_783_033_603_165_251_855_960}

  defp xrecover(y) do
    xx = (y * y - 1) * inv(@d * y * y + 1)
    x = expmod(xx, div(@p + 3, 8), @p)

    x =
      case (x * x - xx) |> mod(@p) do
        0 -> x
        _ -> mod(x * @i, @p)
      end

    case x |> mod(2) do
      0 -> @p - x
      _ -> x
    end
  end

  defp mod(x, _y) when x == 0, do: 0
  defp mod(x, y) when x > 0, do: rem(x, y)
  defp mod(x, y) when x < 0, do: rem(y + rem(x, y), y)

  # __using__ Macro generates the hash function at compile time, which allows the
  # hashing function to be configurable without runtime overhead
  use Ed25519.Hash
  defp hashint(m), do: m |> hash |> :binary.decode_unsigned(:little)

  # :crypto.mod_pow chokes on negative inputs, so we feed it positive values
  # only and patch up the result if necessary
  defp expmod(_b, 0, _m), do: 1

  defp expmod(b, e, m) when b > 0 do
    b |> :crypto.mod_pow(e, m) |> :binary.decode_unsigned()
  end

  defp expmod(b, e, m) do
    i = b |> abs() |> :crypto.mod_pow(e, m) |> :binary.decode_unsigned()

    cond do
      mod(e, 2) == 0 -> i
      i == 0 -> i
      true -> m - i
    end
  end

  defp inv(x), do: x |> expmod(@p - 2, @p)

  defp edwards({x1, y1}, {x2, y2}) do
    x = (x1 * y2 + x2 * y1) * inv(1 + @d * x1 * x2 * y1 * y2)
    y = (y1 * y2 + x1 * x2) * inv(1 - @d * x1 * x2 * y1 * y2)
    {mod(x, @p), mod(y, @p)}
  end

  defp encodepoint({x, y}) do
    val =
      y
      |> band(0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)
      |> bor((x &&& 1) <<< 255)

    <<val::little-size(256)>>
  end

  defp decodepoint(<<n::little-size(256)>>) do
    xc = n |> bsr(255)
    y = n |> band(0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)
    x = xrecover(y)

    point =
      case x &&& 1 do
        ^xc -> {x, y}
        _ -> {@p - x, y}
      end

    if isoncurve(point), do: point, else: raise("Point off Edwards curve")
  end

  defp decodepoint(_), do: raise("Provided value not a key")

  defp isoncurve({x, y}), do: (-x * x + y * y - 1 - @d * x * x * y * y) |> mod(@p) == 0

  @doc """
  Returns whether a given `key` lies on the ed25519 curve.
  """
  @spec on_curve?(key) :: boolean
  def on_curve?(key) do
    try do
      decodepoint(key)
      true
    rescue
      _error -> false
    end
  end

  @doc """
  Sign a message

  If only the secret key is provided, the public key will be derived therefrom.
  This adds significant overhead.
  """
  @spec signature(binary, key, key) :: signature
  def signature(m, sk, pk \\ nil)
  def signature(m, sk, nil), do: signature(m, sk, derive_public_key(sk))

  def signature(m, sk, pk) do
    h = hash(sk)
    a = a_from_hash(h)
    r = hashint(:binary.part(h, 32, 32) <> m)
    bigr = r |> scalarmult(@base) |> encodepoint
    s = mod(r + hashint(bigr <> pk <> m) * a, @l)
    bigr <> <<s::little-size(256)>>
  end

  defp a_from_hash(<<h::little-size(256), _rest::binary>>) do
    @t254 +
      (h
       |> band(0xF3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8))
  end

  defp scalarmult(0, _pair), do: {0, 1}

  defp scalarmult(e, p) do
    q = e |> div(2) |> scalarmult(p)
    q = edwards(q, q)

    case e &&& 1 do
      1 -> edwards(q, p)
      _ -> q
    end
  end

  defp clamp(c) do
    c
    |> band(~~~7)
    |> band(~~~(128 <<< (8 * 31)))
    |> bor(64 <<< (8 * 31))
  end

  @doc """
  validate a signed message
  """
  @spec valid_signature?(signature, binary, key) :: boolean
  def valid_signature?(<<for_r::binary-size(32), s::little-size(256)>>, m, pk)
      when byte_size(pk) == 32 do
    r = decodepoint(for_r)
    a = decodepoint(pk)
    h = hashint(encodepoint(r) <> pk <> m)
    scalarmult(s, @base) == edwards(r, scalarmult(h, a))
  end

  def valid_signature?(_s, _m_, _pk), do: false

  @doc """
  Generate a secret/public key pair

  Returned tuple contains `{random_secret_key, derived_public_key}`
  """
  @spec generate_key_pair :: {key, key}
  def generate_key_pair do
    secret = :crypto.strong_rand_bytes(32)
    {secret, derive_public_key(secret)}
  end

  @doc """
  Generate a secret/public key pair from supplied secret

  Returned tuple contains `{secret_key, derived_public_key}`
  """
  @spec generate_key_pair(key) :: {key, key}
  def generate_key_pair(secret) do
    {secret, derive_public_key(secret)}
  end

  @doc """
  derive the public signing key from the secret key
  """
  @spec derive_public_key(key) :: key
  def derive_public_key(sk) do
    sk
    |> hash
    |> a_from_hash
    |> scalarmult(@base)
    |> encodepoint
  end

  @doc """
  Derive the x25519/curve25519 encryption key from the ed25519 signing key


  By converting an `EdwardsPoint` on the Edwards model to the corresponding `MontgomeryPoint` on the Montgomery model

  Handles either `:secret` or `:public` keys as indicated in the call

  May `raise` on an invalid input key or unknown atom

  See: https://blog.filippo.io/using-ed25519-keys-for-encryption
  """
  @spec to_curve25519(key, atom) :: key
  def to_curve25519(key, which)

  def to_curve25519(ed_public_key, :public) do
    {_, y} = decodepoint(ed_public_key)
    u = mod((1 + y) * inv(1 - y), @p)
    <<u::little-size(256)>>
  end

  def to_curve25519(ed_secret_key, :secret) do
    <<digest32::little-size(256), _::binary-size(32)>> = :crypto.hash(:sha512, ed_secret_key)
    <<clamp(digest32)::little-size(256)>>
  end
end