So, you want to write an encrypted cookie in Rails?
Let's walk through how Rails turns a key and a value into an encrypted cookie!
cookies.encrypted[] = ""
๐ Wrap
Great! First, Rails packages that value up into a cookie payload. That's just the cookie value with some metadata for security.
Are you using cookies with metadata? (It's the default since Rails 6.0.)
Rails.application.config.action_dispatch.use_cookies_with_metadata =
OK, here's the payload:
When reading cookies, pur, which stands for
"purpose", is cross-checked with the cookie's
name. That way, an attacker can't
copy the value
of one cookie to one with a different name.
๐ Serialize
But that payload is a Ruby hash. We need to turn that into a sequence of
bytes to encrypt.
A string is a series of bytes, and JSON is a string, so let's use JSON, the
default cookie serializer
as of Rails 7.0:
Now we have a string. But to encrypt it, we still need an encryption key.
๐ Make an encryption key
We're going to use
PBKDF2 to make one
from your secret key base.
PBKDF2 will essentially hash your
secret key base with a
salt a lot (like 1,000 times).
So we also need a salt, your secret key base, and a hash function:
ENV["SECRET_KEY_BASE"] = ""
Rails.application.configure do |config|
config.action_dispatch.authenticated_encrypted_cookie_salt
= ""
config.active_support.key_generator_hash_digest_class =
OpenSSL::Digest::
end
Lastly, the number of bytes we need our new key to have depends on the
encryption cipher.
AES-GCM is a good cipher. How many bits long do you
want your key to be?
(The default is 256, so I'd stick with that unless you have a
reason not to.)
Rails.application.config.action_dispatch.encrypted_cookie_cipher =
"aes--gcm"
Mix it all together, and... tada! ๐ Here's the secret key!
(It's bits; I
Base64-encoded it for your human eyes.)
...
๐ Encrypt!
Now that we have an encryption key, we're almost ready to encrypt the payload. All we need is a randomly-generated initialization vector , which helps us thwart decryption attacks and lets us decrypt the data on the other end:
...
And presto! Here's the encrypted payload. ๐
...
AES-GCM also outputs an "authentication tag," which prevents tampering. You can think of it kind of like a built-in signature. Here's the authentication tag:
...
Now, we have:
- The ciphertext, which is the encrypted payload containing the cookie's value and some other stuff;
- The initialization vector, which is required for decryption;
- The authentication tag, which ensures that nobody's tampered with the ciphertext.
Finally, we concatenate these three together with -- as a
separator, which allows us to
extract the pieces again for decryption. And voila! We have
the cookie!
...--...--...
Whew! Rails cookies are complex. We learned about a whole host of configuration options that go into encrypting a single cookie:
ENV["SECRET_KEY_BASE"] = ""
Rails.application.configure do |config|
config.action_dispatch.use_cookies_with_metadata =
config.action_dispatch.authenticated_encrypted_cookie_salt
= ""
config.active_support.key_generator_hash_digest_class =
OpenSSL::Digest::
config.action_dispatch.encrypted_cookie_cipher =
"aes--gcm"
end