From 60609bb50d5b99d78a01a945a539cccd061cd7e7 Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Wed, 31 Oct 2012 01:06:46 -0200 Subject: Sign cookies using key deriver --- .../lib/action_dispatch/middleware/cookies.rb | 34 ++++++++++++---------- 1 file changed, 19 insertions(+), 15 deletions(-) (limited to 'actionpack/lib/action_dispatch') diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index eaf922595a..0ec7f24a14 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -27,7 +27,7 @@ module ActionDispatch # cookies[:login] = { value: "XJ-122", expires: 1.hour.from_now } # # # Sets a signed cookie, which prevents users from tampering with its value. - # # The cookie is signed by your app's config.secret_token value. + # # The cookie is signed by your app's config.secret_token_key value. # # It can be read using the signed method cookies.signed[:key] # cookies.signed[:user_id] = current_user.id # @@ -79,8 +79,8 @@ module ActionDispatch # * :httponly - Whether this cookie is accessible via scripting or # only HTTP. Defaults to +false+. class Cookies - HTTP_HEADER = "Set-Cookie".freeze - TOKEN_KEY = "action_dispatch.secret_token".freeze + HTTP_HEADER = "Set-Cookie".freeze + GENERATOR_KEY = "action_dispatch.key_generator".freeze # Raised when storing more than 4K of session data. CookieOverflow = Class.new StandardError @@ -103,17 +103,19 @@ module ActionDispatch DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/ def self.build(request) - secret = request.env[TOKEN_KEY] + env = request.env + key_generator = env[GENERATOR_KEY] + host = request.host secure = request.ssl? - new(secret, host, secure).tap do |hash| + new(key_generator, host, secure).tap do |hash| hash.update(request.cookies) end end - def initialize(secret = nil, host = nil, secure = false) - @secret = secret + def initialize(key_generator, host = nil, secure = false) + @key_generator = key_generator @set_cookies = {} @delete_cookies = {} @host = host @@ -220,7 +222,7 @@ module ActionDispatch # cookies.permanent.signed[:remember_me] = current_user.id # # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT def permanent - @permanent ||= PermanentCookieJar.new(self, @secret) + @permanent ||= PermanentCookieJar.new(self, @key_generator) end # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from @@ -228,7 +230,7 @@ module ActionDispatch # cookie was tampered with by the user (or a 3rd party), an ActiveSupport::MessageVerifier::InvalidSignature exception will # be raised. # - # This jar requires that you set a suitable secret for the verification on your app's +config.secret_token+. + # This jar requires that you set a suitable secret for the verification on your app's +config.secret_token_key+. # # Example: # @@ -237,7 +239,7 @@ module ActionDispatch # # cookies.signed[:discount] # => 45 def signed - @signed ||= SignedCookieJar.new(self, @secret) + @signed ||= SignedCookieJar.new(self, @key_generator) end def write(headers) @@ -261,8 +263,9 @@ module ActionDispatch end class PermanentCookieJar < CookieJar #:nodoc: - def initialize(parent_jar, secret) - @parent_jar, @secret = parent_jar, secret + def initialize(parent_jar, key_generator) + @parent_jar = parent_jar + @key_generator = key_generator end def []=(key, options) @@ -285,9 +288,10 @@ module ActionDispatch MAX_COOKIE_SIZE = 4096 # Cookies can typically store 4096 bytes. SECRET_MIN_LENGTH = 30 # Characters - def initialize(parent_jar, secret) - ensure_secret_secure(secret) + def initialize(parent_jar, key_generator) @parent_jar = parent_jar + secret = key_generator.generate_key('signed cookie') + ensure_secret_secure(secret) @verifier = ActiveSupport::MessageVerifier.new(secret) end @@ -323,7 +327,7 @@ module ActionDispatch if secret.blank? raise ArgumentError, "A secret is required to generate an " + "integrity hash for cookie session data. Use " + - "config.secret_token = \"some secret phrase of at " + + "config.secret_token_key = \"some secret phrase of at " + "least #{SECRET_MIN_LENGTH} characters\"" + "in config/initializers/secret_token.rb" end -- cgit v1.2.3 From 38c40dbbc1de5837a05d762be95e69105acc929c Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Tue, 30 Oct 2012 16:41:11 -0200 Subject: Add cookie.encrypted which returns an EncryptedCookieJar How to use it? cookies.encrypted[:discount] = 45 => Set-Cookie: discount=ZS9ZZ1R4cG1pcUJ1bm80anhQang3dz09LS1mbDZDSU5scGdOT3ltQ2dTdlhSdWpRPT0%3D--ab54663c9f4e3bc340c790d6d2b71e92f5b60315; path=/ cookies.encrypted[:discount] => 45 --- .../lib/action_dispatch/middleware/cookies.rb | 48 ++++++++++++++++++++++ 1 file changed, 48 insertions(+) (limited to 'actionpack/lib/action_dispatch') diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 0ec7f24a14..7206306b94 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/module/attribute_accessors' +require 'active_support/message_verifier' module ActionDispatch class Request < Rack::Request @@ -242,6 +243,22 @@ module ActionDispatch @signed ||= SignedCookieJar.new(self, @key_generator) end + # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read. + # If the cookie was tampered with by the user (or a 3rd party), an ActiveSupport::MessageVerifier::InvalidSignature exception + # will be raised. + # + # This jar requires that you set a suitable secret for the verification on your app's +config.secret_token_key+. + # + # Example: + # + # cookies.encrypted[:discount] = 45 + # # => Set-Cookie: discount=ZS9ZZ1R4cG1pcUJ1bm80anhQang3dz09LS1mbDZDSU5scGdOT3ltQ2dTdlhSdWpRPT0%3D--ab54663c9f4e3bc340c790d6d2b71e92f5b60315; path=/ + # + # cookies.encrypted[:discount] # => 45 + def encrypted + @encrypted ||= EncryptedCookieJar.new(self, @key_generator) + end + def write(headers) @set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) if write_cookie?(v) } @delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) } @@ -341,6 +358,37 @@ module ActionDispatch end end + class EncryptedCookieJar < SignedCookieJar #:nodoc: + def initialize(parent_jar, key_generator) + @parent_jar = parent_jar + secret = key_generator.generate_key('encrypted cookie') + sign_secret = key_generator.generate_key('signed encrypted cookie') + @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret) + ensure_secret_secure(secret) + end + + def [](name) + if encrypted_message = @parent_jar[name] + @encryptor.decrypt_and_verify(encrypted_message) + end + rescue ActiveSupport::MessageVerifier::InvalidSignature, + ActiveSupport::MessageVerifier::InvalidMessage + nil + end + + def []=(key, options) + if options.is_a?(Hash) + options.symbolize_keys! + else + options = { :value => options } + end + options[:value] = @encryptor.encrypt_and_sign(options[:value]) + + raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE + @parent_jar[key] = options + end + end + def initialize(app) @app = app end -- cgit v1.2.3 From fb0cea2b8cf61cde1aa4c640b56e896fbe308aa1 Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Tue, 30 Oct 2012 18:12:23 -0200 Subject: Add encrypted cookie store --- .../middleware/session/cookie_store.rb | 23 +++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) (limited to 'actionpack/lib/action_dispatch') diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index 3f28ea75ef..039846688e 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -57,8 +57,7 @@ module ActionDispatch def unpacked_cookie_data(env) env["action_dispatch.request.unsigned_session_cookie"] ||= begin stale_session_check! do - request = ActionDispatch::Request.new(env) - if data = request.cookie_jar.signed[@key] + if data = cookie_jar(env)[@key] data.stringify_keys! end data || {} @@ -72,8 +71,26 @@ module ActionDispatch end def set_cookie(env, session_id, cookie) + cookie_jar(env)[@key] = cookie + end + + def get_cookie + cookie_jar(env)[@key] + end + + def cookie_jar(env) + request = ActionDispatch::Request.new(env) + request.cookie_jar.signed + end + end + + class EncryptedCookieStore < CookieStore + + private + + def cookie_jar(env) request = ActionDispatch::Request.new(env) - request.cookie_jar.signed[@key] = cookie + request.cookie_jar.encrypted end end end -- cgit v1.2.3 From 47da5744741f0af668d2f915e09003be35dcce66 Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Thu, 1 Nov 2012 20:02:09 -0200 Subject: Allow users to change the default salt if they want, shouldn't be necessary --- .../lib/action_dispatch/middleware/cookies.rb | 33 ++++++++++++++-------- actionpack/lib/action_dispatch/railtie.rb | 4 +++ 2 files changed, 26 insertions(+), 11 deletions(-) (limited to 'actionpack/lib/action_dispatch') diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 7206306b94..daad64d041 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -82,6 +82,10 @@ module ActionDispatch class Cookies HTTP_HEADER = "Set-Cookie".freeze GENERATOR_KEY = "action_dispatch.key_generator".freeze + 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 + # Raised when storing more than 4K of session data. CookieOverflow = Class.new StandardError @@ -106,21 +110,25 @@ module ActionDispatch def self.build(request) env = request.env key_generator = env[GENERATOR_KEY] + options = { signed_cookie_salt: env[SIGNED_COOKIE_SALT], + encrypted_cookie_salt: env[ENCRYPTED_COOKIE_SALT], + encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] } host = request.host secure = request.ssl? - new(key_generator, host, secure).tap do |hash| + new(key_generator, host, secure, options).tap do |hash| hash.update(request.cookies) end end - def initialize(key_generator, host = nil, secure = false) + def initialize(key_generator, host = nil, secure = false, options = {}) @key_generator = key_generator @set_cookies = {} @delete_cookies = {} @host = host @secure = secure + @options = options @cookies = {} end @@ -223,7 +231,7 @@ module ActionDispatch # cookies.permanent.signed[:remember_me] = current_user.id # # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT def permanent - @permanent ||= PermanentCookieJar.new(self, @key_generator) + @permanent ||= PermanentCookieJar.new(self, @key_generator, @options) end # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from @@ -240,7 +248,7 @@ module ActionDispatch # # cookies.signed[:discount] # => 45 def signed - @signed ||= SignedCookieJar.new(self, @key_generator) + @signed ||= SignedCookieJar.new(self, @key_generator, @options) end # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read. @@ -256,7 +264,7 @@ module ActionDispatch # # cookies.encrypted[:discount] # => 45 def encrypted - @encrypted ||= EncryptedCookieJar.new(self, @key_generator) + @encrypted ||= EncryptedCookieJar.new(self, @key_generator, @options) end def write(headers) @@ -280,9 +288,10 @@ module ActionDispatch end class PermanentCookieJar < CookieJar #:nodoc: - def initialize(parent_jar, key_generator) + def initialize(parent_jar, key_generator, options = {}) @parent_jar = parent_jar @key_generator = key_generator + @options = options end def []=(key, options) @@ -305,9 +314,10 @@ module ActionDispatch MAX_COOKIE_SIZE = 4096 # Cookies can typically store 4096 bytes. SECRET_MIN_LENGTH = 30 # Characters - def initialize(parent_jar, key_generator) + def initialize(parent_jar, key_generator, options = {}) @parent_jar = parent_jar - secret = key_generator.generate_key('signed cookie') + @options = options + secret = key_generator.generate_key(@options[:signed_cookie_salt]) ensure_secret_secure(secret) @verifier = ActiveSupport::MessageVerifier.new(secret) end @@ -359,10 +369,11 @@ module ActionDispatch end class EncryptedCookieJar < SignedCookieJar #:nodoc: - def initialize(parent_jar, key_generator) + def initialize(parent_jar, key_generator, options = {}) @parent_jar = parent_jar - secret = key_generator.generate_key('encrypted cookie') - sign_secret = key_generator.generate_key('signed encrypted cookie') + @options = options + secret = key_generator.generate_key(@options[:encrypted_cookie_salt]) + sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt]) @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret) ensure_secret_secure(secret) end diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index 284dd180db..98c87d9b2d 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -13,6 +13,10 @@ module ActionDispatch config.action_dispatch.rescue_responses = { } config.action_dispatch.default_charset = nil config.action_dispatch.rack_cache = false + config.action_dispatch.http_auth_salt = 'http authentication' + 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.default_headers = { 'X-Frame-Options' => 'SAMEORIGIN', -- cgit v1.2.3 From c2a7956eb7fbc099ea38d21601d215ab3def27fb Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Fri, 2 Nov 2012 11:03:18 -0200 Subject: Move ensure_secret_secure to DummyKeyGenerator --- .../lib/action_dispatch/middleware/cookies.rb | 24 ---------------------- 1 file changed, 24 deletions(-) (limited to 'actionpack/lib/action_dispatch') diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index daad64d041..69da24f05b 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -312,13 +312,11 @@ module ActionDispatch class SignedCookieJar < CookieJar #:nodoc: MAX_COOKIE_SIZE = 4096 # Cookies can typically store 4096 bytes. - SECRET_MIN_LENGTH = 30 # Characters def initialize(parent_jar, key_generator, options = {}) @parent_jar = parent_jar @options = options secret = key_generator.generate_key(@options[:signed_cookie_salt]) - ensure_secret_secure(secret) @verifier = ActiveSupport::MessageVerifier.new(secret) end @@ -345,27 +343,6 @@ module ActionDispatch def method_missing(method, *arguments, &block) @parent_jar.send(method, *arguments, &block) end - - protected - - # To prevent users from using something insecure like "Password" we make sure that the - # secret they've provided is at least 30 characters in length. - def ensure_secret_secure(secret) - if secret.blank? - raise ArgumentError, "A secret is required to generate an " + - "integrity hash for cookie session data. Use " + - "config.secret_token_key = \"some secret phrase of at " + - "least #{SECRET_MIN_LENGTH} characters\"" + - "in config/initializers/secret_token.rb" - end - - if secret.length < SECRET_MIN_LENGTH - raise ArgumentError, "Secret should be something secure, " + - "like \"#{SecureRandom.hex(16)}\". The value you " + - "provided, \"#{secret}\", is shorter than the minimum length " + - "of #{SECRET_MIN_LENGTH} characters" - end - end end class EncryptedCookieJar < SignedCookieJar #:nodoc: @@ -375,7 +352,6 @@ module ActionDispatch secret = key_generator.generate_key(@options[:encrypted_cookie_salt]) sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt]) @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret) - ensure_secret_secure(secret) end def [](name) -- cgit v1.2.3 From 4faa0418453055bc81456685d418d486252cc379 Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Fri, 2 Nov 2012 20:27:51 -0200 Subject: Rename secret_token_key to secret_key_base --- actionpack/lib/action_dispatch/middleware/cookies.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'actionpack/lib/action_dispatch') diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 69da24f05b..1090473797 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -28,7 +28,7 @@ module ActionDispatch # cookies[:login] = { value: "XJ-122", expires: 1.hour.from_now } # # # Sets a signed cookie, which prevents users from tampering with its value. - # # The cookie is signed by your app's config.secret_token_key value. + # # The cookie is signed by your app's config.secret_key_base value. # # It can be read using the signed method cookies.signed[:key] # cookies.signed[:user_id] = current_user.id # @@ -239,7 +239,7 @@ module ActionDispatch # cookie was tampered with by the user (or a 3rd party), an ActiveSupport::MessageVerifier::InvalidSignature exception will # be raised. # - # This jar requires that you set a suitable secret for the verification on your app's +config.secret_token_key+. + # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+. # # Example: # @@ -255,7 +255,7 @@ module ActionDispatch # If the cookie was tampered with by the user (or a 3rd party), an ActiveSupport::MessageVerifier::InvalidSignature exception # will be raised. # - # This jar requires that you set a suitable secret for the verification on your app's +config.secret_token_key+. + # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+. # # Example: # -- cgit v1.2.3 From d63783983f8c03d5c624938081615579dcc753f7 Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Fri, 2 Nov 2012 00:43:24 -0200 Subject: Disallow ability to use EncryptedCookieJar with DummyKeyGenerator Developers must set config.secret_key_base in config/initializers/secret_token.rb --- actionpack/lib/action_dispatch/middleware/cookies.rb | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'actionpack/lib/action_dispatch') diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 1090473797..7936dcb515 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -347,6 +347,11 @@ module ActionDispatch class EncryptedCookieJar < SignedCookieJar #:nodoc: def initialize(parent_jar, key_generator, options = {}) + if ActiveSupport::DummyKeyGenerator === key_generator + raise "Encrypted Cookies must be used in conjunction with config.secret_key_base." + + "Set config.secret_key_base in config/initializers/secret_token.rb" + end + @parent_jar = parent_jar @options = options secret = key_generator.generate_key(@options[:encrypted_cookie_salt]) -- cgit v1.2.3