update
This commit is contained in:
359
phoenix/deps/plug_crypto/lib/plug/crypto.ex
Normal file
359
phoenix/deps/plug_crypto/lib/plug/crypto.ex
Normal 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
|
||||
16
phoenix/deps/plug_crypto/lib/plug/crypto/application.ex
Normal file
16
phoenix/deps/plug_crypto/lib/plug/crypto/application.ex
Normal 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
|
||||
115
phoenix/deps/plug_crypto/lib/plug/crypto/key_generator.ex
Normal file
115
phoenix/deps/plug_crypto/lib/plug/crypto/key_generator.ex
Normal 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
|
||||
197
phoenix/deps/plug_crypto/lib/plug/crypto/message_encryptor.ex
Normal file
197
phoenix/deps/plug_crypto/lib/plug/crypto/message_encryptor.ex
Normal 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
|
||||
73
phoenix/deps/plug_crypto/lib/plug/crypto/message_verifier.ex
Normal file
73
phoenix/deps/plug_crypto/lib/plug/crypto/message_verifier.ex
Normal 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
|
||||
Reference in New Issue
Block a user