message_encryptor.ex (8463B)
1 defmodule Plug.Crypto.MessageEncryptor do 2 @moduledoc ~S""" 3 `MessageEncryptor` is a simple way to encrypt values which get stored 4 somewhere you don't trust. 5 6 The encrypted key, initialization vector, cipher text, and cipher tag 7 are base64url encoded and returned to you. 8 9 This can be used in situations similar to the `Plug.Crypto.MessageVerifier`, 10 but where you don't want users to be able to determine the value of the payload. 11 12 The current algorithm used is AES-GCM-128. 13 14 ## Example 15 16 iex> secret_key_base = "072d1e0157c008193fe48a670cce031faa4e..." 17 ...> encrypted_cookie_salt = "encrypted cookie" 18 ...> encrypted_signed_cookie_salt = "signed encrypted cookie" 19 ...> 20 ...> secret = KeyGenerator.generate(secret_key_base, encrypted_cookie_salt) 21 ...> sign_secret = KeyGenerator.generate(secret_key_base, encrypted_signed_cookie_salt) 22 ...> 23 ...> data = "José" 24 ...> encrypted = MessageEncryptor.encrypt(data, secret, sign_secret) 25 ...> MessageEncryptor.decrypt(encrypted, secret, sign_secret) 26 {:ok, "José"} 27 28 """ 29 30 @doc """ 31 Encrypts a message using authenticated encryption. 32 """ 33 def encrypt(message, secret, sign_secret) 34 when is_binary(message) and byte_size(secret) > 0 and is_binary(sign_secret) do 35 aes128_gcm_encrypt(message, secret, sign_secret) 36 rescue 37 e -> reraise e, Plug.Crypto.prune_args_from_stacktrace(__STACKTRACE__) 38 end 39 40 @doc """ 41 Decrypts a message using authenticated encryption. 42 """ 43 def decrypt(encrypted, secret, sign_secret) 44 when is_binary(encrypted) and byte_size(secret) > 0 and is_binary(sign_secret) do 45 aes128_gcm_decrypt(encrypted, secret, sign_secret) 46 rescue 47 e -> reraise e, Plug.Crypto.prune_args_from_stacktrace(__STACKTRACE__) 48 end 49 50 # Encrypts and authenticates a message using AES128-GCM mode. 51 # 52 # A random 128-bit content encryption key (CEK) is generated for 53 # every message which is then encrypted with `aes_gcm_key_wrap/3`. 54 defp aes128_gcm_encrypt(plain_text, secret, sign_secret) when bit_size(secret) > 256 do 55 aes128_gcm_encrypt(plain_text, binary_part(secret, 0, 32), sign_secret) 56 end 57 58 defp aes128_gcm_encrypt(plain_text, secret, sign_secret) 59 when is_binary(plain_text) and bit_size(secret) in [128, 192, 256] and 60 is_binary(sign_secret) do 61 key = :crypto.strong_rand_bytes(16) 62 iv = :crypto.strong_rand_bytes(12) 63 aad = "A128GCM" 64 {cipher_text, cipher_tag} = block_encrypt(:aes_gcm, key, iv, {aad, plain_text}) 65 encrypted_key = aes_gcm_key_wrap(key, secret, sign_secret) 66 encode_token(aad, encrypted_key, iv, cipher_text, cipher_tag) 67 end 68 69 # Verifies and decrypts a message using AES128-GCM mode. 70 # 71 # Decryption will never be performed prior to verification. 72 # 73 # The encrypted content encryption key (CEK) is decrypted 74 # with `aes_gcm_key_unwrap/3`. 75 defp aes128_gcm_decrypt(cipher_text, secret, sign_secret) when bit_size(secret) > 256 do 76 aes128_gcm_decrypt(cipher_text, binary_part(secret, 0, 32), sign_secret) 77 end 78 79 defp aes128_gcm_decrypt(cipher_text, secret, sign_secret) 80 when is_binary(cipher_text) and bit_size(secret) in [128, 192, 256] and 81 is_binary(sign_secret) do 82 case decode_token(cipher_text) do 83 {aad = "A128GCM", encrypted_key, iv, cipher_text, cipher_tag} 84 when bit_size(iv) === 96 and bit_size(cipher_tag) === 128 -> 85 encrypted_key 86 |> aes_gcm_key_unwrap(secret, sign_secret) 87 |> case do 88 {:ok, key} -> 89 block_decrypt(:aes_gcm, key, iv, {aad, cipher_text, cipher_tag}) 90 91 _ -> 92 :error 93 end 94 |> case do 95 plain_text when is_binary(plain_text) -> 96 {:ok, plain_text} 97 98 _ -> 99 :error 100 end 101 102 _ -> 103 :error 104 end 105 end 106 107 # TODO: remove when we require OTP 22 108 if Code.ensure_loaded?(:crypto) and function_exported?(:crypto, :crypto_one_time_aead, 6) do 109 defp block_encrypt(cipher, key, iv, {aad, payload}) do 110 cipher = cipher_alias(cipher, bit_size(key)) 111 :crypto.crypto_one_time_aead(cipher, key, iv, payload, aad, true) 112 catch 113 :error, :notsup -> raise_notsup(cipher) 114 end 115 116 defp block_decrypt(cipher, key, iv, {aad, payload, tag}) do 117 cipher = cipher_alias(cipher, bit_size(key)) 118 :crypto.crypto_one_time_aead(cipher, key, iv, payload, aad, tag, false) 119 catch 120 :error, :notsup -> raise_notsup(cipher) 121 end 122 123 # TODO: remove when we reqwuire OTP 24 (since it has similar alias handling) 124 defp cipher_alias(:aes_gcm, 128), do: :aes_128_gcm 125 defp cipher_alias(:aes_gcm, 192), do: :aes_192_gcm 126 defp cipher_alias(:aes_gcm, 256), do: :aes_256_gcm 127 defp cipher_alias(other, _), do: other 128 else 129 defp block_encrypt(cipher, key, iv, payload) do 130 :crypto.block_encrypt(cipher, key, iv, payload) 131 catch 132 :error, :notsup -> raise_notsup(cipher) 133 end 134 135 defp block_decrypt(cipher, key, iv, payload) do 136 :crypto.block_decrypt(cipher, key, iv, payload) 137 catch 138 :error, :notsup -> raise_notsup(cipher) 139 end 140 end 141 142 defp raise_notsup(algo) do 143 raise "the algorithm #{inspect(algo)} is not supported by your Erlang/OTP installation. " <> 144 "Please make sure it was compiled with the correct OpenSSL/BoringSSL bindings" 145 end 146 147 # Wraps a decrypted content encryption key (CEK) with secret and 148 # sign_secret using AES GCM mode. Accepts keys of 128, 192, or 149 # 256 bits based on the length of the secret key. 150 # 151 # See: https://tools.ietf.org/html/rfc7518#section-4.7 152 defp aes_gcm_key_wrap(cek, secret, sign_secret) when bit_size(secret) > 256 do 153 aes_gcm_key_wrap(cek, binary_part(secret, 0, 32), sign_secret) 154 end 155 156 defp aes_gcm_key_wrap(cek, secret, sign_secret) 157 when bit_size(cek) in [128, 192, 256] and bit_size(secret) in [128, 192, 256] and 158 is_binary(sign_secret) do 159 iv = :crypto.strong_rand_bytes(12) 160 {cipher_text, cipher_tag} = block_encrypt(:aes_gcm, secret, iv, {sign_secret, cek}) 161 cipher_text <> cipher_tag <> iv 162 end 163 164 # Unwraps an encrypted content encryption key (CEK) with secret and 165 # sign_secret using AES GCM mode. Accepts keys of 128, 192, or 256 166 # bits based on the length of the secret key. 167 # 168 # See: https://tools.ietf.org/html/rfc7518#section-4.7 169 defp aes_gcm_key_unwrap(wrapped_cek, secret, sign_secret) when bit_size(secret) > 256 do 170 aes_gcm_key_unwrap(wrapped_cek, binary_part(secret, 0, 32), sign_secret) 171 end 172 173 defp aes_gcm_key_unwrap(wrapped_cek, secret, sign_secret) 174 when bit_size(secret) in [128, 192, 256] and is_binary(sign_secret) do 175 wrapped_cek 176 |> case do 177 <<cipher_text::128-bitstring, cipher_tag::128-bitstring, iv::96-bitstring>> -> 178 block_decrypt(:aes_gcm, secret, iv, {sign_secret, cipher_text, cipher_tag}) 179 180 <<cipher_text::192-bitstring, cipher_tag::128-bitstring, iv::96-bitstring>> -> 181 block_decrypt(:aes_gcm, secret, iv, {sign_secret, cipher_text, cipher_tag}) 182 183 <<cipher_text::256-bitstring, cipher_tag::128-bitstring, iv::96-bitstring>> -> 184 block_decrypt(:aes_gcm, secret, iv, {sign_secret, cipher_text, cipher_tag}) 185 186 _ -> 187 :error 188 end 189 |> case do 190 cek when bit_size(cek) in [128, 192, 256] -> 191 {:ok, cek} 192 193 _ -> 194 :error 195 end 196 end 197 198 defp encode_token(protected, encrypted_key, iv, cipher_text, cipher_tag) do 199 Base.url_encode64(protected, padding: false) 200 |> Kernel.<>(".") 201 |> Kernel.<>(Base.url_encode64(encrypted_key, padding: false)) 202 |> Kernel.<>(".") 203 |> Kernel.<>(Base.url_encode64(iv, padding: false)) 204 |> Kernel.<>(".") 205 |> Kernel.<>(Base.url_encode64(cipher_text, padding: false)) 206 |> Kernel.<>(".") 207 |> Kernel.<>(Base.url_encode64(cipher_tag, padding: false)) 208 end 209 210 defp decode_token(token) do 211 with [protected, encrypted_key, iv, cipher_text, cipher_tag] <- 212 String.split(token, ".", parts: 5), 213 {:ok, protected} <- Base.url_decode64(protected, padding: false), 214 {:ok, encrypted_key} <- Base.url_decode64(encrypted_key, padding: false), 215 {:ok, iv} <- Base.url_decode64(iv, padding: false), 216 {:ok, cipher_text} <- Base.url_decode64(cipher_text, padding: false), 217 {:ok, cipher_tag} <- Base.url_decode64(cipher_tag, padding: false) do 218 {protected, encrypted_key, iv, cipher_text, cipher_tag} 219 else 220 _ -> :error 221 end 222 end 223 end