Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove libsodium #86

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: 2
jobs:
build:
docker:
- image: grappigpanda/elixir-libsodium:1.13_otp24
- image: cimg/elixir:1.13.4-erlang-24.3
environment:
MIX_ENV: test

Expand All @@ -19,6 +19,7 @@ jobs:
- v1-build-cache-{{ .Branch }}
- v1-build-cache

- run: mix local.hex --force
- run: mix do deps.get --only test, compile
- run: mix test
- run: mix credo
Expand Down
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
elixir 1.13
erlang 24.1
erlang 24.3.4.14
47 changes: 12 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,12 @@ use Paseto in [an insecure way](https://auth0.com/blog/critical-vulnerabilities-

## Considerations for using this library

There are a few library/binary requirements required in order for the Paseto
There are a few library/binary requirements required in order for the Paseto
library to work on any computer:
1. Erlang version >= 20.1
1. Erlang version >= 24.3
* This is required because this was the first Erlang version to introduce
crypto:sign/5.
2. libsodium >= 1.0.13
* This is required for cryptography used in Paseto.
* This can be found at https://github.com/jedisct1/libsodium
3. openssl >= 1.1
2. openssl >= 1.1
* This is needed for XChaCha-Poly1305 used for V2.Local Paseto

## Want to use this library through Guardian or Plugs?
Expand Down Expand Up @@ -72,8 +69,8 @@ This decodes to:
```
* Key used in this example (hex-encoded):
```
707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f
```
707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f
```
* Footer:
```
Paragon Initiative Enterprises
Expand Down Expand Up @@ -112,27 +109,26 @@ To learn what each version means, please see [this page in the documentation](ht

### Generating a token
```elixir
iex> {:ok, pk, sk} = Salty.Sign.Ed25519.keypair()
iex> keypair = {pk, sk}
iex> token = Paseto.generate_token("v2", "public", "This is a test message", keypair)
iex> {:ok, sk, _pk} = Paseto.Crypto.Ed25519.generate_keypair()
iex> token = Paseto.generate_token("v2", "public", "This is a test message", sk)
"v2.public.VGhpcyBpcyBhIHRlc3QgbWVzc2FnZSe-sJyD2x_fCDGEUKDcvjU9y3jRHxD4iEJ8iQwwfMUq5jUR47J15uPbgyOmBkQCxNDydR0yV1iBR-GPpyE-NQw"
```

In short, we generate a keypair using [libsalty2](https://github.com/Ianleeclark/libsalty2) (libsodium elixir bindings) and generate the token using that keypair.
In short, first we generate a keypair and then a token using the secret key.

P.S. If you're confused about how to serialize the above keys, you can use functions
from the [`Base`](https://hexdocs.pm/elixir/Base.html) module:

```elixir
iex> {:ok, pk, sk} = Salty.Sign.Ed25519.keypair()
iex> pk |> Base.encode16(case: :lower)
iex> {:ok, sk, pk} = Paseto.Crypto.Ed25519.generate_keypair()
iex> Base.encode16(pk, case: :lower)
"a17c258ffdd864b3614bd445465ff96e0b16e8509e28e7ba60734f7c433ab7e8"
```

### Parsing a token
```elixir
iex> token = "v2.public.VGhpcyBpcyBhIHRlc3QgbWVzc2FnZSe-sJyD2x_fCDGEUKDcvjU9y3jRHxD4iEJ8iQwwfMUq5jUR47J15uPbgyOmBkQCxNDydR0yV1iBR-GPpyE-NQw"
iex> Paseto.parse_token(token, keypair)
iex> Paseto.parse_token(token, pk)
{:ok,
%Paseto.Token{
footer: nil,
Expand All @@ -147,26 +143,7 @@ More info can be found in the [HexDocs][].

## Installation

You need libsodium installed on your machine.

```bash
# Installing on FreeBSD
$ cd /usr/ports/security/libsodium/ && make install clean

# Installing on Ubuntu
$ sudo apt install libsodium-dev

# Installing on Fedora
$ dnf install libsodium-devel

# Redhat & Cent OS
$ yum install libsodium-devel

# Installing on OSX
$ brew install libsodium
```

The package can be installed by adding `paseto` to your list of
The package can be installed by adding `paseto` to your list of
dependencies in `mix.exs`:

```elixir
Expand Down
10 changes: 6 additions & 4 deletions lib/paseto.ex
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ defmodule Paseto do
* footer: An optional value, often used for storing keyIDs or other similar info.

# Examples:
iex> {:ok, pk, sk} = Salty.Sign.Ed25519.keypair()
iex> {:ok, sk, pk} = Paseto.Crypto.Ed25519.generate_keypair()
iex> token = generate_token("v2", "public", "This is a test message", sk)
"v2.public.VGhpcyBpcyBhIHRlc3QgbWVzc2FnZSe-sJyD2x_fCDGEUKDcvjU9y3jRHxD4iEJ8iQwwfMUq5jUR47J15uPbgyOmBkQCxNDydR0yV1iBR-GPpyE-NQw"
iex> Paseto.parse_token(token, pk)
Expand All @@ -110,7 +110,8 @@ defmodule Paseto do
version: "v2"
}}
"""
@spec generate_token(String.t(), String.t(), String.t(), binary, String.t()) :: String.t() | {:error, String.t()}
@spec generate_token(String.t(), String.t(), String.t(), binary, String.t()) ::
String.t() | {:error, String.t()}
def generate_token(version, purpose, payload, secret_key, footer \\ "") do
_generate_token(version, purpose, payload, secret_key, footer)
end
Expand All @@ -132,8 +133,9 @@ defmodule Paseto do

try do
{:ok, String.to_existing_atom("Elixir.Paseto.#{version}")}
rescue RuntimeError ->
{:error, "Invalid version selected. Only v1 & v2 supported."}
rescue
RuntimeError ->
{:error, "Invalid version selected. Only v1 & v2 supported."}
end
end
end
36 changes: 36 additions & 0 deletions lib/paseto/crypto/ed25519.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
defmodule Paseto.Crypto.Ed25519 do
@moduledoc """
Wrap crypto erlang functions to work with ED25519
"""

@dialyzer {:nowarn_function, sign: 2}
@spec sign(binary(), binary()) :: {:ok, binary()}
def sign(message, private_key) when byte_size(private_key) == 32 do
{:ok, ^private_key, public_key} = seed_keypair(private_key)
signature = :public_key.sign(message, :none, {:ed_pri, :ed25519, public_key, private_key})
{:ok, signature}
end

@dialyzer {:nowarn_function, verify_detached: 3}
@spec verify_detached(binary(), binary(), binary()) :: :ok | {:error, :invalid_signature}
def verify_detached(message, signature, public_key)
when byte_size(signature) == 64 and byte_size(public_key) == 32 do
if :public_key.verify(message, :none, signature, {:ed_pub, :ed25519, public_key}) do
:ok
else
{:error, :invalid_signature}
end
end

@spec seed_keypair(binary()) :: {:ok, binary(), binary()}
def seed_keypair(private_key) when byte_size(private_key) == 32 do
{pk, sk} = :crypto.generate_key(:eddsa, :ed25519, private_key)
{:ok, sk, pk}
end

@spec generate_keypair() :: {:ok, binary(), binary()}
def generate_keypair do
{pk, sk} = :crypto.generate_key(:eddsa, :ed25519)
{:ok, sk, pk}
end
end
101 changes: 101 additions & 0 deletions lib/paseto/crypto/xchacha20_poly1305.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
defmodule Paseto.Crypto.XChaCha20Poly1305 do
@moduledoc """
Implement XChaCha20Poly1305 encryption and decryption.
"""

@spec encrypt(binary(), binary(), binary(), binary()) :: {:ok, binary()}
def encrypt(message, aad, <<iv::192-bits>>, <<key::256-bits>>) do
# Perform the HChaCha20 operation to generate the subkey and nonce
{subkey, nonce} = xchacha20_subkey_and_nonce(key, iv)

# Perform the ChaCha20 operation to encrypt the message
block_encrypt(subkey, nonce, {aad, message})
end

@spec decrypt(binary(), binary(), binary(), binary()) :: {:ok, binary()}
def decrypt(encrypted, aad, <<iv::192-bits>>, <<key::256-bits>>) do
cipher_text_size = byte_size(encrypted) - 16
<<cipher_text::bytes-size(cipher_text_size), cipher_tag::128-bits>> = encrypted

# Perform the HChaCha20 operation to generate the subkey and nonce
{subkey, nonce} = xchacha20_subkey_and_nonce(key, iv)

# Perform the ChaCha20 operation to decrypt the message
block_decrypt(:chacha20_poly1305, subkey, nonce, {aad, cipher_text, cipher_tag})
end

defp xchacha20_subkey_and_nonce(key, <<nonce0::128-bits, nonce1::64-bits>>) do
subkey = hchacha20(key, nonce0)
nonce = <<0::32, nonce1::64-bits>>
{subkey, nonce}
end

defp hchacha20(key, nonce) do
# ChaCha20 has an internal blocksize of 512-bits (64-bytes).
# Let's use a Mask of random 64-bytes to blind the intermediate keystream.
mask = <<mask_h::128-bits, _::256-bits, mask_t::128-bits>> = :crypto.strong_rand_bytes(64)

<<state_2h::128-bits, _::256-bits, state_2t::128-bits>> =
:crypto.crypto_one_time(:chacha20, key, nonce, mask, true)

<<
x00::32-unsigned-little-integer,
x01::32-unsigned-little-integer,
x02::32-unsigned-little-integer,
x03::32-unsigned-little-integer,
x12::32-unsigned-little-integer,
x13::32-unsigned-little-integer,
x14::32-unsigned-little-integer,
x15::32-unsigned-little-integer
>> =
:crypto.exor(
<<mask_h::128-bits, mask_t::128-bits>>,
<<state_2h::128-bits, state_2t::128-bits>>
)

## The final step of ChaCha20 is `State2 = State0 + State1', so let's
## recover `State1' with subtraction: `State1 = State2 - State0'
<<
y00::32-unsigned-little-integer,
y01::32-unsigned-little-integer,
y02::32-unsigned-little-integer,
y03::32-unsigned-little-integer,
y12::32-unsigned-little-integer,
y13::32-unsigned-little-integer,
y14::32-unsigned-little-integer,
y15::32-unsigned-little-integer
>> = <<"expand 32-byte k", nonce::128-bits>>

<<
x00 - y00::32-unsigned-little-integer,
x01 - y01::32-unsigned-little-integer,
x02 - y02::32-unsigned-little-integer,
x03 - y03::32-unsigned-little-integer,
x12 - y12::32-unsigned-little-integer,
x13 - y13::32-unsigned-little-integer,
x14 - y14::32-unsigned-little-integer,
x15 - y15::32-unsigned-little-integer
>>
end

defp block_encrypt(key, iv, {aad, payload}) do
{cipher_text, cipher_tag} =
:crypto.crypto_one_time_aead(:chacha20_poly1305, key, iv, payload, aad, true)

{:ok, cipher_text <> cipher_tag}
catch
:error, :notsup -> raise_notsup()
end

defp block_decrypt(cipher, key, iv, {aad, payload, tag}) do
plain = :crypto.crypto_one_time_aead(cipher, key, iv, payload, aad, tag, false)
{:ok, plain}
catch
:error, :notsup -> raise_notsup()
end

defp raise_notsup do
raise "The algorithm chacha20_poly1305 is not supported by your Erlang/OTP installation. " <>
"Please make sure it was compiled with the correct OpenSSL/BoringSSL bindings"
end
end
8 changes: 3 additions & 5 deletions lib/paseto/utils/crypto.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule Paseto.Utils.Crypto do
@moduledoc false

alias Salty.Aead.Xchacha20poly1305Ietf
alias Paseto.Crypto.XChaCha20Poly1305

@doc """
AES-256 in counter mode for encrypting. Used for v1 local.
Expand Down Expand Up @@ -43,8 +43,7 @@ defmodule Paseto.Utils.Crypto do

def xchacha20_poly1305_encrypt(message, aad, nonce, key)
when byte_size(nonce) == 24 and byte_size(key) == 32 do
# NOTE: nsec (the `nil` value here, isn't used in libsodium.)
Xchacha20poly1305Ietf.encrypt(message, aad, nil, nonce, key)
XChaCha20Poly1305.encrypt(message, aad, nonce, key)
end

@doc """
Expand All @@ -70,8 +69,7 @@ defmodule Paseto.Utils.Crypto do

def xchacha20_poly1305_decrypt(message, aad, nonce, key)
when byte_size(nonce) == 24 and byte_size(key) == 32 do
# NOTE: Again, `nsec` isn't used.
Xchacha20poly1305Ietf.decrypt(nil, message, aad, nonce, key)
XChaCha20Poly1305.decrypt(message, aad, nonce, key)
rescue
err -> {:error, "Decrypt failed due to #{inspect(err)}"}
end
Expand Down
12 changes: 6 additions & 6 deletions lib/paseto/v2.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ defmodule Paseto.V2 do
alias Paseto.Token
alias Paseto.Utils
alias Paseto.Utils.Crypto
alias Salty.Sign.Ed25519
alias Paseto.Crypto.Ed25519

import Paseto.Utils, only: [b64_decode!: 1]

Expand Down Expand Up @@ -74,15 +74,15 @@ defmodule Paseto.V2 do
Handles signing the token for public use.

# Examples:
iex> {:ok, pk, sk} = Salty.Sign.Ed25519.keypair()
iex> {:ok, sk, pk} = Paseto.Crypto.Ed25519.generate_keypair()
iex> Paseto.V2.sign("Test Message", sk)
"v2.public.VGVzdAJxQsXSrgYBkcwiOnWamiattqhhhNN_1jsY-LR_YbsoYpZ18-ogVSxWv7d8DlqzLSz9csqNtSzDk4y0JV5xaAE"
"""
@spec sign(String.t(), String.t(), String.t()) :: String.t() | {:error, String.t()}
def sign(data, secret_key, footer \\ "") when byte_size(secret_key) == 64 do
def sign(data, secret_key, footer \\ "") when byte_size(secret_key) == 32 do
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

each key in Ed25519 is 32 bytes

pre_auth_encode = Utils.pre_auth_encode([@header_public, data, footer])

{:ok, sig} = Ed25519.sign_detached(pre_auth_encode, secret_key)
{:ok, sig} = Ed25519.sign(pre_auth_encode, secret_key)

Utils.b64_encode_token(@header_public, data <> sig, footer)
rescue
Expand All @@ -93,7 +93,7 @@ defmodule Paseto.V2 do
Handles verifying the signature belongs to the provided key.

# Examples:
iex> {:ok, pk, sk} = Salty.Sign.Ed25519.keypair()
iex> {:ok, sk, pk} = Paseto.Crypto.Ed25519.generate_keypair()
iex> Paseto.V2.sign("Test Message", sk)
"v2.public.VGVzdAJxQsXSrgYBkcwiOnWamiattqhhhNN_1jsY-LR_YbsoYpZ18-ogVSxWv7d8DlqzLSz9csqNtSzDk4y0JV5xaAE"
iex> Paseto.V2.verify("VGVzdAJxQsXSrgYBkcwiOnWamiattqhhhNN_1jsY-LR_YbsoYpZ18-ogVSxWv7d8DlqzLSz9csqNtSzDk4y0JV5xaAE", pk)
Expand All @@ -109,7 +109,7 @@ defmodule Paseto.V2 do

pre_auth_encode = Utils.pre_auth_encode([@header_public, data, decoded_footer])

:ok = Ed25519.verify_detached(sig, pre_auth_encode, public_key)
:ok = Ed25519.verify_detached(pre_auth_encode, sig, public_key)
{:ok, data}
rescue
_ -> {:error, "Failed to verify signature."}
Expand Down
3 changes: 1 addition & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ defmodule Paseto.MixProject do

def application do
[
extra_applications: [:logger]
extra_applications: [:logger, :public_key]
]
end

Expand All @@ -28,7 +28,6 @@ defmodule Paseto.MixProject do
{:credo, "~> 1.4", only: [:dev, :test], runtime: false},
{:hkdf, "~> 0.2.0"},
{:blake2, "~> 1.0"},
{:libsalty2, "~> 0.3.0"},
{:ex_doc, "~> 0.19", only: :dev, runtime: false},
{:stream_data, "~> 0.5.0", only: :test}
]
Expand Down
Loading