Current section
Files
Jump to
Current section
Files
lib/poly1305.ex
defmodule Poly1305 do
require Chacha20
import Bitwise
@moduledoc """
Poly1305 message authentication
https://tools.ietf.org/html/rfc7539
"""
@typedoc """
Encryption key
"""
@type key :: <<_::32 * 8>>
@typedoc """
Per-message nonce
By convention, the first 4 bytes should be sender-specific.
The trailing 8 bytes may be as simple as a counter.
"""
@type nonce :: <<_::12 * 8 >>
@typedoc """
MAC tag
"""
@type tag :: <<_::16 * 8 >>
defp clamp(r), do: r &&& 0x0ffffffc0ffffffc0ffffffc0fffffff
defp split_key(k), do: {binary_part(k,0,16) |> :binary.decode_unsigned(:little) |> clamp,
binary_part(k,16,16) |> :binary.decode_unsigned(:little)}
defp p, do: 0x3fffffffffffffffffffffffffffffffb
@doc """
Compute a Message authentication code
The one-time key should never be reused.
"""
@spec hmac(binary,key) :: tag
def hmac(m,k) do
{r,s} = split_key(k)
process_message(m,r,0)+s |> :binary.encode_unsigned(:little) |> result_align
end
@doc false
def key_gen(k,n), do: Chacha20.block(k,n,0) |> binary_part(0,32)
defp result_align(s) when byte_size(s) >= 16, do: binary_part(s,0,16)
defp result_align(s) when byte_size(s) < 16, do: align_pad(s,16)
defp process_message(<<>>,_r,a), do: a
defp process_message(m,r,a) do
size = byte_size m
bend = Enum.min([16,size])
rest = size - bend
n = binary_part(m,0,bend)<><<1>> |> :binary.decode_unsigned(:little)
binary_part(m,bend,rest) |> process_message(r,rem((r * (a + n)) , p))
end
@doc """
authenticated encryption with additional data - encryption
- message to be encrypted
- shared secret key
- one-time use nonce
- additional authenticated data
The return value will be a tuple of `{ciphertext, MAC}`
The algorithm is applied as described in RFC7539:
- The key and nonce are used to encrypt the message with ChaCha20.
- The one-time MAC key is derived from the cipher key and nonce.
- The ciphertext and additional data are authenticated with the MAC
"""
@spec aead_encrypt(binary,key,nonce,binary) :: {binary, tag}
def aead_encrypt(m,k,n,a \\ "") do
otk = key_gen(k,n)
c = Chacha20.crypt(m,k,n,1)
md = align_pad(a,16)<>align_pad(c,16)<>msg_length(a)<>msg_length(c)
{c, hmac(md,otk)}
end
@doc """
authenticated encryption with additional data - decryption
- encrypted message
- shared secret key
- one-time use nonce
- additional authenticated data
- MAC
On success, returns the plaintext message. If the message cannot be
authenticated `:error` is returned.
"""
@spec aead_decrypt(binary,key,nonce,binary,tag) :: binary | :error
def aead_decrypt(c,k,n,a \\ "",t) do
otk = key_gen(k,n)
md = align_pad(a,16)<>align_pad(c,16)<>msg_length(a)<>msg_length(c)
m = Chacha20.crypt(c,k,n,1)
if hmac(md,otk) |> string_compare(t), do: m, else: :error
end
defp msg_length(s), do: s |> byte_size |> :binary.encode_unsigned(:little) |> align_pad(8)
defp align_pad(s,n) do
case s |> byte_size |> rem(n) do
0 -> s # Already the proper width
r -> s<>zeroes(n - r)
end
end
defp zeroes(n), do: zero_loop(<<>>, n)
defp zero_loop(z,0), do: z
defp zero_loop(z,n), do: zero_loop(z<><<0>>, n-1)
defp string_compare(a,b) when byte_size(a) != byte_size(b), do: false # They already know how long it should be, so this does not leak info
defp string_compare(a,b) when byte_size(a) == byte_size(b), do: cmp_strings_loop(a,b,0,byte_size(a))
defp cmp_strings_loop(<<>>,<<>>,c,s), do: c == s
defp cmp_strings_loop(<<a,resta::binary>>,<<b,restb::binary>>,c,s), do: cmp_strings_loop(resta, restb, c+(if a == b, do: 1, else: 2), s)
end