366 lines
12 KiB
Elixir
366 lines
12 KiB
Elixir
defmodule Plug.Crypto do
|
|
@moduledoc """
|
|
Namespace and module for crypto-related functionality.
|
|
|
|
For low-level functionality, see `Plug.Crypto.KeyGenerator`,
|
|
`Plug.Crypto.MessageEncryptor`, and `Plug.Crypto.MessageVerifier`.
|
|
"""
|
|
|
|
import Bitwise
|
|
alias Plug.Crypto.{KeyGenerator, MessageVerifier, MessageEncryptor}
|
|
|
|
@doc """
|
|
Prunes the stacktrace to remove any argument trace.
|
|
|
|
This is useful when working with functions that receives secrets
|
|
and we want to make sure those secrets do not leak on error messages.
|
|
"""
|
|
@spec prune_args_from_stacktrace(Exception.stacktrace()) :: Exception.stacktrace()
|
|
def prune_args_from_stacktrace(stacktrace)
|
|
|
|
def prune_args_from_stacktrace([{mod, fun, [_ | _] = args, info} | rest]),
|
|
do: [{mod, fun, length(args), info} | rest]
|
|
|
|
def prune_args_from_stacktrace(stacktrace) when is_list(stacktrace),
|
|
do: stacktrace
|
|
|
|
@doc false
|
|
@deprecated "Use non_executable_binary_to_term/2"
|
|
def safe_binary_to_term(binary, opts \\ []) do
|
|
non_executable_binary_to_term(binary, opts)
|
|
end
|
|
|
|
@doc """
|
|
A restricted version of `:erlang.binary_to_term/2` that forbids
|
|
*executable* terms, such as anonymous functions.
|
|
|
|
The `opts` are given to the underlying `:erlang.binary_to_term/2`
|
|
call, with an empty list as a default.
|
|
|
|
By default this function does not restrict atoms, as an atom
|
|
interned in one node may not yet have been interned on another
|
|
(except for releases, which preload all code).
|
|
|
|
If you want to avoid atoms from being created, then you can pass
|
|
`[:safe]` as options, as that will also enable the safety mechanisms
|
|
from `:erlang.binary_to_term/2` itself.
|
|
"""
|
|
@spec non_executable_binary_to_term(binary(), [atom()]) :: term()
|
|
def non_executable_binary_to_term(binary, opts \\ []) when is_binary(binary) do
|
|
term = :erlang.binary_to_term(binary, opts)
|
|
non_executable_terms(term)
|
|
term
|
|
end
|
|
|
|
defp non_executable_terms(list) when is_list(list) do
|
|
non_executable_list(list)
|
|
end
|
|
|
|
defp non_executable_terms(tuple) when is_tuple(tuple) do
|
|
non_executable_tuple(tuple, tuple_size(tuple))
|
|
end
|
|
|
|
defp non_executable_terms(map) when is_map(map) do
|
|
folder = fn key, value, acc ->
|
|
non_executable_terms(key)
|
|
non_executable_terms(value)
|
|
acc
|
|
end
|
|
|
|
:maps.fold(folder, map, map)
|
|
end
|
|
|
|
defp non_executable_terms(other)
|
|
when is_atom(other) or is_number(other) or is_bitstring(other) or is_pid(other) or
|
|
is_reference(other) do
|
|
other
|
|
end
|
|
|
|
defp non_executable_terms(other) do
|
|
raise ArgumentError,
|
|
"cannot deserialize #{inspect(other)}, the term is not safe for deserialization"
|
|
end
|
|
|
|
defp non_executable_list([]), do: :ok
|
|
|
|
defp non_executable_list([h | t]) when is_list(t) do
|
|
non_executable_terms(h)
|
|
non_executable_list(t)
|
|
end
|
|
|
|
defp non_executable_list([h | t]) do
|
|
non_executable_terms(h)
|
|
non_executable_terms(t)
|
|
end
|
|
|
|
defp non_executable_tuple(_tuple, 0), do: :ok
|
|
|
|
defp non_executable_tuple(tuple, n) do
|
|
non_executable_terms(:erlang.element(n, tuple))
|
|
non_executable_tuple(tuple, n - 1)
|
|
end
|
|
|
|
@doc """
|
|
Masks the token on the left with the token on the right.
|
|
|
|
Both tokens are required to have the same size.
|
|
"""
|
|
@spec mask(binary(), binary()) :: binary()
|
|
def mask(left, right) do
|
|
:crypto.exor(left, right)
|
|
end
|
|
|
|
@doc """
|
|
Compares the two binaries (one being masked) in constant-time to avoid
|
|
timing attacks.
|
|
|
|
It is assumed the right token is masked according to the given mask.
|
|
"""
|
|
@spec masked_compare(binary(), binary(), binary()) :: boolean()
|
|
def masked_compare(left, right, mask)
|
|
when is_binary(left) and is_binary(right) and is_binary(mask) do
|
|
byte_size(left) == byte_size(right) and masked_compare(left, right, mask, 0)
|
|
end
|
|
|
|
defp masked_compare(<<x, left::binary>>, <<y, right::binary>>, <<z, mask::binary>>, acc) do
|
|
xorred = bxor(x, bxor(y, z))
|
|
masked_compare(left, right, mask, acc ||| xorred)
|
|
end
|
|
|
|
defp masked_compare(<<>>, <<>>, <<>>, acc) do
|
|
acc === 0
|
|
end
|
|
|
|
@doc """
|
|
Compares the two binaries in constant-time to avoid timing attacks.
|
|
|
|
See: http://codahale.com/a-lesson-in-timing-attacks/
|
|
"""
|
|
@spec secure_compare(binary(), binary()) :: boolean()
|
|
def secure_compare(left, right) when is_binary(left) and is_binary(right) do
|
|
byte_size(left) == byte_size(right) and secure_compare(left, right, 0)
|
|
end
|
|
|
|
defp secure_compare(<<x, left::binary>>, <<y, right::binary>>, acc) do
|
|
xorred = bxor(x, y)
|
|
secure_compare(left, right, acc ||| xorred)
|
|
end
|
|
|
|
defp secure_compare(<<>>, <<>>, acc) do
|
|
acc === 0
|
|
end
|
|
|
|
@doc """
|
|
Encodes and signs data into a token you can send to clients.
|
|
|
|
Plug.Crypto.sign(conn.secret_key_base, "user-secret", {:elixir, :terms})
|
|
|
|
A key will be derived from the secret key base and the given user secret.
|
|
The key will also be cached for performance reasons on future calls.
|
|
|
|
## Options
|
|
|
|
* `:key_iterations` - option passed to `Plug.Crypto.KeyGenerator`
|
|
when generating the encryption and signing keys. Defaults to 1000
|
|
* `:key_length` - option passed to `Plug.Crypto.KeyGenerator`
|
|
when generating the encryption and signing keys. Defaults to 32
|
|
* `:key_digest` - option passed to `Plug.Crypto.KeyGenerator`
|
|
when generating the encryption and signing keys. Defaults to `:sha256`
|
|
* `:signed_at` - set the timestamp of the token in seconds.
|
|
Defaults to `System.system_time(:second)`
|
|
* `:max_age` - the default maximum age of the token. Defaults to
|
|
`86400` seconds (1 day) and it may be overridden on `verify/4`.
|
|
|
|
"""
|
|
def sign(key_base, salt, data, opts \\ []) when is_binary(key_base) and is_binary(salt) do
|
|
data
|
|
|> encode(opts)
|
|
|> MessageVerifier.sign(get_secret(key_base, salt, opts))
|
|
end
|
|
|
|
@doc """
|
|
Encodes, encrypts, and signs data into a token you can send to clients.
|
|
|
|
Plug.Crypto.encrypt(conn.secret_key_base, "user-secret", {:elixir, :terms})
|
|
|
|
A key will be derived from the secret key base and the given user secret.
|
|
The key will also be cached for performance reasons on future calls.
|
|
|
|
## Options
|
|
|
|
* `:key_iterations` - option passed to `Plug.Crypto.KeyGenerator`
|
|
when generating the encryption and signing keys. Defaults to 1000
|
|
* `:key_length` - option passed to `Plug.Crypto.KeyGenerator`
|
|
when generating the encryption and signing keys. Defaults to 32
|
|
* `:key_digest` - option passed to `Plug.Crypto.KeyGenerator`
|
|
when generating the encryption and signing keys. Defaults to `:sha256`
|
|
* `:signed_at` - set the timestamp of the token in seconds.
|
|
Defaults to `System.system_time(:second)`
|
|
* `:max_age` - the default maximum age of the token. Defaults to
|
|
`86400` seconds (1 day) and it may be overridden on `decrypt/4`.
|
|
|
|
"""
|
|
def encrypt(key_base, secret, data, opts \\ [])
|
|
when is_binary(key_base) and is_binary(secret) do
|
|
encrypt(key_base, secret, nil, data, opts)
|
|
end
|
|
|
|
@doc false
|
|
def encrypt(key_base, secret, salt, data, opts) do
|
|
data
|
|
|> encode(opts)
|
|
|> MessageEncryptor.encrypt(
|
|
get_secret(key_base, secret, opts),
|
|
get_secret(key_base, salt, opts)
|
|
)
|
|
end
|
|
|
|
defp encode(data, opts) do
|
|
signed_at_seconds = Keyword.get(opts, :signed_at)
|
|
signed_at_ms = if signed_at_seconds, do: trunc(signed_at_seconds * 1000), else: now_ms()
|
|
max_age_in_seconds = Keyword.get(opts, :max_age, 86400)
|
|
:erlang.term_to_binary({data, signed_at_ms, max_age_in_seconds})
|
|
end
|
|
|
|
@doc """
|
|
Decodes the original data from the token and verifies its integrity.
|
|
|
|
## Examples
|
|
|
|
In this scenario we will create a token, sign it, then provide it to a client
|
|
application. The client will then use this token to authenticate requests for
|
|
resources from the server. See `Plug.Crypto` summary for more info about
|
|
creating tokens.
|
|
|
|
iex> user_id = 99
|
|
iex> secret = "kjoy3o1zeidquwy1398juxzldjlksahdk3"
|
|
iex> user_salt = "user salt"
|
|
iex> token = Plug.Crypto.sign(secret, user_salt, user_id)
|
|
|
|
The mechanism for passing the token to the client is typically through a
|
|
cookie, a JSON response body, or HTTP header. For now, assume the client has
|
|
received a token it can use to validate requests for protected resources.
|
|
|
|
When the server receives a request, it can use `verify/4` to determine if it
|
|
should provide the requested resources to the client:
|
|
|
|
iex> Plug.Crypto.verify(secret, user_salt, token, max_age: 86400)
|
|
{:ok, 99}
|
|
|
|
In this example, we know the client sent a valid token because `verify/4`
|
|
returned a tuple of type `{:ok, user_id}`. The server can now proceed with
|
|
the request.
|
|
|
|
However, if the client had sent an expired or otherwise invalid token
|
|
`verify/4` would have returned an error instead:
|
|
|
|
iex> Plug.Crypto.verify(secret, user_salt, expired, max_age: 86400)
|
|
{:error, :expired}
|
|
|
|
iex> Plug.Crypto.verify(secret, user_salt, invalid, max_age: 86400)
|
|
{:error, :invalid}
|
|
|
|
## Options
|
|
|
|
* `:max_age` - verifies the token only if it has been generated
|
|
"max age" ago in seconds. Defaults to the max age signed in the
|
|
token (86400)
|
|
* `:key_iterations` - option passed to `Plug.Crypto.KeyGenerator`
|
|
when generating the encryption and signing keys. Defaults to 1000
|
|
* `:key_length` - option passed to `Plug.Crypto.KeyGenerator`
|
|
when generating the encryption and signing keys. Defaults to 32
|
|
* `:key_digest` - option passed to `Plug.Crypto.KeyGenerator`
|
|
when generating the encryption and signing keys. Defaults to `:sha256`
|
|
|
|
"""
|
|
def verify(key_base, salt, token, opts \\ [])
|
|
|
|
def verify(key_base, salt, token, opts)
|
|
when is_binary(key_base) and is_binary(salt) and is_binary(token) do
|
|
secret = get_secret(key_base, salt, opts)
|
|
|
|
case MessageVerifier.verify(token, secret) do
|
|
{:ok, message} -> decode(message, opts)
|
|
:error -> {:error, :invalid}
|
|
end
|
|
end
|
|
|
|
def verify(_key_base, salt, nil, _opts) when is_binary(salt) do
|
|
{:error, :missing}
|
|
end
|
|
|
|
@doc """
|
|
Decrypts the original data from the token and verifies its integrity.
|
|
|
|
## Options
|
|
|
|
* `:max_age` - verifies the token only if it has been generated
|
|
"max age" ago in seconds. A reasonable value is 1 day (86400
|
|
seconds)
|
|
* `:key_iterations` - option passed to `Plug.Crypto.KeyGenerator`
|
|
when generating the encryption and signing keys. Defaults to 1000
|
|
* `:key_length` - option passed to `Plug.Crypto.KeyGenerator`
|
|
when generating the encryption and signing keys. Defaults to 32
|
|
* `:key_digest` - option passed to `Plug.Crypto.KeyGenerator`
|
|
when generating the encryption and signing keys. Defaults to `:sha256`
|
|
|
|
"""
|
|
def decrypt(key_base, secret, token, opts \\ [])
|
|
when is_binary(key_base) and is_binary(secret) and is_list(opts) do
|
|
decrypt(key_base, secret, nil, token, opts)
|
|
end
|
|
|
|
@doc false
|
|
def decrypt(key_base, secret, salt, token, opts) when is_binary(token) do
|
|
secret = get_secret(key_base, secret, opts)
|
|
salt = get_secret(key_base, salt, opts)
|
|
|
|
case MessageEncryptor.decrypt(token, secret, salt) do
|
|
{:ok, message} -> decode(message, opts)
|
|
:error -> {:error, :invalid}
|
|
end
|
|
end
|
|
|
|
def decrypt(_key_base, _secret, _salt, nil, _opts) do
|
|
{:error, :missing}
|
|
end
|
|
|
|
defp decode(message, opts) do
|
|
{data, signed, max_age} =
|
|
case non_executable_binary_to_term(message) do
|
|
{data, signed, max_age} -> {data, signed, max_age}
|
|
# For backwards compatibility with Plug.Crypto v1.1
|
|
{data, signed} -> {data, signed, 86400}
|
|
# For backwards compatibility with Phoenix which had the original code
|
|
%{data: data, signed: signed} -> {data, signed, 86400}
|
|
end
|
|
|
|
if expired?(signed, Keyword.get(opts, :max_age, max_age)) do
|
|
{:error, :expired}
|
|
else
|
|
{:ok, data}
|
|
end
|
|
end
|
|
|
|
## Helpers
|
|
|
|
# Gathers configuration and generates the key secrets and signing secrets.
|
|
defp get_secret(_secret_key_base, nil, _opts) do
|
|
""
|
|
end
|
|
|
|
defp get_secret(secret_key_base, salt, opts) do
|
|
iterations = Keyword.get(opts, :key_iterations, 1000)
|
|
length = Keyword.get(opts, :key_length, 32)
|
|
digest = Keyword.get(opts, :key_digest, :sha256)
|
|
cache = Keyword.get(opts, :cache, Plug.Crypto.Keys)
|
|
KeyGenerator.generate(secret_key_base, salt, iterations, length, digest, cache)
|
|
end
|
|
|
|
defp expired?(_signed, :infinity), do: false
|
|
defp expired?(_signed, max_age_secs) when max_age_secs <= 0, do: true
|
|
defp expired?(signed, max_age_secs), do: signed + trunc(max_age_secs * 1000) < now_ms()
|
|
|
|
defp now_ms, do: System.system_time(:millisecond)
|
|
end
|