defmodule Eddy do @moduledoc """ ![Curvy](https://raw.githubusercontent.com/libitx/eddy/main/media/poster.png) ![License](https://img.shields.io/github/license/libitx/eddy?color=informational) Meet Eddy! A steady little `Ed25519` library for Elixir. Ed25519 is an elliptic curve that can be used in signature schemes and ECDH shared secrets. ## Highlights - Pure Elixir implementation of `Ed25519` (no external dependencies) - Secure generation of EdDSA key pairs - Ed25519 signature schemes - X25519 (ECDH) shared secrets - Build your own crypto - customisable hash algo ## Instalation The package can be installed by adding `eddy` to your list of dependencies in `mix.exs`. ```elixir def deps do [ {:eddy, "~> 1.0.0"} ] end ``` ## Quick start ### 1. Key generation Generate new EdDSA keypairs. ```elixir iex> privkey = Eddy.generate_key() %Eddy.PrivKey{} iex> pubkey = Eddy.get_pubkey(privkey) %Eddy.PubKey{} ``` ### 2. Sign messages Sign messages with a private key. ```elixir iex> sig = Eddy.sign("test", privkey) %Eddy.Sig{} ``` ### 3. Verify messages Verify a signature against the message and a public key. ```elixir iex> Eddy.verify(sig, "test", pubkey) true iex> Eddy.verify(sig, "test", wrong_pubkey) false ``` ### 4. X25519 shared secrets ECDH shared secrets are computed by multiplying a public key with a private key. The operation yields the same result in both directions. ```elixir iex> s1 = Eddy.get_shared_secret(priv_a, pubkey_b) iex> s2 = Eddy.get_shared_secret(priv_b, pubkey_a) iex> s1 == s2 true ``` ## Custom hash function As per the [rfc8032 spec](https://www.rfc-editor.org/rfc/rfc8032#section-5.1), by default Eddy uses the `sha512` hash function internally. Optionally, a custom hash function can be configured in your application's `config/config.exs`. *The custom hash function **must** return 64 bytes.* ```elixir import Config # The hash function will be invoked as `:crypto.hash(:sha3_512, payload)` config :eddy, hash_fn: {:crypto, :hash, [:sha3_512], []} # The hash function will be invoked as `B3.hash(payload, length: 64)` config :eddy, hash_fn: {B3, :hash, [], [[length: 64]]} ``` """ use Eddy.Hash alias Eddy.{ ExtendedPoint, Point, PrivKey, PubKey, Serializable, Sig, Util, X25519, } alias Serializable.Encoder @typedoc """ Private Key. Are represented as [`PrivKey structs`](`t:Eddy.PrivKey.t/0`) or 32 byte binaries. """ @type privkey() :: PrivKey.t() | binary() @typedoc """ Public Key. Are represented as [`PubKey structs`](`t:Eddy.PubKey.t/0`) or 32 byte binaries. """ @type pubkey() :: PubKey.t() | binary() @typedoc """ Signature. Are represented as [`Sig structs`](`t:Eddy.Sig.t/0`) or 64 byte binaries. """ @type sig() :: Sig.t() | binary() @typedoc """ Binary encoding format. Eddy can encoding keys and signatures in raw, base16 or base64 encodings. Hex is as base16, but with lower case letters. """ @type encoding() :: :raw | :base16 | :base64 | :hex @typedoc false @type encodable() :: Point.t() | PrivKey.t() | PubKey.t() | Sig.t() @params %{ p: 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED, a: -0x01, d: 0x52036CEE2B6FFE738CC740797779E89800700A4D4141D8AB75EB4DCA135978A3, G: %Point{ x: 0x216936D3CD6E53FEC0A4E231FDD6DC5C692CC7609525A7B2C9562D608F25D51A, y: 0x6666666666666666666666666666666666666666666666666666666666666658, }, l: 0x1000000000000000000000000000000014DEF9DEA2F79CD65812631A5CF5D3ED, h: 0x08, } @doc false defdelegate mod(number, modulo \\ @params.p), to: Util @doc """ Returns the `Ed25519` elliptic curve parameters. """ @spec params() :: map() def params(), do: @params @doc """ Generates a new random private key. The private key can optionally be returned as a raw or encoded binary. ## Options - `:encoding` - Optionally encode with a binary `t:encoding/0`. ## Examples ```elixir iex> privkey = Eddy.generate_key() %Eddy.PrivKey{} iex> privkey = Eddy.generate_key(encoding: :raw) <<182, 7, 194, 105, 23, 114, 238, 195, 188, 101, 41, 99, 155, 2, 174, 52, 187, 235, 72, 4, 221, 189, 111, 49, 33, 240, 224, 53, 161, 77, 253, 50>> iex> privkey = Eddy.generate_key(encoding: :hex) "3056ade0bc0215aa21db1dfddd3ea6786a4127b28efddb7e9b6af9845b8ef57a" ``` """ @spec generate_key(keyword()) :: privkey() def generate_key(opts \\ []) do d = :crypto.strong_rand_bytes(32) case Keyword.get(opts, :encoding) do nil -> %PrivKey{ d: d } enc -> encode(d, enc) end end @doc """ Takes a private key and returns the corresponding public key. Acceps a private key struct or raw binary. The public key can optionally be returned as a raw or encoded binary. ## Options - `:encoding` - Optionally encode with a binary `t:encoding/0`. ## Examples ```elixir iex> pubkey = Eddy.get_pubkey(privkey) %Eddy.PubKey{} iex> pubkey = Eddy.get_pubkey(privkey, encoding: :hex) "9dcfaa3dca4a02da72c500885dd6824a7c9abb76b88f9e3f10378f33c56d2465" ``` """ @spec get_pubkey(privkey(), keyword()) :: pubkey() def get_pubkey(privkey, opts \\ []) def get_pubkey(%PrivKey{d: d}, opts), do: get_pubkey(d, opts) def get_pubkey(privkey, opts) when is_binary(privkey) and byte_size(privkey) == 32 do {point, _, _, _} = calculate_point(privkey) case Keyword.get(opts, :encoding) do nil -> %PubKey{point: point} enc -> encode(point, enc) end end @doc """ Computes an ECDH shared secret from the given private and public keys. Acceps both keys as structs or raw binaries. Returns a 32 byte raw binary which can optionally be encoded. ## Options - `:encoding` - Optionally encode with a binary `t:encoding/0`. ## Examples ```elixir iex> secret = Eddy.get_shared_secret(privkey, pubkey) <<109, 226, 95, 89, 0, 39, 15, 239, 181, 187, 28, 242, 106, 214, 8, 227, 116, 66, 47, 52, 133, 10, 111, 113, 107, 173, 191, 203, 207, 135, 18, 114>> iex> secret = Eddy.get_shared_secret(privkey, pubkey, encoding: :hex) "6de25f5900270fefb5bb1cf26ad608e374422f34850a6f716badbfcbcf871272" ``` """ @spec get_shared_secret(privkey(), pubkey(), keyword()) :: binary() def get_shared_secret(privkey, pubkey, opts \\ []) def get_shared_secret(%PrivKey{d: d}, pubkey, opts), do: get_shared_secret(d, pubkey, opts) def get_shared_secret(privkey, pubkey, opts) when is_binary(pubkey) and byte_size(pubkey) == 32 do with {:ok, pubkey, _rest} <- Encoder.parse(struct(PubKey), pubkey) do get_shared_secret(privkey, pubkey, opts) else _ -> raise "invalid pubkey" end end def get_shared_secret(d, %PubKey{point: point}, opts) when is_binary(d) and byte_size(d) == 32 do encoding = Keyword.get(opts, :encoding) {_, head, _, _} = calculate_point(d) u = X25519.from_point(point) head |> X25519.scalar_mult(u) |> encode(encoding) end @doc """ Signs the message with the given private key. Acceps a private key struct or raw binary. The signature can optionally be returned as a raw or encoded binary. ## Options - `:encoding` - Optionally encode with a binary `t:encoding/0`. ## Examples ```elixir iex> sig = Eddy.sign("test", privkey) %Eddy.Sig{} iex> sig = Eddy.sign("test", privkey, encoding: :base64) "uS5X1ek6+aHAYGMEMWLF5+O9W8rxK6HDHHI2QOoBOReVaAsf5sFSI3Dqvms4LUtecW/ILAOaWS1L737ye6dkBg==" ``` """ @spec sign(binary(), privkey(), keyword()) :: sig() def sign(message, privkey, opts \\ []) def sign(message, %PrivKey{d: d}, opts), do: sign(message, d, opts) def sign(message, privkey, opts) when is_binary(message) and is_binary(privkey) and byte_size(privkey) == 32 do {point, _head, prefix, scalar} = calculate_point(privkey) r = hash(prefix <> message) |> :binary.decode_unsigned(:little) |> mod(@params.l) r_point = Point.mul(@params[:G], r) k = Encoder.serialize(r_point) |> Kernel.<>(Encoder.serialize(point)) |> Kernel.<>(message) |> hash() |> :binary.decode_unsigned(:little) |> mod(@params.l) s = mod(r + k * scalar, @params.l) sig = %Sig{r: r_point, s: s} case Keyword.get(opts, :encoding) do nil -> sig enc -> encode(sig, enc) end end @doc """ Verifies the signature against the given message and public key. Returns a boolean or error tuple. Acceps a public key struct or raw binary. The signature can optionally be decoded from a raw or encoded binary. ## Options - `:encoding` - Optionally decode from a binary `t:encoding/0`. ## Examples ```elixir iex> Eddy.verify(sig, "test", pubkey) true iex> sig = "uS5X1ek6+aHAYGMEMWLF5+O9W8rxK6HDHHI2QOoBOReVaAsf5sFSI3Dqvms4LUtecW/ILAOaWS1L737ye6dkBg==" iex> Eddy.verify(sig, "test", pubkey, encoding: :base64) true ``` """ @spec verify(sig(), binary(), pubkey(), keyword()) :: boolean() | {:error, term()} def verify(sig, message, pubkey, opts \\ []) def verify(sig, message, pubkey, opts) when is_binary(pubkey) and byte_size(pubkey) == 32 do with {:ok, pubkey, _rest} <- Encoder.parse(struct(PubKey), pubkey) do verify(sig, message, pubkey, opts) end end def verify(sig, message, %PubKey{} = pubkey, opts) when is_binary(sig) do encoding = Keyword.get(opts, :encoding) with {:ok, sig} when byte_size(sig) == 64 <- Util.decode(sig, encoding), {:ok, sig, ""} <- Encoder.parse(struct(Sig), sig) do verify(sig, message, pubkey, opts) else {:ok, _} -> {:error, {:decode_error, "invalid sig length"}} {:ok, _, _} -> {:error, {:decode_error, "invalid sig length"}} end end def verify(%Sig{r: r_point, s: s}, message, %PubKey{point: point}, _opts) when is_binary(message) do sb = @params[:G] |> ExtendedPoint.from_point() |> ExtendedPoint.mul!(s) k = r_point |> Encoder.serialize() |> Kernel.<>(Encoder.serialize(point)) |> Kernel.<>(message) |> hash() |> :binary.decode_unsigned(:little) |> mod(@params.l) ka = point |> ExtendedPoint.from_point() |> ExtendedPoint.mul!(k) r_point |> ExtendedPoint.from_point() |> ExtendedPoint.add(ka) |> ExtendedPoint.sub(sb) |> ExtendedPoint.mul!(@params.h) |> ExtendedPoint.eq(%ExtendedPoint{x: 0, y: 1, z: 1, t: 0}) end # Calculates the point from a private key, along with additional info @spec calculate_point(binary()) :: {Point.t(), binary(), binary(), integer()} defp calculate_point(privkey) when is_binary(privkey) do <> = hash(privkey) scalar = head |> X25519.adjust_bytes() |> :binary.decode_unsigned(:little) |> mod(@params.l) point = Point.mul(@params[:G], scalar) {point, head, prefix, scalar} end # Encode helper method @spec encode(encodable() | binary(), atom()) :: encodable() | binary() defp encode(item, enc) when is_binary(item), do: Util.encode(item, enc) defp encode(item, enc), do: Encoder.serialize(item) |> Util.encode(enc) end