Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions actionpack/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
51 changes: 48 additions & 3 deletions actionpack/lib/action_dispatch/middleware/cookies.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should just make this the default for ActiveSupport::MessageEncryptor, so we wouldn't need this line.

I'll make that part of @assain's GSoC tasks 😉

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds good. Agreed this should become the new default!

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
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions actionpack/lib/action_dispatch/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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

Expand Down
Loading