Skip to content

Commit

Permalink
AEAD encrypted cookies and sessions
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mjc-gh committed May 22, 2017
1 parent 7a20413 commit 5a3ba63
Show file tree
Hide file tree
Showing 10 changed files with 295 additions and 108 deletions.
10 changes: 10 additions & 0 deletions actionpack/CHANGELOG.md
@@ -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
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"
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
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

0 comments on commit 5a3ba63

Please sign in to comment.