mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Merge pull request #28132 from mikeycgto/aead-encrypted-cookies
AEAD encrypted cookies and sessions
This commit is contained in:
commit
b88200f103
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 @@ module ActionDispatch
|
|||
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 @@ module ActionDispatch
|
|||
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 @@ module ActionDispatch
|
|||
# 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 @@ module ActionDispatch
|
|||
@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 @@ module ActionDispatch
|
|||
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 @@ module ActionDispatch
|
|||
"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 @@ module ActionDispatch
|
|||
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 @@ module ActionDispatch
|
|||
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 @@ module ActionDispatch
|
|||
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 @@ class CookiesTest < ActionController::TestCase
|
|||
@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 @@ class CookiesTest < ActionController::TestCase
|
|||
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 @@ class CookiesTest < ActionController::TestCase
|
|||
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 @@ class CookiesTest < ActionController::TestCase
|
|||
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 @@ class CookiesTest < ActionController::TestCase
|
|||
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 @@ class CookiesTest < ActionController::TestCase
|
|||
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 @@ class CookiesTest < ActionController::TestCase
|
|||
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 @@ class CookiesTest < ActionController::TestCase
|
|||
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 @@ class CookiesTest < ActionController::TestCase
|
|||
|
||||
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 @@ class CookiesTest < ActionController::TestCase
|
|||
@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 @@ class CookiesTest < ActionController::TestCase
|
|||
|
||||
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 @@ class CookiesTest < ActionController::TestCase
|
|||
@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 @@ class CookiesTest < ActionController::TestCase
|
|||
|
||||
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 @@ class CookiesTest < ActionController::TestCase
|
|||
@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 @@ class CookiesTest < ActionController::TestCase
|
|||
|
||||
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 @@ class CookiesTest < ActionController::TestCase
|
|||
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 @@ module Rails
|
|||
"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 @@ module Rails
|
|||
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 @@ module ApplicationTests
|
|||
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 @@ module ApplicationTests
|
|||
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 @@ module ApplicationTests
|
|||
|
||||
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 @@ module ApplicationTests
|
|||
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 @@ module ApplicationTests
|
|||
|
||||
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 @@ module ApplicationTests
|
|||
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 a new issue