Skip to content

Commit

Permalink
Clean up Plug.Crypto's code
Browse files Browse the repository at this point in the history
  • Loading branch information
José Valim committed Feb 26, 2017
1 parent e792d2c commit c30ffae
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 151 deletions.
279 changes: 130 additions & 149 deletions lib/plug/crypto/message_encryptor.ex
Expand Up @@ -42,131 +42,15 @@ defmodule Plug.Crypto.MessageEncryptor do
aes128_gcm_decrypt(encrypted, secret, sign_secret)
end

## Deprecated API

@doc """
WARNING: This function is deprecated in favor of `encrypt/3`.
Encrypts and signs a message.
"""
def encrypt_and_sign(message, secret, sign_secret, cipher \\ nil)
when is_binary(message) and is_binary(secret) and is_binary(sign_secret) do
# TODO: Deprecate after backwards compatibility period
# IO.puts :stderr, "warning: `Plug.Crypto.MessageEncryptor.encrypt_and_sign/4` is deprecated," <>
# "please use `encrypt/3` instead\n" <> Exception.format_stacktrace
case cipher do
nil ->
encrypt(message, secret, sign_secret)
:aes_cbc256 ->
aes256_cbc_hmac_sha1_encrypt(message, secret, sign_secret)
_ ->
iv = :crypto.strong_rand_bytes(16)

message
|> pkcs7_pad()
|> encrypt_legacy(cipher, secret, iv)
|> Base.encode64()
|> Kernel.<>("--")
|> Kernel.<>(Base.encode64(iv))
|> MessageVerifier.sign(sign_secret)
end
end

@doc """
WARNING: This function is deprecated in favor of `decrypt/3`.
Decrypts and verifies a message.
We need to verify the message in order to avoid padding attacks.
Reference: http://www.limited-entropy.com/padding-oracle-attacks
"""
def verify_and_decrypt(encrypted, secret, sign_secret, cipher \\ nil)
when is_binary(encrypted) and is_binary(secret) and is_binary(sign_secret) do
# TODO: Deprecate after backwards compatibility period
# IO.puts :stderr, "warning: `Plug.Crypto.MessageEncryptor.verify_and_decrypt/4` is deprecated," <>
# "please use `decrypt/3` instead\n" <> Exception.format_stacktrace
case cipher do
nil ->
if String.contains?(encrypted, ".") do
decrypt(encrypted, secret, sign_secret)
else
verify_and_decrypt(encrypted, secret, sign_secret, :aes_cbc256)
end
:aes_cbc256 ->
aes256_cbc_hmac_sha1_decrypt(encrypted, secret, sign_secret)
_ ->
case MessageVerifier.verify(encrypted, sign_secret) do
{:ok, verified} ->
[encrypted, iv] = String.split(verified, "--")
case Base.decode64(encrypted) do
{:ok, encrypted} ->
case Base.decode64(iv) do
{:ok, iv} ->
encrypted |> decrypt_legacy(cipher, secret, iv) |> pkcs7_unpad
:error ->
:error
end
:error ->
:error
end
:error ->
:error
end
end
end

## Authenticated Encryption Algorithms

# Encrypts and authenticates a message using AES128-CBC mode
# with HMAC-SHA-1 for the authentication code.
defp aes256_cbc_hmac_sha1_encrypt(plain_text, secret, sign_secret)
when bit_size(secret) > 256,
do: aes256_cbc_hmac_sha1_encrypt(plain_text, binary_part(secret, 0, 32), sign_secret)
defp aes256_cbc_hmac_sha1_encrypt(plain_text, secret, sign_secret)
when is_binary(plain_text)
and bit_size(secret) in [128, 192, 256]
and is_binary(sign_secret) do
iv = :crypto.strong_rand_bytes(16)
cipher_text = :crypto.block_encrypt(:aes_cbc256, secret, iv, pkcs7_pad(plain_text))
encode_legacy_token(sign_secret, iv, cipher_text)
end

# Verifies and decrypts a message using AES128-CBC mode
# with HMAC-SHA-1 for the authentication code.
#
# Decryption will never be performed prior to verification.
defp aes256_cbc_hmac_sha1_decrypt(cipher_text, secret, sign_secret)
when bit_size(secret) > 256,
do: aes256_cbc_hmac_sha1_decrypt(cipher_text, binary_part(secret, 0, 32), sign_secret)
defp aes256_cbc_hmac_sha1_decrypt(cipher_text, secret, sign_secret)
when is_binary(cipher_text)
and bit_size(secret) === 256
and is_binary(sign_secret) do
case decode_legacy_token(cipher_text, sign_secret) do
{"A256CBC-HS1", _encrypted_key, iv, cipher_text, cipher_tag}
when bit_size(iv) === 128 and bit_size(cipher_tag) === 160 ->
key = secret
:crypto.block_decrypt(:aes_cbc256, key, iv, cipher_text)
|> case do
plain_text when is_binary(plain_text) ->
pkcs7_unpad(plain_text)
_ ->
:error
end
_ ->
:error
end
end

# Encrypts and authenticates a message using AES128-GCM mode.
#
# A random 128-bit content encryption key (CEK) is generated for
# every message which is then encrypted with `aes_gcm_key_wrap/3`.
defp aes128_gcm_encrypt(plain_text, secret, sign_secret) when bit_size(secret) > 256 do
aes128_gcm_encrypt(plain_text, binary_part(secret, 0, 32), sign_secret)
end
defp aes128_gcm_encrypt(plain_text, secret, sign_secret)
when bit_size(secret) > 256,
do: aes128_gcm_encrypt(plain_text, binary_part(secret, 0, 32), sign_secret)
defp aes128_gcm_encrypt(plain_text, secret, sign_secret)
when is_binary(plain_text)
and bit_size(secret) in [128, 192, 256]
and is_binary(sign_secret) do
when is_binary(plain_text) and bit_size(secret) in [128, 192, 256] and is_binary(sign_secret) do
key = :crypto.strong_rand_bytes(16)
iv = :crypto.strong_rand_bytes(12)
aad = "A128GCM"
Expand All @@ -181,16 +65,13 @@ defmodule Plug.Crypto.MessageEncryptor do
#
# The encrypted content encryption key (CEK) is decrypted
# with `aes_gcm_key_unwrap/3`.
defp aes128_gcm_decrypt(cipher_text, secret, sign_secret) when bit_size(secret) > 256 do
aes128_gcm_decrypt(cipher_text, binary_part(secret, 0, 32), sign_secret)
end
defp aes128_gcm_decrypt(cipher_text, secret, sign_secret)
when bit_size(secret) > 256,
do: aes128_gcm_decrypt(cipher_text, binary_part(secret, 0, 32), sign_secret)
defp aes128_gcm_decrypt(cipher_text, secret, sign_secret)
when is_binary(cipher_text)
and bit_size(secret) in [128, 192, 256]
and is_binary(sign_secret) do
when is_binary(cipher_text) and bit_size(secret) in [128, 192, 256] and is_binary(sign_secret) do
case decode_token(cipher_text) do
{aad = "A128GCM", encrypted_key, iv, cipher_text, cipher_tag}
when bit_size(iv) === 96 and bit_size(cipher_tag) === 128 ->
{aad = "A128GCM", encrypted_key, iv, cipher_text, cipher_tag} when bit_size(iv) === 96 and bit_size(cipher_tag) === 128 ->
encrypted_key
|> aes_gcm_key_unwrap(secret, sign_secret)
|> case do
Expand All @@ -214,13 +95,11 @@ defmodule Plug.Crypto.MessageEncryptor do
# sign_secret using AES GCM mode.
#
# See: https://tools.ietf.org/html/rfc7518#section-4.7
defp aes_gcm_key_wrap(cek, secret, sign_secret) when bit_size(secret) > 256 do
aes_gcm_key_wrap(cek, binary_part(secret, 0, 32), sign_secret)
end
defp aes_gcm_key_wrap(cek, secret, sign_secret)
when bit_size(secret) > 256,
do: aes_gcm_key_wrap(cek, binary_part(secret, 0, 32), sign_secret)
defp aes_gcm_key_wrap(cek, secret, sign_secret)
when bit_size(cek) in [128, 192, 256]
and bit_size(secret) in [128, 192, 256]
and is_binary(sign_secret) do
when bit_size(cek) in [128, 192, 256] and bit_size(secret) in [128, 192, 256] and is_binary(sign_secret) do
iv = :crypto.strong_rand_bytes(12)
{cipher_text, cipher_tag} = :crypto.block_encrypt(:aes_gcm, secret, iv, {sign_secret, cek})
cipher_text <> cipher_tag <> iv
Expand All @@ -230,19 +109,18 @@ defmodule Plug.Crypto.MessageEncryptor do
# sign_secret using AES GCM mode.
#
# See: https://tools.ietf.org/html/rfc7518#section-4.7
defp aes_gcm_key_unwrap(wrapped_cek, secret, sign_secret) when bit_size(secret) > 256 do
aes_gcm_key_unwrap(wrapped_cek, binary_part(secret, 0, 32), sign_secret)
end
defp aes_gcm_key_unwrap(wrapped_cek, secret, sign_secret)
when bit_size(secret) > 256,
do: aes_gcm_key_unwrap(wrapped_cek, binary_part(secret, 0, 32), sign_secret)
defp aes_gcm_key_unwrap(wrapped_cek, secret, sign_secret)
when bit_size(secret) in [128, 192, 256]
and is_binary(sign_secret) do
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 >> ->
<<cipher_text :: 128-bitstring, cipher_tag :: 128-bitstring, iv :: 96-bitstring>> ->
:crypto.block_decrypt(:aes_gcm, secret, iv, {sign_secret, cipher_text, cipher_tag})
<< cipher_text :: 192-bitstring, cipher_tag :: 128-bitstring, iv :: 96-bitstring >> ->
<<cipher_text :: 192-bitstring, cipher_tag :: 128-bitstring, iv :: 96-bitstring>> ->
:crypto.block_decrypt(:aes_gcm, secret, iv, {sign_secret, cipher_text, cipher_tag})
<< cipher_text :: 256-bitstring, cipher_tag :: 128-bitstring, iv :: 96-bitstring >> ->
<<cipher_text :: 256-bitstring, cipher_tag :: 128-bitstring, iv :: 96-bitstring>> ->
:crypto.block_decrypt(:aes_gcm, secret, iv, {sign_secret, cipher_text, cipher_tag})
_ ->
:error
Expand All @@ -269,8 +147,9 @@ defmodule Plug.Crypto.MessageEncryptor do
#
# See: https://tools.ietf.org/html/rfc2315
# See: `pkcs7_pad/1`
defp pkcs7_unpad(<<>>),
do: :error
defp pkcs7_unpad(<<>>) do
:error
end
defp pkcs7_unpad(message) do
padding_size = :binary.last(message)
if padding_size <= 16 do
Expand All @@ -285,11 +164,8 @@ defmodule Plug.Crypto.MessageEncryptor do
end
end

## Helpers

defp encode_token(protected, encrypted_key, iv, cipher_text, cipher_tag) do
protected
|> Base.url_encode64(padding: false)
Base.url_encode64(protected, padding: false)

This comment has been minimized.

Copy link
@geonnave

geonnave Feb 28, 2017

Hi, I have a doubt here, most out of curiosity.
I remember learning in Elixir style guides that one should "use bare variables in the beginning of a pipeline". Does this recommendation had been changed?

|> Kernel.<>(".")
|> Kernel.<>(Base.url_encode64(encrypted_key, padding: false))
|> Kernel.<>(".")
Expand Down Expand Up @@ -322,7 +198,76 @@ defmodule Plug.Crypto.MessageEncryptor do
end
end

## Legacy Helpers
## Deprecated API

@doc """
WARNING: This function is deprecated in favor of `encrypt/3`.
Encrypts and signs a message.
"""
def encrypt_and_sign(message, secret, sign_secret, cipher \\ nil)
when is_binary(message) and is_binary(secret) and is_binary(sign_secret) do
# TODO: Deprecate after backwards compatibility period
# IO.puts :stderr, "warning: `Plug.Crypto.MessageEncryptor.encrypt_and_sign/4` is deprecated," <>
# "please use `encrypt/3` instead\n" <> Exception.format_stacktrace
case cipher do
nil ->
encrypt(message, secret, sign_secret)
:aes_cbc256 ->
aes256_cbc_hmac_sha1_encrypt(message, secret, sign_secret)
_ ->
iv = :crypto.strong_rand_bytes(16)

message
|> pkcs7_pad()
|> encrypt_legacy(cipher, secret, iv)
|> Base.encode64()
|> Kernel.<>("--")
|> Kernel.<>(Base.encode64(iv))
|> MessageVerifier.sign(sign_secret)
end
end

@doc """
WARNING: This function is deprecated in favor of `decrypt/3`.
Decrypts and verifies a message.
We need to verify the message in order to avoid padding attacks.
Reference: http://www.limited-entropy.com/padding-oracle-attacks
"""
def verify_and_decrypt(encrypted, secret, sign_secret, cipher \\ nil)
when is_binary(encrypted) and is_binary(secret) and is_binary(sign_secret) do
# TODO: Deprecate after backwards compatibility period
# IO.puts :stderr, "warning: `Plug.Crypto.MessageEncryptor.verify_and_decrypt/4` is deprecated," <>
# "please use `decrypt/3` instead\n" <> Exception.format_stacktrace
case cipher do
nil ->
if String.contains?(encrypted, ".") do
decrypt(encrypted, secret, sign_secret)
else
verify_and_decrypt(encrypted, secret, sign_secret, :aes_cbc256)
end
:aes_cbc256 ->
aes256_cbc_hmac_sha1_decrypt(encrypted, secret, sign_secret)
_ ->
case MessageVerifier.verify(encrypted, sign_secret) do
{:ok, verified} ->
[encrypted, iv] = String.split(verified, "--")
case Base.decode64(encrypted) do
{:ok, encrypted} ->
case Base.decode64(iv) do
{:ok, iv} ->
encrypted |> decrypt_legacy(cipher, secret, iv) |> pkcs7_unpad
:error ->
:error
end
:error ->
:error
end
:error ->
:error
end
end
end

defp encode_legacy_token(sign_secret, iv, cipher_text) do
cipher_text = Base.encode64(cipher_text) <> "--" <> Base.encode64(iv)
Expand Down Expand Up @@ -376,4 +321,40 @@ defmodule Plug.Crypto.MessageEncryptor do
:crypto.block_decrypt(cipher, secret, iv, encrypted)
end

# Encrypts and authenticates a message using AES128-CBC mode
# with HMAC-SHA-1 for the authentication code.
defp aes256_cbc_hmac_sha1_encrypt(plain_text, secret, sign_secret) when bit_size(secret) > 256 do
aes256_cbc_hmac_sha1_encrypt(plain_text, binary_part(secret, 0, 32), sign_secret)
end
defp aes256_cbc_hmac_sha1_encrypt(plain_text, secret, sign_secret)
when is_binary(plain_text) and bit_size(secret) in [128, 192, 256] and is_binary(sign_secret) do
iv = :crypto.strong_rand_bytes(16)
cipher_text = :crypto.block_encrypt(:aes_cbc256, secret, iv, pkcs7_pad(plain_text))
encode_legacy_token(sign_secret, iv, cipher_text)
end

# Verifies and decrypts a message using AES128-CBC mode
# with HMAC-SHA-1 for the authentication code.
#
# Decryption will never be performed prior to verification.
defp aes256_cbc_hmac_sha1_decrypt(cipher_text, secret, sign_secret) when bit_size(secret) > 256 do
aes256_cbc_hmac_sha1_decrypt(cipher_text, binary_part(secret, 0, 32), sign_secret)
end
defp aes256_cbc_hmac_sha1_decrypt(cipher_text, secret, sign_secret)
when is_binary(cipher_text) and bit_size(secret) === 256 and is_binary(sign_secret) do
case decode_legacy_token(cipher_text, sign_secret) do
{"A256CBC-HS1", _encrypted_key, iv, cipher_text, cipher_tag}
when bit_size(iv) === 128 and bit_size(cipher_tag) === 160 ->
key = secret
:crypto.block_decrypt(:aes_cbc256, key, iv, cipher_text)
|> case do
plain_text when is_binary(plain_text) ->
pkcs7_unpad(plain_text)
_ ->
:error
end
_ ->
:error
end
end
end
2 changes: 0 additions & 2 deletions lib/plug/crypto/message_verifier.ex
Expand Up @@ -8,8 +8,6 @@ defmodule Plug.Crypto.MessageVerifier do
tampered with.
"""

# @delimiter "##"

@doc """
Signs a message according to the given secret.
"""
Expand Down

0 comments on commit c30ffae

Please sign in to comment.