@@ -9,34 +9,38 @@ defmodule Plug.Crypto.MessageEncryptor do
99 This can be used in situations similar to the `Plug.Crypto.MessageVerifier`,
1010 but where you don't want users to be able to determine the value of the payload.
1111
12- The current algorithm used is AES-GCM-128 .
12+ The current algorithm used is XChaCha20-Poly1305 .
1313
1414 ## Example
1515
1616 iex> secret_key_base = "072d1e0157c008193fe48a670cce031faa4e..."
1717 ...> encrypted_cookie_salt = "encrypted cookie"
18- ...> encrypted_signed_cookie_salt = "signed encrypted cookie"
19- ...>
2018 ...> secret = KeyGenerator.generate(secret_key_base, encrypted_cookie_salt)
21- ...> sign_secret = KeyGenerator.generate(secret_key_base, encrypted_signed_cookie_salt)
2219 ...>
2320 ...> data = "José"
24- ...> encrypted = MessageEncryptor.encrypt(data, secret, sign_secret )
25- ...> MessageEncryptor.decrypt(encrypted, secret, sign_secret )
21+ ...> encrypted = MessageEncryptor.encrypt(data, secret, "UNUSED" )
22+ ...> MessageEncryptor.decrypt(encrypted, secret, "UNUSED" )
2623 {:ok, "José"}
2724
2825 """
2926
3027 @ doc """
3128 Encrypts a message using authenticated encryption.
3229
30+ The `sign_secret` is currently only used on decryption
31+ for backwards compatibility.
32+
3333 A custom authentication message can be provided.
3434 It defaults to "A128GCM" for backwards compatibility.
3535 """
3636 def encrypt ( message , aad \\ "A128GCM" , secret , sign_secret )
37- when is_binary ( message ) and ( is_binary ( aad ) or is_list ( aad ) ) and byte_size ( secret ) > 0 and
37+ when is_binary ( message ) and ( is_binary ( aad ) or is_list ( aad ) ) and
38+ bit_size ( secret ) == 256 and
3839 is_binary ( sign_secret ) do
39- aes128_gcm_encrypt ( message , aad , secret , sign_secret )
40+ iv = :crypto . strong_rand_bytes ( 24 )
41+ { subkey , nonce } = xchacha20_subkey_and_nonce ( secret , iv )
42+ { cipher_text , cipher_tag } = block_encrypt ( :chacha20_poly1305 , subkey , nonce , { aad , message } )
43+ "XCP." <> Base . url_encode64 ( iv <> cipher_tag <> cipher_text , padding: false )
4044 rescue
4145 e -> reraise e , Plug.Crypto . prune_args_from_stacktrace ( __STACKTRACE__ )
4246 end
@@ -45,67 +49,45 @@ defmodule Plug.Crypto.MessageEncryptor do
4549 Decrypts a message using authenticated encryption.
4650 """
4751 def decrypt ( encrypted , aad \\ "A128GCM" , secret , sign_secret )
48- when is_binary ( encrypted ) and ( is_binary ( aad ) or is_list ( aad ) ) and byte_size ( secret ) > 0 and
52+ when is_binary ( encrypted ) and ( is_binary ( aad ) or is_list ( aad ) ) and
53+ bit_size ( secret ) in [ 128 , 192 , 256 ] and
4954 is_binary ( sign_secret ) do
50- aes128_gcm_decrypt ( encrypted , aad , secret , sign_secret )
55+ unguarded_decrypt ( encrypted , aad , secret , sign_secret )
5156 rescue
5257 e -> reraise e , Plug.Crypto . prune_args_from_stacktrace ( __STACKTRACE__ )
5358 end
5459
55- # Encrypts and authenticates a message using AES128-GCM mode.
56- #
57- # A random 128-bit content encryption key (CEK) is generated for
58- # every message which is then encrypted with `aes_gcm_key_wrap/3`.
59- defp aes128_gcm_encrypt ( plain_text , aad , secret , sign_secret ) when bit_size ( secret ) > 256 do
60- aes128_gcm_encrypt ( plain_text , aad , binary_part ( secret , 0 , 32 ) , sign_secret )
61- end
62-
63- defp aes128_gcm_encrypt ( plain_text , aad , secret , sign_secret )
64- when is_binary ( plain_text ) and bit_size ( secret ) in [ 128 , 192 , 256 ] and
65- is_binary ( sign_secret ) do
66- key = :crypto . strong_rand_bytes ( 16 )
67- iv = :crypto . strong_rand_bytes ( 12 )
68- { cipher_text , cipher_tag } = block_encrypt ( :aes_gcm , key , iv , { aad , plain_text } )
69- encrypted_key = aes_gcm_key_wrap ( key , secret , sign_secret )
70- encode_token ( "A128GCM" , encrypted_key , iv , cipher_text , cipher_tag )
60+ defp unguarded_decrypt ( "XCP." <> iv_cipher_text_cipher_tag , aad , secret , _sign_secret ) do
61+ with { :ok , << iv :: 192 - bits , cipher_tag :: 128 - bits , cipher_text :: binary >> } <-
62+ Base . url_decode64 ( iv_cipher_text_cipher_tag , padding: false ) ,
63+ { subkey , nonce } = xchacha20_subkey_and_nonce ( secret , iv ) ,
64+ plain_text when is_binary ( plain_text ) <-
65+ block_decrypt ( :chacha20_poly1305 , subkey , nonce , { aad , cipher_text , cipher_tag } ) do
66+ { :ok , plain_text }
67+ else
68+ _ -> :error
69+ end
7170 end
7271
73- # Verifies and decrypts a message using AES128-GCM mode.
74- #
75- # Decryption will never be performed prior to verification.
76- #
77- # The encrypted content encryption key (CEK) is decrypted
78- # with `aes_gcm_key_unwrap/3`.
79- defp aes128_gcm_decrypt ( cipher_text , aad , secret , sign_secret ) when bit_size ( secret ) > 256 do
80- aes128_gcm_decrypt ( cipher_text , aad , binary_part ( secret , 0 , 32 ) , sign_secret )
72+ # Messages from Plug.Crypto v1.x
73+ defp unguarded_decrypt ( "QTEyOEdDTQ." <> rest , aad , secret , sign_secret ) do
74+ with [ encrypted_key , iv , cipher_text , cipher_tag ] <- :binary . split ( rest , "." , [ :global ] ) ,
75+ { :ok , encrypted_key } <- Base . url_decode64 ( encrypted_key , padding: false ) ,
76+ { :ok , iv } when bit_size ( iv ) === 96 <- Base . url_decode64 ( iv , padding: false ) ,
77+ { :ok , cipher_text } <- Base . url_decode64 ( cipher_text , padding: false ) ,
78+ { :ok , cipher_tag } when bit_size ( cipher_tag ) === 128 <-
79+ Base . url_decode64 ( cipher_tag , padding: false ) ,
80+ { :ok , key } <- aes_gcm_key_unwrap ( encrypted_key , secret , sign_secret ) ,
81+ plain_text when is_binary ( plain_text ) <-
82+ block_decrypt ( :aes_gcm , key , iv , { aad , cipher_text , cipher_tag } ) do
83+ { :ok , plain_text }
84+ else
85+ _ -> :error
86+ end
8187 end
8288
83- defp aes128_gcm_decrypt ( cipher_text , aad , secret , sign_secret )
84- when is_binary ( cipher_text ) and bit_size ( secret ) in [ 128 , 192 , 256 ] and
85- is_binary ( sign_secret ) do
86- case decode_token ( cipher_text ) do
87- { "A128GCM" , encrypted_key , iv , cipher_text , cipher_tag }
88- when bit_size ( iv ) === 96 and bit_size ( cipher_tag ) === 128 ->
89- encrypted_key
90- |> aes_gcm_key_unwrap ( secret , sign_secret )
91- |> case do
92- { :ok , key } ->
93- block_decrypt ( :aes_gcm , key , iv , { aad , cipher_text , cipher_tag } )
94-
95- _ ->
96- :error
97- end
98- |> case do
99- plain_text when is_binary ( plain_text ) ->
100- { :ok , plain_text }
101-
102- _ ->
103- :error
104- end
105-
106- _ ->
107- :error
108- end
89+ defp unguarded_decrypt ( _rest , _aad , _secret , _sign_secret ) do
90+ :error
10991 end
11092
11193 defp block_encrypt ( cipher , key , iv , { aad , payload } ) do
@@ -132,32 +114,65 @@ defmodule Plug.Crypto.MessageEncryptor do
132114 "Please make sure it was compiled with the correct OpenSSL/BoringSSL bindings"
133115 end
134116
135- # Wraps a decrypted content encryption key (CEK) with secret and
136- # sign_secret using AES GCM mode. Accepts keys of 128, 192, or
137- # 256 bits based on the length of the secret key.
138- #
139- # See: https://tools.ietf.org/html/rfc7518#section-4.7
140- defp aes_gcm_key_wrap ( cek , secret , sign_secret ) when bit_size ( secret ) > 256 do
141- aes_gcm_key_wrap ( cek , binary_part ( secret , 0 , 32 ) , sign_secret )
117+ defp xchacha20_subkey_and_nonce ( << key :: 256 - bits >> , << nonce0 :: 128 - bits , nonce1 :: 64 - bits >> ) do
118+ subkey = hchacha20 ( key , nonce0 )
119+ nonce = << 0 :: 32 , nonce1 :: 64 - bits >>
120+ { subkey , nonce }
142121 end
143122
144- defp aes_gcm_key_wrap ( cek , secret , sign_secret )
145- when bit_size ( cek ) in [ 128 , 192 , 256 ] and bit_size ( secret ) in [ 128 , 192 , 256 ] and
146- is_binary ( sign_secret ) do
147- iv = :crypto . strong_rand_bytes ( 12 )
148- { cipher_text , cipher_tag } = block_encrypt ( :aes_gcm , secret , iv , { sign_secret , cek } )
149- cipher_text <> cipher_tag <> iv
123+ defp hchacha20 ( << key :: 256 - bits >> , << nonce :: 128 - bits >> ) do
124+ # ChaCha20 has an internal blocksize of 512-bits (64-bytes).
125+ # Let's use a Mask of random 64-bytes to blind the intermediate keystream.
126+ mask = << mask_h :: 128 - bits , _ :: 256 - bits , mask_t :: 128 - bits >> = :crypto . strong_rand_bytes ( 64 )
127+
128+ << state_2h :: 128 - bits , _ :: 256 - bits , state_2t :: 128 - bits >> =
129+ :crypto . crypto_one_time ( :chacha20 , key , nonce , mask , true )
130+
131+ <<
132+ x00 :: 32 - unsigned - little - integer ,
133+ x01 :: 32 - unsigned - little - integer ,
134+ x02 :: 32 - unsigned - little - integer ,
135+ x03 :: 32 - unsigned - little - integer ,
136+ x12 :: 32 - unsigned - little - integer ,
137+ x13 :: 32 - unsigned - little - integer ,
138+ x14 :: 32 - unsigned - little - integer ,
139+ x15 :: 32 - unsigned - little - integer
140+ >> =
141+ :crypto . exor (
142+ << mask_h :: 128 - bits , mask_t :: 128 - bits >> ,
143+ << state_2h :: 128 - bits , state_2t :: 128 - bits >>
144+ )
145+
146+ ## The final step of ChaCha20 is `State2 = State0 + State1', so let's
147+ ## recover `State1' with subtraction: `State1 = State2 - State0'
148+ <<
149+ y00 :: 32 - unsigned - little - integer ,
150+ y01 :: 32 - unsigned - little - integer ,
151+ y02 :: 32 - unsigned - little - integer ,
152+ y03 :: 32 - unsigned - little - integer ,
153+ y12 :: 32 - unsigned - little - integer ,
154+ y13 :: 32 - unsigned - little - integer ,
155+ y14 :: 32 - unsigned - little - integer ,
156+ y15 :: 32 - unsigned - little - integer
157+ >> = << "expand 32-byte k" , nonce :: 128 - bits >>
158+
159+ <<
160+ x00 - y00 :: 32 - unsigned - little - integer ,
161+ x01 - y01 :: 32 - unsigned - little - integer ,
162+ x02 - y02 :: 32 - unsigned - little - integer ,
163+ x03 - y03 :: 32 - unsigned - little - integer ,
164+ x12 - y12 :: 32 - unsigned - little - integer ,
165+ x13 - y13 :: 32 - unsigned - little - integer ,
166+ x14 - y14 :: 32 - unsigned - little - integer ,
167+ x15 - y15 :: 32 - unsigned - little - integer
168+ >>
150169 end
151170
152171 # Unwraps an encrypted content encryption key (CEK) with secret and
153172 # sign_secret using AES GCM mode. Accepts keys of 128, 192, or 256
154173 # bits based on the length of the secret key.
155174 #
156175 # See: https://tools.ietf.org/html/rfc7518#section-4.7
157- defp aes_gcm_key_unwrap ( wrapped_cek , secret , sign_secret ) when bit_size ( secret ) > 256 do
158- aes_gcm_key_unwrap ( wrapped_cek , binary_part ( secret , 0 , 32 ) , sign_secret )
159- end
160-
161176 defp aes_gcm_key_unwrap ( wrapped_cek , secret , sign_secret )
162177 when bit_size ( secret ) in [ 128 , 192 , 256 ] and is_binary ( sign_secret ) do
163178 wrapped_cek
@@ -175,36 +190,7 @@ defmodule Plug.Crypto.MessageEncryptor do
175190 :error
176191 end
177192 |> case do
178- cek when bit_size ( cek ) in [ 128 , 192 , 256 ] ->
179- { :ok , cek }
180-
181- _ ->
182- :error
183- end
184- end
185-
186- defp encode_token ( protected , encrypted_key , iv , cipher_text , cipher_tag ) do
187- Base . url_encode64 ( protected , padding: false )
188- |> Kernel . <> ( "." )
189- |> Kernel . <> ( Base . url_encode64 ( encrypted_key , padding: false ) )
190- |> Kernel . <> ( "." )
191- |> Kernel . <> ( Base . url_encode64 ( iv , padding: false ) )
192- |> Kernel . <> ( "." )
193- |> Kernel . <> ( Base . url_encode64 ( cipher_text , padding: false ) )
194- |> Kernel . <> ( "." )
195- |> Kernel . <> ( Base . url_encode64 ( cipher_tag , padding: false ) )
196- end
197-
198- defp decode_token ( token ) do
199- with [ protected , encrypted_key , iv , cipher_text , cipher_tag ] <-
200- String . split ( token , "." , parts: 5 ) ,
201- { :ok , protected } <- Base . url_decode64 ( protected , padding: false ) ,
202- { :ok , encrypted_key } <- Base . url_decode64 ( encrypted_key , padding: false ) ,
203- { :ok , iv } <- Base . url_decode64 ( iv , padding: false ) ,
204- { :ok , cipher_text } <- Base . url_decode64 ( cipher_text , padding: false ) ,
205- { :ok , cipher_tag } <- Base . url_decode64 ( cipher_tag , padding: false ) do
206- { protected , encrypted_key , iv , cipher_text , cipher_tag }
207- else
193+ cek when bit_size ( cek ) in [ 128 , 192 , 256 ] -> { :ok , cek }
208194 _ -> :error
209195 end
210196 end
0 commit comments