This commit is contained in:
2026-07-14 22:50:46 +04:00
parent 27143319e3
commit bd19e0682b
3116 changed files with 467189 additions and 0 deletions

View File

@@ -0,0 +1,359 @@
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`.
"""
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 """
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 byte_size(right) == byte_size(mask) and
crypto_exor_hash_equals(left, right, mask)
end
defp crypto_exor_hash_equals(x, y, z) do
crypto_hash_equals(mask(x, y), z)
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 crypto_hash_equals(left, right)
end
# TODO: remove when we require OTP 25.0
if Code.ensure_loaded?(:crypto) and function_exported?(:crypto, :hash_equals, 2) do
defp crypto_hash_equals(x, y) do
# Depending on the linked OpenSSL library hash_equals is available.
# If not, we fall back to the legacy implementation.
try do
:crypto.hash_equals(x, y)
rescue
# Still can throw "Unsupported CRYPTO_memcmp"
ErlangError ->
legacy_secure_compare(x, y, 0)
end
end
else
defp crypto_hash_equals(x, y) do
legacy_secure_compare(x, y, 0)
end
end
defp legacy_secure_compare(<<x, left::binary>>, <<y, right::binary>>, acc) do
import Bitwise
xorred = bxor(x, y)
legacy_secure_compare(left, right, acc ||| xorred)
end
defp legacy_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.os_time(:millisecond)`
* `: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.os_time(:millisecond)`
* `: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
data
|> encode(opts)
|> MessageEncryptor.encrypt(get_secret(key_base, secret, 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 \\ [])
def decrypt(key_base, secret, nil, opts)
when is_binary(key_base) and is_binary(secret) and is_list(opts) do
{:error, :missing}
end
def decrypt(key_base, secret, token, opts)
when is_binary(key_base) and is_binary(secret) and is_list(opts) do
secret = get_secret(key_base, secret, opts)
case MessageEncryptor.decrypt(token, secret, "") do
{:ok, message} -> decode(message, opts)
:error -> {:error, :invalid}
end
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, 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.os_time(:millisecond)
end

View File

@@ -0,0 +1,16 @@
defmodule Plug.Crypto.Application do
@moduledoc false
use Application
def start(_, _) do
children = [
{Agent, &start_crypto_keys/0}
]
Supervisor.start_link(children, strategy: :one_for_one, name: __MODULE__)
end
defp start_crypto_keys do
Plug.Crypto.Keys = :ets.new(Plug.Crypto.Keys, [:named_table, :public, read_concurrency: true])
end
end

View File

@@ -0,0 +1,115 @@
defmodule Plug.Crypto.KeyGenerator do
@moduledoc """
`KeyGenerator` implements PBKDF2 (Password-Based Key Derivation Function 2),
part of PKCS #5 v2.0 (Password-Based Cryptography Specification).
It can be used to derive a number of keys for various purposes from a given
secret. This lets applications have a single secure secret, but avoid reusing
that key in multiple incompatible contexts.
The returned key is a binary. You may invoke functions in the `Base` module,
such as `Base.url_encode64/2`, to convert this binary into a textual
representation.
See http://tools.ietf.org/html/rfc2898#section-5.2
"""
import Bitwise
@max_length bsl(1, 32) - 1
@doc """
Returns a derived key suitable for use.
## Options
* `:iterations` - defaults to 1000 (increase to at least 2^16 if used for passwords);
* `:length` - a length in octets for the derived key. Defaults to 32;
* `:digest` - an hmac function to use as the pseudo-random function. Defaults to `:sha256`;
* `:cache` - an ETS table name to be used as cache.
Only use an ETS table as cache if the secret and salt is a bound set of values.
For example: `:ets.new(:your_name, [:named_table, :public, read_concurrency: true])`
"""
def generate(secret, salt, opts \\ []) do
iterations = Keyword.get(opts, :iterations, 1000)
length = Keyword.get(opts, :length, 32)
digest = Keyword.get(opts, :digest, :sha256)
cache = Keyword.get(opts, :cache)
generate(secret, salt, iterations, length, digest, cache)
end
@doc false
def generate(secret, salt, iterations, length, digest, cache) do
cond do
not is_integer(iterations) or iterations < 1 ->
raise ArgumentError, "iterations must be an integer >= 1"
length > @max_length ->
raise ArgumentError, "length must be less than or equal to #{@max_length}"
true ->
with_cache(cache, {secret, salt, iterations, length, digest}, fn ->
pbkdf2_hmac(digest, secret, salt, iterations, length)
end)
end
rescue
e -> reraise e, Plug.Crypto.prune_args_from_stacktrace(__STACKTRACE__)
end
defp with_cache(nil, _key, fun), do: fun.()
defp with_cache(ets, key, fun) do
case :ets.lookup(ets, key) do
[{_key, value}] ->
value
[] ->
value = fun.()
:ets.insert(ets, [{key, value}])
value
end
end
# TODO: remove when we require OTP 24.2
if Code.ensure_loaded?(:crypto) and function_exported?(:crypto, :pbkdf2_hmac, 5) do
defp pbkdf2_hmac(digest, secret, salt, iterations, length) do
:crypto.pbkdf2_hmac(digest, secret, salt, iterations, length)
end
else
defp pbkdf2_hmac(digest, secret, salt, iterations, length) do
legacy_pbkdf2_hmac(hmac_fun(digest, secret), salt, iterations, length, 1, [], 0)
end
defp legacy_pbkdf2_hmac(_fun, _salt, _iterations, max_length, _block_index, acc, length)
when length >= max_length do
acc
|> IO.iodata_to_binary()
|> binary_part(0, max_length)
end
defp legacy_pbkdf2_hmac(fun, salt, iterations, max_length, block_index, acc, length) do
initial = fun.(<<salt::binary, block_index::integer-size(32)>>)
block = iterate(fun, iterations - 1, initial, initial)
length = byte_size(block) + length
legacy_pbkdf2_hmac(
fun,
salt,
iterations,
max_length,
block_index + 1,
[acc | block],
length
)
end
defp iterate(_fun, 0, _prev, acc), do: acc
defp iterate(fun, iteration, prev, acc) do
next = fun.(prev)
iterate(fun, iteration - 1, next, :crypto.exor(next, acc))
end
defp hmac_fun(digest, key), do: &:crypto.mac(:hmac, digest, key, &1)
end
end

View File

@@ -0,0 +1,197 @@
defmodule Plug.Crypto.MessageEncryptor do
@moduledoc ~S"""
`MessageEncryptor` is a simple way to encrypt values which get stored
somewhere you don't trust.
The encrypted key, initialization vector, cipher text, and cipher tag
are base64url encoded and returned to you.
This can be used in situations similar to the `Plug.Crypto.MessageVerifier`,
but where you don't want users to be able to determine the value of the payload.
The current algorithm used is XChaCha20-Poly1305.
## Example
iex> secret_key_base = "072d1e0157c008193fe48a670cce031faa4e..."
...> encrypted_cookie_salt = "encrypted cookie"
...> secret = KeyGenerator.generate(secret_key_base, encrypted_cookie_salt)
...>
...> data = "José"
...> encrypted = MessageEncryptor.encrypt(data, secret, "UNUSED")
...> MessageEncryptor.decrypt(encrypted, secret, "UNUSED")
{:ok, "José"}
"""
@doc """
Encrypts a message using authenticated encryption.
The `sign_secret` is currently only used on decryption
for backwards compatibility.
A custom authentication message can be provided.
It defaults to "A128GCM" for backwards compatibility.
"""
def encrypt(message, aad \\ "A128GCM", secret, sign_secret)
when is_binary(message) and (is_binary(aad) or is_list(aad)) and
bit_size(secret) == 256 and
is_binary(sign_secret) do
iv = :crypto.strong_rand_bytes(24)
{subkey, nonce} = xchacha20_subkey_and_nonce(secret, iv)
{cipher_text, cipher_tag} = block_encrypt(:chacha20_poly1305, subkey, nonce, {aad, message})
"XCP." <> Base.url_encode64(iv <> cipher_tag <> cipher_text, padding: false)
rescue
e -> reraise e, Plug.Crypto.prune_args_from_stacktrace(__STACKTRACE__)
end
@doc """
Decrypts a message using authenticated encryption.
"""
def decrypt(encrypted, aad \\ "A128GCM", secret, sign_secret)
when is_binary(encrypted) and (is_binary(aad) or is_list(aad)) and
bit_size(secret) in [128, 192, 256] and
is_binary(sign_secret) do
unguarded_decrypt(encrypted, aad, secret, sign_secret)
rescue
e -> reraise e, Plug.Crypto.prune_args_from_stacktrace(__STACKTRACE__)
end
defp unguarded_decrypt("XCP." <> iv_cipher_text_cipher_tag, aad, secret, _sign_secret) do
with {:ok, <<iv::192-bits, cipher_tag::128-bits, cipher_text::binary>>} <-
Base.url_decode64(iv_cipher_text_cipher_tag, padding: false),
{subkey, nonce} = xchacha20_subkey_and_nonce(secret, iv),
plain_text when is_binary(plain_text) <-
block_decrypt(:chacha20_poly1305, subkey, nonce, {aad, cipher_text, cipher_tag}) do
{:ok, plain_text}
else
_ -> :error
end
end
# Messages from Plug.Crypto v1.x
defp unguarded_decrypt("QTEyOEdDTQ." <> rest, aad, secret, sign_secret) do
with [encrypted_key, iv, cipher_text, cipher_tag] <- :binary.split(rest, ".", [:global]),
{:ok, encrypted_key} <- Base.url_decode64(encrypted_key, padding: false),
{:ok, iv} when bit_size(iv) === 96 <- Base.url_decode64(iv, padding: false),
{:ok, cipher_text} <- Base.url_decode64(cipher_text, padding: false),
{:ok, cipher_tag} when bit_size(cipher_tag) === 128 <-
Base.url_decode64(cipher_tag, padding: false),
{:ok, key} <- aes_gcm_key_unwrap(encrypted_key, secret, sign_secret),
plain_text when is_binary(plain_text) <-
block_decrypt(:aes_gcm, key, iv, {aad, cipher_text, cipher_tag}) do
{:ok, plain_text}
else
_ -> :error
end
end
defp unguarded_decrypt(_rest, _aad, _secret, _sign_secret) do
:error
end
defp block_encrypt(cipher, key, iv, {aad, payload}) do
cipher = cipher_alias(cipher, bit_size(key))
:crypto.crypto_one_time_aead(cipher, key, iv, payload, aad, true)
catch
:error, :notsup -> raise_notsup(cipher)
end
defp block_decrypt(cipher, key, iv, {aad, payload, tag}) do
cipher = cipher_alias(cipher, bit_size(key))
:crypto.crypto_one_time_aead(cipher, key, iv, payload, aad, tag, false)
catch
:error, :notsup -> raise_notsup(cipher)
end
defp cipher_alias(:aes_gcm, 128), do: :aes_128_gcm
defp cipher_alias(:aes_gcm, 192), do: :aes_192_gcm
defp cipher_alias(:aes_gcm, 256), do: :aes_256_gcm
defp cipher_alias(other, _), do: other
defp raise_notsup(algo) do
raise "the algorithm #{inspect(algo)} is not supported by your Erlang/OTP installation. " <>
"Please make sure it was compiled with the correct OpenSSL/BoringSSL bindings"
end
defp xchacha20_subkey_and_nonce(<<key::256-bits>>, <<nonce0::128-bits, nonce1::64-bits>>) do
subkey = hchacha20(key, nonce0)
nonce = <<0::32, nonce1::64-bits>>
{subkey, nonce}
end
defp hchacha20(<<key::256-bits>>, <<nonce::128-bits>>) 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
# Unwraps an encrypted content encryption key (CEK) with secret and
# sign_secret using AES GCM mode. Accepts keys of 128, 192, or 256
# bits based on the length of the secret key.
#
# See: https://tools.ietf.org/html/rfc7518#section-4.7
defp aes_gcm_key_unwrap(wrapped_cek, secret, sign_secret)
when bit_size(secret) in [128, 192, 256] and is_binary(sign_secret) do
wrapped_cek
|> case do
<<cipher_text::128-bitstring, cipher_tag::128-bitstring, iv::96-bitstring>> ->
block_decrypt(:aes_gcm, secret, iv, {sign_secret, cipher_text, cipher_tag})
<<cipher_text::192-bitstring, cipher_tag::128-bitstring, iv::96-bitstring>> ->
block_decrypt(:aes_gcm, secret, iv, {sign_secret, cipher_text, cipher_tag})
<<cipher_text::256-bitstring, cipher_tag::128-bitstring, iv::96-bitstring>> ->
block_decrypt(:aes_gcm, secret, iv, {sign_secret, cipher_text, cipher_tag})
_ ->
:error
end
|> case do
cek when bit_size(cek) in [128, 192, 256] -> {:ok, cek}
_ -> :error
end
end
end

View File

@@ -0,0 +1,73 @@
defmodule Plug.Crypto.MessageVerifier do
@moduledoc """
`MessageVerifier` makes it easy to generate and verify messages
which are signed to prevent tampering.
For example, the cookie store uses this verifier to send data
to the client. The data can be read by the client, but cannot be
tampered with.
The message and its verification are base64url encoded and returned
to you.
The current algorithm used is HMAC-SHA, with SHA256, SHA384, and
SHA512 as supported digest types.
"""
@doc """
Signs a message according to the given secret.
"""
def sign(message, secret, digest_type \\ :sha256)
when is_binary(message) and byte_size(secret) > 0 and
digest_type in [:sha256, :sha384, :sha512] do
hmac_sha2_sign(message, secret, digest_type)
rescue
e -> reraise e, Plug.Crypto.prune_args_from_stacktrace(__STACKTRACE__)
end
@doc """
Decodes and verifies the encoded binary was not tampered with.
"""
def verify(signed, secret) when is_binary(signed) and byte_size(secret) > 0 do
hmac_sha2_verify(signed, secret)
rescue
e -> reraise e, Plug.Crypto.prune_args_from_stacktrace(__STACKTRACE__)
end
## Signature Algorithms
defp hmac_sha2_to_protected(:sha256), do: "SFMyNTY"
defp hmac_sha2_to_protected(:sha384), do: "SFMzODQ"
defp hmac_sha2_to_protected(:sha512), do: "SFM1MTI"
defp hmac_sha2_to_digest_type("SFMyNTY"), do: :sha256
defp hmac_sha2_to_digest_type("SFMzODQ"), do: :sha384
defp hmac_sha2_to_digest_type("SFM1MTI"), do: :sha512
defp hmac_sha2_sign(payload, key, digest_type) do
protected = hmac_sha2_to_protected(digest_type)
plain_text = [protected, ?., Base.url_encode64(payload, padding: false)]
signature = :crypto.mac(:hmac, digest_type, key, plain_text)
IO.iodata_to_binary([plain_text, ".", Base.url_encode64(signature, padding: false)])
end
defp hmac_sha2_verify(signed, key) when is_binary(signed) and is_binary(key) do
with [protected, payload, signature] when protected in ["SFMyNTY", "SFMzODQ", "SFM1MTI"] <-
:binary.split(signed, ".", [:global]),
plain_text = [protected, ?., payload],
{:ok, payload} <- Base.url_decode64(payload, padding: false),
{:ok, signature} <- Base.url_decode64(signature, padding: false) do
digest_type = hmac_sha2_to_digest_type(protected)
challenge = :crypto.mac(:hmac, digest_type, key, plain_text)
if Plug.Crypto.secure_compare(challenge, signature) do
{:ok, payload}
else
:error
end
else
_ ->
:error
end
end
end