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:
parent
7a2041335f
commit
5a3ba63d9a
@ -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"]
|
||||
|
Loading…
Reference in New Issue
Block a user