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,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