Permalink
Browse files

EncrytedCookieJar

Adding an encrypted cookie jar and associated tests. SignedCookieJar
and EncryptedCookieJar are subclasses of SecretCookieJar, which takes
care of ensuring the secret it good enough.
  • Loading branch information...
1 parent 308c0a7 commit 32feaa3f0cb88b04fa04901a597ee01b22e0ec85 @hmcfletch committed Dec 5, 2011
Showing with 138 additions and 19 deletions.
  1. +80 −19 actionpack/lib/action_dispatch/middleware/cookies.rb
  2. +58 −0 actionpack/test/dispatch/cookies_test.rb
@@ -233,6 +233,31 @@ def signed
@signed ||= SignedCookieJar.new(self, @secret)
end
+ # Returns a jar that'll automatically encrypt cookies values before writing them and decrypt the values when reading. If the
+ # cookie is tampered with by the user (or a 3rd party), an ActiveSupport::MessageVerifier::InvalidSignature exception will
+ # be raised. If the cookie is verified, but unable to be decrypted, an ActiveSupport::MessageEncryptor::InvalidMessage
+ # exception will be raised.
+ #
+ # This jar requires that you set a suitable secret for the verification on your app's config.secret_token.
+ #
+ # Example:
+ #
+ # cookies.encypted[:encrypted_cookie] = "you don't know what this says"
+ # # => Set-Cookie: LSuus8pkXd...ckqiG6qGlwuhSQn--4Eb16w1z7ouNXQZAxV5Bjw==; path=/; expires=Sun, 27-Mar-2011 03:24:16 GMT
+ #
+ # This jar allows chaining with other jars as well, so you can set permanent, encrypted cookies. Examples:
+ #
+ # cookies.permanent.encypted[:encrypted_permanent] = "you don't know what this says, but it will be here for 20 years"
+ # # => Set-Cookie: Sok2G6hGs...XFeUpDWQLT8=--UZe+JlZPlMuxHYSq09oV0w==; path=/; expires=Thu, 27 Mar 2031 13:48:43 GMT
+ #
+ # To read encypted cookies:
+ #
+ # cookies.encrypted[:encrypted_cookie] # => "you don't know what this says"
+ # cookies.encrypted[:encrypted_permanent] # => "you don't know what this says, but it will be here for 20 years"
+ def encrypted
+ @encrypted ||= EncryptedCookieJar.new(self, @secret)
+ 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) }
@@ -275,13 +300,45 @@ def method_missing(method, *arguments, &block)
end
end
- class SignedCookieJar < CookieJar #:nodoc:
+ class SecretCookieJar < CookieJar #:nodoc:
MAX_COOKIE_SIZE = 4096 # Cookies can typically store 4096 bytes.
SECRET_MIN_LENGTH = 30 # Characters
def initialize(parent_jar, secret)
ensure_secret_secure(secret)
@parent_jar = parent_jar
+ end
+
+
+ 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 = \"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 SignedCookieJar < SecretCookieJar #:nodoc:
+ def initialize(parent_jar, secret)
+ super(parent_jar, secret)
@verifier = ActiveSupport::MessageVerifier.new(secret)
end
@@ -305,30 +362,34 @@ def []=(key, options)
raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE
@parent_jar[key] = options
end
+ end
- def method_missing(method, *arguments, &block)
- @parent_jar.send(method, *arguments, &block)
+ class EncryptedCookieJar < SecretCookieJar #:nodoc:
+ def initialize(parent_jar, secret)
+ super(parent_jar, secret)
+ @encrypter = ActiveSupport::MessageEncryptor.new(secret)
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 = \"some secret phrase of at " +
- "least #{SECRET_MIN_LENGTH} characters\"" +
- "in config/initializers/secret_token.rb"
+ def [](name)
+ if encrypted_message = @parent_jar[name]
+ @encrypter.decrypt_and_verify(encrypted_message)
end
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
+ nil
+ rescue ActiveSupport::MessageEncryptor::InvalidMessage
+ nil
+ 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"
+ def []=(key, options)
+ if options.is_a?(Hash)
+ options.symbolize_keys!
+ options[:value] = @encrypter.encrypt_and_sign(options[:value])
+ else
+ options = { :value => @encrypter.encrypt_and_sign(options) }
end
+
+ raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE
+ @parent_jar[key] = options
end
end
@@ -63,6 +63,11 @@ def set_signed_cookie
head :ok
end
+ def set_encrypted_cookie
+ cookies.encrypted[:treasure_map] = "X marks the spot"
+ head :ok
+ end
+
def raise_data_overflow
cookies.signed[:foo] = 'bye!' * 1024
head :ok
@@ -74,11 +79,22 @@ def tampered_cookies
head :ok
end
+ def tampered_encrypted_cookies
+ cookies[:tampered_encrypted] = "BAhJIlBzZGZhc2YxeUd2NmFJZTluZUVpdFZVck1BZDV0KytnS1JJVXBsU0RXUmpBTDdIRms9LS02ZGczb04vRGpsMyswN0xnWjJmeExRPT0GOgZFVA%3D%3D--637b89be586e940f9c3e25ef51f1e6d0dc9f6251"
+ cookies.encrypted[:tampered_encrypted]
+ head :ok
+ end
+
def set_permanent_signed_cookie
cookies.permanent.signed[:remember_me] = 100
head :ok
end
+ def set_permanent_encrypted_cookie
+ cookies.permanent.encrypted[:buried_tresure] = "Gold!!!"
+ head :ok
+ end
+
def delete_and_set_cookie
cookies.delete :user_name
cookies[:user_name] = { :value => "david", :expires => Time.utc(2005, 10, 10,5) }
@@ -261,17 +277,33 @@ def test_signed_cookie
assert_equal 45, @controller.send(:cookies).signed[:user_id]
end
+ def test_encrypted_cookie
+ get :set_encrypted_cookie
+ assert_equal "X marks the spot", @controller.send(:cookies).encrypted[:treasure_map]
+ end
+
def test_accessing_nonexistant_signed_cookie_should_not_raise_an_invalid_signature
get :set_signed_cookie
assert_nil @controller.send(:cookies).signed[:non_existant_attribute]
end
+ def test_accessing_nonexistant_encrypted_cookie_should_not_raise_an_invalid_signature
+ get :set_encrypted_cookie
+ assert_nil @controller.send(:cookies).encrypted[:non_existant_attribute]
+ end
+
def test_permanent_signed_cookie
get :set_permanent_signed_cookie
assert_match(%r(#{20.years.from_now.utc.year}), @response.headers["Set-Cookie"])
assert_equal 100, @controller.send(:cookies).signed[:remember_me]
end
+ def test_permanent_encrypted_cookie
+ get :set_permanent_encrypted_cookie
+ assert_match(%r(#{20.years.from_now.utc.year}), @response.headers["Set-Cookie"])
+ assert_equal "Gold!!!", @controller.send(:cookies).encrypted[:buried_tresure]
+ end
+
def test_delete_and_set_cookie
get :delete_and_set_cookie
assert_cookie_header "user_name=david; path=/; expires=Mon, 10-Oct-2005 05:00:00 GMT"
@@ -291,6 +323,13 @@ def test_tampered_cookies
end
end
+ def test_tampered_encrypted_cookies
+ assert_nothing_raised do
+ get :tampered_encrypted_cookies
+ assert_response :success
+ end
+ end
+
def test_raises_argument_error_if_missing_secret
assert_raise(ArgumentError, nil.inspect) {
@request.env["action_dispatch.secret_token"] = nil
@@ -638,6 +677,25 @@ def test_setting_signed_cookies_raises_after_stream_back_to_client_even_without_
end
end
+ def test_setting_encrypted_cookies_raises_after_stream_back_to_client
+ with_test_route_set do
+ get '/set_cookies'
+ assert_raise(ActionDispatch::ClosedError) {
+ request.cookie_jar.encrypted['alert'] = 'alert'
+ cookies['alert'] = 'alert'
+ }
+ end
+ end
+
+ def test_setting_encrypted_cookies_raises_after_stream_back_to_client_even_without_cookies
+ with_test_route_set do
+ get '/dont_set_cookies'
+ assert_raise(ActionDispatch::ClosedError) {
+ request.cookie_jar.encrypted['alert'] = 'alert'
+ }
+ end
+ end
+
private
# Overwrite get to send SessionSecret in env hash

0 comments on commit 32feaa3

Please sign in to comment.