defmodule Eddy do
@moduledoc """


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
<<head::binary-32, prefix::binary-32>> = 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