AEAD encrypted cookies and sessions

This commit changes encrypted cookies from AES in CBC HMAC mode to
Authenticated Encryption using AES-GCM. It also provides a cookie jar
to transparently upgrade encrypted cookies to this new scheme. Some
other notable changes include:

- There is a new application configuration value:
  +use_authenticated_cookie_encryption+. When enabled, AEAD encrypted
  cookies will be used.

- +cookies.signed+ does not raise a +TypeError+ now if the name of an
  encrypted cookie is used. Encrypted cookies using the same key as
  signed cookies would be verified and serialization would then fail
  due the message still be encrypted.
This commit is contained in:
Michael Coyne 2017-02-23 13:54:17 -05:00
parent 7a2041335f
commit 5a3ba63d9a
10 changed files with 295 additions and 108 deletions

@ -1,3 +1,13 @@
* AEAD encrypted cookies and sessions with GCM
Encrypted cookies now use AES-GCM which couples authentication and
encryption in one faster step and produces shorter ciphertexts. Cookies
encrypted using AES in CBC HMAC mode will be seamlessly upgraded when
this new mode is enabled via the
`action_dispatch.use_authenticated_cookie_encryption` configuration value.
*Michael J Coyne*
* Change the cache key format for fragments to make it easier to debug key churn. The new format is:
views/template/action.html.erb:7a1156131a6928cb0026877f8b749ac9/projects/123

@ -43,6 +43,10 @@ def encrypted_signed_cookie_salt
get_header Cookies::ENCRYPTED_SIGNED_COOKIE_SALT
end
def authenticated_encrypted_cookie_salt
get_header Cookies::AUTHENTICATED_ENCRYPTED_COOKIE_SALT
end
def secret_token
get_header Cookies::SECRET_TOKEN
end
@ -149,6 +153,7 @@ class Cookies
SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt".freeze
ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt".freeze
ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze
AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "action_dispatch.authenticated_encrypted_cookie_salt".freeze
SECRET_TOKEN = "action_dispatch.secret_token".freeze
SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze
COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze
@ -207,6 +212,9 @@ def signed
# If +secrets.secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
# legacy cookies signed with the old key generator will be transparently upgraded.
#
# If +config.action_dispatch.encrypted_cookie_salt+ and +config.action_dispatch.encrypted_signed_cookie_salt+
# are both set, legacy cookies encrypted with HMAC AES-256-CBC will be transparently upgraded.
#
# This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+.
#
# Example:
@ -219,6 +227,8 @@ def encrypted
@encrypted ||=
if upgrade_legacy_signed_cookies?
UpgradeLegacyEncryptedCookieJar.new(self)
elsif upgrade_legacy_hmac_aes_cbc_cookies?
UpgradeLegacyHmacAesCbcCookieJar.new(self)
else
EncryptedCookieJar.new(self)
end
@ -240,6 +250,13 @@ def signed_or_encrypted
def upgrade_legacy_signed_cookies?
request.secret_token.present? && request.secret_key_base.present?
end
def upgrade_legacy_hmac_aes_cbc_cookies?
request.secret_key_base.present? &&
request.authenticated_encrypted_cookie_salt.present? &&
request.encrypted_signed_cookie_salt.present? &&
request.encrypted_cookie_salt.present?
end
end
# Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream
@ -576,9 +593,11 @@ def initialize(parent_jar)
"Read the upgrade documentation to learn more about this new config option."
end
secret = key_generator.generate_key(request.encrypted_cookie_salt || "")[0, ActiveSupport::MessageEncryptor.key_len]
sign_secret = key_generator.generate_key(request.encrypted_signed_cookie_salt || "")
@encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
cipher = "aes-256-gcm"
key_len = ActiveSupport::MessageEncryptor.key_len(cipher)
secret = key_generator.generate_key(request.authenticated_encrypted_cookie_salt || "")[0, key_len]
@encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
end
private
@ -603,6 +622,32 @@ class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc:
include VerifyAndUpgradeLegacySignedMessage
end
# UpgradeLegacyHmacAesCbcCookieJar is used by ActionDispatch::Session::CookieStore
# to upgrade cookies encrypted with AES-256-CBC with HMAC to AES-256-GCM
class UpgradeLegacyHmacAesCbcCookieJar < EncryptedCookieJar
def initialize(parent_jar)
super
secret = key_generator.generate_key(request.encrypted_cookie_salt || "")[0, ActiveSupport::MessageEncryptor.key_len]
sign_secret = key_generator.generate_key(request.encrypted_signed_cookie_salt || "")
@legacy_encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
end
def decrypt_and_verify_legacy_encrypted_message(name, signed_message)
deserialize(name, @legacy_encryptor.decrypt_and_verify(signed_message)).tap do |value|
self[name] = { value: value }
end
rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage
nil
end
private
def parse(name, signed_message)
super || decrypt_and_verify_legacy_encrypted_message(name, signed_message)
end
end
def initialize(app)
@app = app
end

@ -16,6 +16,7 @@ class Railtie < Rails::Railtie # :nodoc:
config.action_dispatch.signed_cookie_salt = "signed cookie"
config.action_dispatch.encrypted_cookie_salt = "encrypted cookie"
config.action_dispatch.encrypted_signed_cookie_salt = "signed encrypted cookie"
config.action_dispatch.use_authenticated_cookie_encryption = false
config.action_dispatch.perform_deep_munge = true
config.action_dispatch.default_headers = {
@ -36,6 +37,8 @@ class Railtie < Rails::Railtie # :nodoc:
ActionDispatch::ExceptionWrapper.rescue_responses.merge!(config.action_dispatch.rescue_responses)
ActionDispatch::ExceptionWrapper.rescue_templates.merge!(config.action_dispatch.rescue_templates)
config.action_dispatch.authenticated_encrypted_cookie_salt = "authenticated encrypted cookie" if config.action_dispatch.use_authenticated_cookie_encryption
config.action_dispatch.always_write_cookie = Rails.env.development? if config.action_dispatch.always_write_cookie.nil?
ActionDispatch::Cookies::CookieJar.always_write_cookie = config.action_dispatch.always_write_cookie

@ -288,8 +288,7 @@ def setup
@request.env["action_dispatch.key_generator"] = ActiveSupport::KeyGenerator.new(SALT, iterations: 2)
@request.env["action_dispatch.signed_cookie_salt"] =
@request.env["action_dispatch.encrypted_cookie_salt"] =
@request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT
@request.env["action_dispatch.authenticated_encrypted_cookie_salt"] = SALT
@request.host = "www.nextangle.com"
end
@ -531,9 +530,7 @@ def test_encrypted_cookie_using_default_serializer
get :set_encrypted_cookie
cookies = @controller.send :cookies
assert_not_equal "bar", cookies[:foo]
assert_raise TypeError do
cookies.signed[:foo]
end
assert_nil cookies.signed[:foo]
assert_equal "bar", cookies.encrypted[:foo]
end
@ -542,9 +539,7 @@ def test_encrypted_cookie_using_marshal_serializer
get :set_encrypted_cookie
cookies = @controller.send :cookies
assert_not_equal "bar", cookies[:foo]
assert_raises TypeError do
cookies.signed[:foo]
end
assert_nil cookies.signed[:foo]
assert_equal "bar", cookies.encrypted[:foo]
end
@ -553,9 +548,7 @@ def test_encrypted_cookie_using_json_serializer
get :set_encrypted_cookie
cookies = @controller.send :cookies
assert_not_equal "bar", cookies[:foo]
assert_raises ::JSON::ParserError do
cookies.signed[:foo]
end
assert_nil cookies.signed[:foo]
assert_equal "bar", cookies.encrypted[:foo]
end
@ -564,9 +557,7 @@ def test_wrapped_encrypted_cookie_using_json_serializer
get :set_wrapped_encrypted_cookie
cookies = @controller.send :cookies
assert_not_equal "wrapped: bar", cookies[:foo]
assert_raises ::JSON::ParserError do
cookies.signed[:foo]
end
assert_nil cookies.signed[:foo]
assert_equal "wrapped: bar", cookies.encrypted[:foo]
end
@ -577,38 +568,16 @@ def test_encrypted_cookie_using_custom_serializer
assert_equal "bar was dumped and loaded", cookies.encrypted[:foo]
end
def test_encrypted_cookie_using_custom_digest
@request.env["action_dispatch.cookies_digest"] = "SHA256"
get :set_encrypted_cookie
cookies = @controller.send :cookies
assert_not_equal "bar", cookies[:foo]
assert_equal "bar", cookies.encrypted[:foo]
sign_secret = @request.env["action_dispatch.key_generator"].generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"])
sha1_verifier = ActiveSupport::MessageVerifier.new(sign_secret, serializer: ActiveSupport::MessageEncryptor::NullSerializer, digest: "SHA1")
sha256_verifier = ActiveSupport::MessageVerifier.new(sign_secret, serializer: ActiveSupport::MessageEncryptor::NullSerializer, digest: "SHA256")
assert_raises(ActiveSupport::MessageVerifier::InvalidSignature) do
sha1_verifier.verify(cookies[:foo])
end
assert_nothing_raised do
sha256_verifier.verify(cookies[:foo])
end
end
def test_encrypted_cookie_using_hybrid_serializer_can_migrate_marshal_dumped_value_to_json
@request.env["action_dispatch.cookies_serializer"] = :hybrid
key_generator = @request.env["action_dispatch.key_generator"]
encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"]
encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"]
secret = key_generator.generate_key(encrypted_cookie_salt)
sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt)
cipher = "aes-256-gcm"
salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)]
encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: Marshal)
marshal_value = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: Marshal).encrypt_and_sign("bar")
@request.headers["Cookie"] = "foo=#{marshal_value}"
marshal_value = encryptor.encrypt_and_sign("bar")
@request.headers["Cookie"] = "foo=#{::Rack::Utils.escape marshal_value}"
get :get_encrypted_cookie
@ -616,20 +585,21 @@ def test_encrypted_cookie_using_hybrid_serializer_can_migrate_marshal_dumped_val
assert_not_equal "bar", cookies[:foo]
assert_equal "bar", cookies.encrypted[:foo]
encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: JSON)
assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"])
json_encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON)
assert_not_nil @response.cookies["foo"]
assert_equal "bar", json_encryptor.decrypt_and_verify(@response.cookies["foo"])
end
def test_encrypted_cookie_using_hybrid_serializer_can_read_from_json_dumped_value
@request.env["action_dispatch.cookies_serializer"] = :hybrid
key_generator = @request.env["action_dispatch.key_generator"]
encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"]
encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"]
secret = key_generator.generate_key(encrypted_cookie_salt)
sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt)
json_value = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: JSON).encrypt_and_sign("bar")
@request.headers["Cookie"] = "foo=#{json_value}"
cipher = "aes-256-gcm"
salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)]
encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON)
json_value = encryptor.encrypt_and_sign("bar")
@request.headers["Cookie"] = "foo=#{::Rack::Utils.escape json_value}"
get :get_encrypted_cookie
@ -640,19 +610,6 @@ def test_encrypted_cookie_using_hybrid_serializer_can_read_from_json_dumped_valu
assert_nil @response.cookies["foo"]
end
def test_compat_encrypted_cookie_using_64_byte_key
# Cookie generated with 64 bytes secret
message = ["566d4e75536d686e633246564e6b493062557079626c566d51574d30515430394c53315665564a694e4563786555744f57537454576b396a5a31566a626e52525054303d2d2d34663234333330623130623261306163363562316266323335396164666364613564643134623131"].pack("H*")
@request.headers["Cookie"] = "foo=#{message}"
get :get_encrypted_cookie
cookies = @controller.send :cookies
assert_not_equal "bar", cookies[:foo]
assert_equal "bar", cookies.encrypted[:foo]
assert_nil @response.cookies["foo"]
end
def test_accessing_nonexistent_encrypted_cookie_should_not_raise_invalid_message
get :set_encrypted_cookie
assert_nil @controller.send(:cookies).encrypted[:non_existent_attribute]
@ -813,10 +770,10 @@ def test_legacy_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_c
assert_equal "bar", @controller.send(:cookies).encrypted[:foo]
key_generator = @request.env["action_dispatch.key_generator"]
secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"])
sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"])
encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret)
cipher = "aes-256-gcm"
salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)]
encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: Marshal)
assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"])
end
@ -842,8 +799,6 @@ def test_legacy_json_signed_cookie_is_read_and_transparently_encrypted_by_encryp
@request.env["action_dispatch.cookies_serializer"] = :json
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
@request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
@request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee"
@request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9"
legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate("bar")
@ -852,10 +807,10 @@ def test_legacy_json_signed_cookie_is_read_and_transparently_encrypted_by_encryp
assert_equal "bar", @controller.send(:cookies).encrypted[:foo]
key_generator = @request.env["action_dispatch.key_generator"]
secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"])
sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"])
encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: JSON)
cipher = "aes-256-gcm"
salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)]
encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON)
assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"])
end
@ -881,8 +836,6 @@ def test_legacy_json_signed_cookie_is_read_and_transparently_encrypted_by_encryp
@request.env["action_dispatch.cookies_serializer"] = :hybrid
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
@request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
@request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee"
@request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9"
legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate("bar")
@ -891,10 +844,10 @@ def test_legacy_json_signed_cookie_is_read_and_transparently_encrypted_by_encryp
assert_equal "bar", @controller.send(:cookies).encrypted[:foo]
key_generator = @request.env["action_dispatch.key_generator"]
secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"])
sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"])
encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: JSON)
cipher = "aes-256-gcm"
salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)]
encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON)
assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"])
end
@ -920,8 +873,6 @@ def test_legacy_marshal_signed_cookie_is_read_and_transparently_encrypted_by_enc
@request.env["action_dispatch.cookies_serializer"] = :hybrid
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
@request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
@request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee"
@request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9"
legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate("bar")
@ -930,10 +881,10 @@ def test_legacy_marshal_signed_cookie_is_read_and_transparently_encrypted_by_enc
assert_equal "bar", @controller.send(:cookies).encrypted[:foo]
key_generator = @request.env["action_dispatch.key_generator"]
secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"])
sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"])
encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: JSON)
cipher = "aes-256-gcm"
salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)]
encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON)
assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"])
end
@ -959,6 +910,89 @@ def test_legacy_signed_cookie_is_treated_as_nil_by_encrypted_cookie_jar_if_tampe
assert_nil @response.cookies["foo"]
end
def test_legacy_hmac_aes_cbc_encrypted_marshal_cookie_is_upgraded_to_authenticated_encrypted_cookie
@request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
@request.env["action_dispatch.encrypted_cookie_salt"] =
@request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT
key_generator = @request.env["action_dispatch.key_generator"]
encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"]
encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"]
secret = key_generator.generate_key(encrypted_cookie_salt)
sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt)
marshal_value = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: Marshal).encrypt_and_sign("bar")
@request.headers["Cookie"] = "foo=#{marshal_value}"
get :get_encrypted_cookie
cookies = @controller.send :cookies
assert_not_equal "bar", cookies[:foo]
assert_equal "bar", cookies.encrypted[:foo]
aead_cipher = "aes-256-gcm"
aead_salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
aead_secret = key_generator.generate_key(aead_salt)[0, ActiveSupport::MessageEncryptor.key_len(aead_cipher)]
aead_encryptor = ActiveSupport::MessageEncryptor.new(aead_secret, cipher: aead_cipher, serializer: Marshal)
assert_equal "bar", aead_encryptor.decrypt_and_verify(@response.cookies["foo"])
end
def test_legacy_hmac_aes_cbc_encrypted_json_cookie_is_upgraded_to_authenticated_encrypted_cookie
@request.env["action_dispatch.cookies_serializer"] = :json
@request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
@request.env["action_dispatch.encrypted_cookie_salt"] =
@request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT
key_generator = @request.env["action_dispatch.key_generator"]
encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"]
encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"]
secret = key_generator.generate_key(encrypted_cookie_salt)
sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt)
marshal_value = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: JSON).encrypt_and_sign("bar")
@request.headers["Cookie"] = "foo=#{marshal_value}"
get :get_encrypted_cookie
cookies = @controller.send :cookies
assert_not_equal "bar", cookies[:foo]
assert_equal "bar", cookies.encrypted[:foo]
aead_cipher = "aes-256-gcm"
aead_salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
aead_secret = key_generator.generate_key(aead_salt)[0, ActiveSupport::MessageEncryptor.key_len(aead_cipher)]
aead_encryptor = ActiveSupport::MessageEncryptor.new(aead_secret, cipher: aead_cipher, serializer: JSON)
assert_equal "bar", aead_encryptor.decrypt_and_verify(@response.cookies["foo"])
end
def test_legacy_hmac_aes_cbc_encrypted_cookie_using_64_byte_key_is_upgraded_to_authenticated_encrypted_cookie
@request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
@request.env["action_dispatch.encrypted_cookie_salt"] =
@request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT
# Cookie generated with 64 bytes secret
message = ["566d4e75536d686e633246564e6b493062557079626c566d51574d30515430394c53315665564a694e4563786555744f57537454576b396a5a31566a626e52525054303d2d2d34663234333330623130623261306163363562316266323335396164666364613564643134623131"].pack("H*")
@request.headers["Cookie"] = "foo=#{message}"
get :get_encrypted_cookie
cookies = @controller.send :cookies
assert_not_equal "bar", cookies[:foo]
assert_equal "bar", cookies.encrypted[:foo]
cipher = "aes-256-gcm"
salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)]
encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: Marshal)
assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"])
end
def test_cookie_with_all_domain_option
get :set_cookie_with_domain
assert_response :success

@ -456,10 +456,14 @@ to `'http authentication'`.
Defaults to `'signed cookie'`.
* `config.action_dispatch.encrypted_cookie_salt` sets the encrypted cookies salt
value. Defaults to `'encrypted cookie'`.
value. Defaults to `'encrypted cookie'`.
* `config.action_dispatch.encrypted_signed_cookie_salt` sets the signed
encrypted cookies salt value. Defaults to `'signed encrypted cookie'`.
encrypted cookies salt value. Defaults to `'signed encrypted cookie'`.
* `config.action_dispatch.authenticated_encrypted_cookie_salt` sets the
authenticated encrypted cookie salt. Defaults to `'authenticated encrypted
cookie'`.
* `config.action_dispatch.perform_deep_munge` configures whether `deep_munge`
method should be performed on the parameters. See [Security Guide](security.html#unsafe-query-generation)

@ -95,16 +95,23 @@ Rails 2 introduced a new default session storage, CookieStore. CookieStore saves
* The client can see everything you store in a session, because it is stored in clear-text (actually Base64-encoded, so not encrypted). So, of course, _you don't want to store any secrets here_. To prevent session hash tampering, a digest is calculated from the session with a server-side secret (`secrets.secret_token`) and inserted into the end of the cookie.
However, since Rails 4, the default store is EncryptedCookieStore. With
EncryptedCookieStore the session is encrypted before being stored in a cookie.
This prevents the user from accessing and tampering the content of the cookie.
Thus the session becomes a more secure place to store data. The encryption is
done using a server-side secret key `secrets.secret_key_base` stored in
`config/secrets.yml`.
In Rails 4, encrypted cookies through AES in CBC mode with HMAC using SHA1 for
verification was introduced. This prevents the user from accessing and tampering
the content of the cookie. Thus the session becomes a more secure place to store
data. The encryption is performed using a server-side `secrets.secret_key_base`.
Two salts are used when deriving keys for encryption and verification. These
salts are set via the `config.action_dispatch.encrypted_cookie_salt` and
`config.action_dispatch.encrypted_signed_cookie_salt` configuration values.
That means the security of this storage depends on this secret (and on the digest algorithm, which defaults to SHA1, for compatibility). So _don't use a trivial secret, i.e. a word from a dictionary, or one which is shorter than 30 characters, use `rails secret` instead_.
Rails 5.2 uses AES-GCM for the encryption which couples authentication
and encryption in one faster step and produces shorter ciphertexts.
`secrets.secret_key_base` is used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get `secrets.secret_key_base` initialized to a random key present in `config/secrets.yml`, e.g.:
Encrypted cookies are automatically upgraded if the
`config.action_dispatch.use_authenticated_cookie_encryption` is enabled.
_Do not use a trivial secret, i.e. a word from a dictionary, or one which is shorter than 30 characters! Instead use `rails secret` to generate secret keys!_
Applications get `secrets.secret_key_base` initialized to a random key present in `config/secrets.yml`, e.g.:
development:
secret_key_base: a75d...

@ -260,6 +260,7 @@ def env_config
"action_dispatch.signed_cookie_salt" => config.action_dispatch.signed_cookie_salt,
"action_dispatch.encrypted_cookie_salt" => config.action_dispatch.encrypted_cookie_salt,
"action_dispatch.encrypted_signed_cookie_salt" => config.action_dispatch.encrypted_signed_cookie_salt,
"action_dispatch.authenticated_encrypted_cookie_salt" => config.action_dispatch.authenticated_encrypted_cookie_salt,
"action_dispatch.cookies_serializer" => config.action_dispatch.cookies_serializer,
"action_dispatch.cookies_digest" => config.action_dispatch.cookies_digest
)

@ -88,6 +88,10 @@ def load_defaults(target_version)
active_record.cache_versioning = true
end
if respond_to?(:action_dispatch)
action_dispatch.use_authenticated_cookie_encryption = true
end
else
raise "Unknown version #{target_version.to_s.inspect}"
end

@ -9,3 +9,7 @@
# Make Active Record use stable #cache_key alongside new #cache_version method.
# This is needed for recyclable cache keys.
# Rails.application.config.active_record.cache_versioning = true
# Use AES 256 GCM authenticated encryption for encrypted cookies.
# Existing cookies will be converted on read then written with the new scheme.
# Rails.application.config.action_dispatch.use_authenticated_cookie_encryption = true

@ -162,6 +162,11 @@ def read_raw_cookie
end
RUBY
add_to_config <<-RUBY
# Enable AEAD cookies
config.action_dispatch.use_authenticated_cookie_encryption = true
RUBY
require "#{app_path}/config/environment"
get "/foo/write_session"
@ -171,9 +176,9 @@ def read_raw_cookie
get "/foo/read_encrypted_cookie"
assert_equal "1", last_response.body
secret = app.key_generator.generate_key("encrypted cookie")
sign_secret = app.key_generator.generate_key("signed encrypted cookie")
encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret)
cipher = "aes-256-gcm"
secret = app.key_generator.generate_key("authenticated encrypted cookie")
encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher)
get "/foo/read_raw_cookie"
assert_equal 1, encryptor.decrypt_and_verify(last_response.body)["foo"]
@ -209,6 +214,9 @@ def read_raw_cookie
add_to_config <<-RUBY
secrets.secret_token = "3b7cd727ee24e8444053437c36cc66c4"
# Enable AEAD cookies
config.action_dispatch.use_authenticated_cookie_encryption = true
RUBY
require "#{app_path}/config/environment"
@ -220,9 +228,9 @@ def read_raw_cookie
get "/foo/read_encrypted_cookie"
assert_equal "1", last_response.body
secret = app.key_generator.generate_key("encrypted cookie")
sign_secret = app.key_generator.generate_key("signed encrypted cookie")
encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret)
cipher = "aes-256-gcm"
secret = app.key_generator.generate_key("authenticated encrypted cookie")
encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher)
get "/foo/read_raw_cookie"
assert_equal 1, encryptor.decrypt_and_verify(last_response.body)["foo"]
@ -264,6 +272,9 @@ def read_raw_cookie
add_to_config <<-RUBY
secrets.secret_token = "3b7cd727ee24e8444053437c36cc66c4"
# Enable AEAD cookies
config.action_dispatch.use_authenticated_cookie_encryption = true
RUBY
require "#{app_path}/config/environment"
@ -279,9 +290,73 @@ def read_raw_cookie
get "/foo/read_encrypted_cookie"
assert_equal "2", last_response.body
secret = app.key_generator.generate_key("encrypted cookie")
sign_secret = app.key_generator.generate_key("signed encrypted cookie")
encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret)
cipher = "aes-256-gcm"
secret = app.key_generator.generate_key("authenticated encrypted cookie")
encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher)
get "/foo/read_raw_cookie"
assert_equal 2, encryptor.decrypt_and_verify(last_response.body)["foo"]
end
test "session upgrading from AES-CBC-HMAC encryption to AES-GCM encryption" do
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get ':controller(/:action)'
end
RUBY
controller :foo, <<-RUBY
class FooController < ActionController::Base
def write_raw_session
# AES-256-CBC with SHA1 HMAC
# {"session_id"=>"1965d95720fffc123941bdfb7d2e6870", "foo"=>1}
cookies[:_myapp_session] = "TlgrdS85aUpDd1R2cDlPWlR6K0FJeGExckwySjZ2Z0pkR3d2QnRObGxZT25aalJWYWVvbFVLcHF4d0VQVDdSaFF2QjFPbG9MVjJzeWp3YjcyRUlKUUU2ZlR4bXlSNG9ZUkJPRUtld0E3dVU9LS0xNDZXbGpRZ3NjdW43N2haUEZJSUNRPT0=--3639b5ce54c09495cfeaae928cd5634e0c4b2e96"
head :ok
end
def write_session
session[:foo] = session[:foo] + 1
head :ok
end
def read_session
render plain: session[:foo]
end
def read_encrypted_cookie
render plain: cookies.encrypted[:_myapp_session]['foo']
end
def read_raw_cookie
render plain: cookies[:_myapp_session]
end
end
RUBY
add_to_config <<-RUBY
# Use a static key
secrets.secret_key_base = "known key base"
# Enable AEAD cookies
config.action_dispatch.use_authenticated_cookie_encryption = true
RUBY
require "#{app_path}/config/environment"
get "/foo/write_raw_session"
get "/foo/read_session"
assert_equal "1", last_response.body
get "/foo/write_session"
get "/foo/read_session"
assert_equal "2", last_response.body
get "/foo/read_encrypted_cookie"
assert_equal "2", last_response.body
cipher = "aes-256-gcm"
secret = app.key_generator.generate_key("authenticated encrypted cookie")
encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher)
get "/foo/read_raw_cookie"
assert_equal 2, encryptor.decrypt_and_verify(last_response.body)["foo"]