Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

encrypted cookie jar #5034

Closed
wants to merge 8 commits into from

10 participants

@hmcfletch

related to #3955

hmcfletch added some commits
@hmcfletch hmcfletch signed on PermanentCookieJar taken care of in CookieJar 308c0a7
@hmcfletch hmcfletch 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.
32feaa3
@hmcfletch hmcfletch EncryptedCookieJar
encrypted cookie data, refractor encrypted and signed cookies jars to
base off of a secret cookie jar to consolidate secret token validation
1b57610
@hmcfletch hmcfletch EncryptedCookieJar tests
mirroring all SignedCookieJar tests
d8fb51f
@hmcfletch hmcfletch Merge branch 'master' of github.com:hmcfletch/rails
Conflicts:
	actionpack/lib/action_dispatch/middleware/cookies.rb
	actionpack/test/dispatch/cookies_test.rb
0454248
@josevalim
Owner

This looks good. Could you please also add a CHANGELOG entry? /cc @NZKoz @jeremy

actionpack/lib/action_dispatch/middleware/cookies.rb
((8 lines not shown))
def method_missing(method, *arguments, &block)
@parent_jar.send(method, *arguments, &block)
end
end
- class SignedCookieJar < CookieJar #:nodoc:
+ # Base class for the signed and enrypted cookie jar.

There is a typo: encrypted

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@jeremy
Owner

Very nice, Les! Could you rebase and squash these commits?

@hmcfletch

Oh man... I will attempt to rebase... Please to not hold me responsible if I accidentally delete Minnesota during in the process.

@hmcfletch

Upon further inspection, my newness to git shines through. While working on this pull request, I merged rails/master into my branch along the way to keep up to date instead of rebasing my changes. It seems that this will mess with my ability to rebase easily since I have a few of the earlier commits were from a good while a go. Not really sure the best way to go about remedying this situation to keep the logs clean. Any help would be appreciated.

@josevalim
Owner

We may have a oneliner for this, but the simplest way I can suggest is to create a new branch from rails master and cherry-pick commits:

git remote add rails https://josevalim@github.com/rails/rails.git
git fetch rails
git checkout rails/master
git checkout -b my_new_branch

After you create this new branch from master, you can cherry-pick the commits from this pull-request branch. At the end you will have a clean slate to rebase and squash it.

@drogus
Collaborator

@hmcfletch there is one issue brought up in #5251, the same secret key should not be used for both encryption and authentication.

@NZKoz
Owner

I don't believe that the issues raised in #5251 are relevant here, CBC-MAC vs HMAC are completely different.

If someone wanted to reach out to a cryptographer for confirmation that'd be good, but for now barring specific issues with the HMAC / encrypt and sign approach we're using I think we can keep using it.

@thaidn

Hi NZKoz,

I also stated clearly in #5251 that I know CookieStore uses HMAC, but using the wrong MAC is just one of the issues that I raised.

Ask any cryptographers, and they would tell you the same issues or more.

@hmcfletch

I was going to do my rebase tonight and resubmit the pull request. I can try to address the issue of separate secrets as well. Briefly looking at Cookie and MessageEncryptor I tentatively propose the following for now:

In MessageEncryptor

  • Add an options key for verifier_secret that falls back to secret if absent
  • Raise warning/deprecation if secret and verify_secret are the same
  • A verifier_secret argument could made required at a later time

In Cookies

  • Add an ENCRYPTION_TOKEN_KEY = "action_dispatch.encryption_secret_token".freeze to Cookies
  • Look for ENCRYPTION_TOKEN_KEY while initializing, but fall back to TOKEN_KEY if absent
  • Raise a warning in EncryptedCookie saying that action_dispatch.encryption_secret_token is needed if secret and encryption_secret are the same

Thoughts?

@NZKoz
Owner

@thaidn the other two issues you mention are padding oracle attacks which I believe are only possible when the encrypted data isn't signed at all. So of the three issues you mentioned, none of them apply here as far as I can tell. I'm loathe to introduce another configuration option 'just because maybe there are issues with other crypto systems'.

@igrigorik could you get some word from your friends on the google security team who suggested the feature, see if we can get a final answer on it?

@hmcfletch: don't make those changes until we get final word on whether they're necessary. If they are truly necessary then the fallback behaviour you're describing shouldn't be implemented as we'd be allowing users to run less-secure systems.

@hmcfletch

cool, i'll hold off until i hear otherwise on this thread.

@NZKoz
Owner

However I certainly agree with you @thaidn that we shouldn't permit people from using the EncryptedCookie without using the HMACs as well, that should be deprecated in MessageEncryptor and not exposed at all in Cookies

@thaidn

@NZKoz I'm from the google security team. I'm one of the researchers that discovered possible crypto attacks in Ruby on Rails (and many other frameworks), so I guess I'm qualified to comment here.

What I meant in the second issue is that we don't want somebody with a key recovery attack on HMAC to be able to decrypt our data as well.

And for the padding oracle issue: we want to prevent the re-use of config.secret_token in unauthenticated encryptions elsewhere in plugins, applications or Rails itself from undermining the authenticated encryption that we use in CookieStore. If we use the same key everywhere, then developers might think it's okay to re-use the key for other purposes as well, and then all it takes to decrypt our cookies is only one mistake. This is exactly what happened in ASP.NET, and I strongly suggest we should not make the same mistake in Rails.

You might think that all of the issues I raised are too conservative, but this is crypto and experience tells us that we should be conservative as much as possible when it comes to this area.

@NZKoz
Owner

@thaidn, HA! apologies for the miscommunication then :)

So in your ticket you mentioned using key derivation functions like pbkdf2 from the secret token for the encryption key and the hmac secret, that sounds like a great option. I just see two potential issues:

1) The existing signed-only cookies will have to continue to use the raw value of the secret to ensure backwards compatibility, and that means that the application will, to some extent, continue to be using the raw key in hmacs. Doesn't that leave us in the same situation we're in now?

2) We'll need to add a new class to replace MessageEncryptor which does all this transparently and deprecate the old one, this seems fine to me. The existing class exposes raw cbc encryption which people may already be using to shoot themselves in the foot.

So to summarize the suggested approach:

1) New Message...Something class, which is initialized with a secret, and uses pbkdf2 to generate 'key' and 'secret' from that value in a deterministic fashion
2) Encrypted cookies should use that class internally

By replacing MessageEncryptor with another class entirely, we can avoid any backwards compatibility issues and also ensure that people aren't inadvertently re-using the secret as a key without hmacs.

@thaidn

@NZKoz

1) As long as we use a different keys for new encryption/HMAC, then we'll be fine.

2) Good idea. Please CC me on any patch so that I can help review.

@NZKoz
Owner

@thaidn: So you'd consider removing/deprecating the existing 'raw keys' encryptor as sufficient for us to say we're using different keys?

Specifically an upgraded app will be using:

  • secret - to sign cookies
  • pbkdf2(secret, 'some salt') - to encrypt values
  • pbkdf2(secret, 'some other salt') - to sign those encrypted values
@thaidn

@NZKoz yes, exactly. have you come up with anything yet so that I can review?

cheers,

thai.

@hmcfletch

My question is where would these other salts come from? That and what would the name of this new class be? I suck at names.

@NZKoz
Owner

@hmcfletch I'll add a class for deriving keys, takes a secret then has a method like derive_key(salt), once I've done that I'll let you know

@thaidn: I'll have something next week some time. I'm assuming that we can safely use something like "encryption key" and "hmac secret" as default values for the pbkdf2 salt and let the user override them if they want to?

@thaidn
@meder

friendly ping

@steveklabnik
Collaborator

So, now that #6952 has been merged, is it time for this patch? It's way out of date...

@spastorino
Owner

I've already implemented this closing ...

@spastorino spastorino closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Dec 5, 2011
  1. @hmcfletch
  2. @hmcfletch

    EncrytedCookieJar

    hmcfletch authored
    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.
Commits on Feb 14, 2012
  1. @hmcfletch

    EncryptedCookieJar

    hmcfletch authored
    encrypted cookie data, refractor encrypted and signed cookies jars to
    base off of a secret cookie jar to consolidate secret token validation
  2. @hmcfletch

    EncryptedCookieJar tests

    hmcfletch authored
    mirroring all SignedCookieJar tests
  3. @hmcfletch

    Merge branch 'master' of github.com:hmcfletch/rails

    hmcfletch authored
    Conflicts:
    	actionpack/lib/action_dispatch/middleware/cookies.rb
    	actionpack/test/dispatch/cookies_test.rb
  4. @hmcfletch

    comment typo

    hmcfletch authored
  5. @hmcfletch

    update the CHNAGELOG

    hmcfletch authored
  6. @hmcfletch

    typo

    hmcfletch authored
This page is out of date. Refresh to see the latest.
View
8 actionpack/CHANGELOG.md
@@ -1,5 +1,13 @@
## Rails 4.0.0 (unreleased) ##
+* Add an encrypted cookie jar. It can be used like the signed cookies.
+ Example:
+
+ cookies.encrypted[:buried_treasure] = "X marks the spot"
+ cookies.permanent.encrypted[:encrypted_permanent] = "X marks the spot... forever"
+
+ *Les Fletcher*
+
* Add `date_field` and `date_field_tag` helpers which render an `input[type="date"]` tag *Olek Janiszewski*
* Adds `image_url`, `javascript_url`, `stylesheet_url`, `audio_url`, `video_url`, and `font_url`
View
104 actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -237,6 +237,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.encrypted[: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) }
@@ -273,22 +298,51 @@ def []=(key, options)
@parent_jar[key] = options
end
- def signed
- @signed ||= SignedCookieJar.new(self, @secret)
- end
-
def method_missing(method, *arguments, &block)
@parent_jar.send(method, *arguments, &block)
end
end
- class SignedCookieJar < CookieJar #:nodoc:
+ # Base class for the signed and encrypted cookie jar.
+ 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
@@ -311,30 +365,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
View
69 actionpack/test/dispatch/cookies_test.rb
@@ -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) }
@@ -272,17 +288,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"
@@ -302,6 +334,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
@@ -312,6 +351,16 @@ def test_raises_argument_error_if_missing_secret
@request.env["action_dispatch.secret_token"] = ""
get :set_signed_cookie
}
+
+ assert_raise(ArgumentError, nil.inspect) {
+ @request.env["action_dispatch.secret_token"] = nil
+ get :set_encrypted_cookie
+ }
+
+ assert_raise(ArgumentError, ''.inspect) {
+ @request.env["action_dispatch.secret_token"] = ""
+ get :set_encrypted_cookie
+ }
end
def test_raises_argument_error_if_secret_is_probably_insecure
@@ -329,6 +378,21 @@ def test_raises_argument_error_if_secret_is_probably_insecure
@request.env["action_dispatch.secret_token"] = "12345678901234567890123456789"
get :set_signed_cookie
}
+
+ assert_raise(ArgumentError, "password".inspect) {
+ @request.env["action_dispatch.secret_token"] = "password"
+ get :set_encrypted_cookie
+ }
+
+ assert_raise(ArgumentError, "secret".inspect) {
+ @request.env["action_dispatch.secret_token"] = "secret"
+ get :set_encrypted_cookie
+ }
+
+ assert_raise(ArgumentError, "12345678901234567890123456789".inspect) {
+ @request.env["action_dispatch.secret_token"] = "12345678901234567890123456789"
+ get :set_encrypted_cookie
+ }
end
def test_cookie_with_all_domain_option
@@ -463,8 +527,6 @@ def test_cookies_hash_is_indifferent_access
assert_equal "dhh", cookies['user_name']
end
-
-
def test_setting_request_cookies_is_indifferent_access
cookies.clear
cookies[:user_name] = "andrew"
@@ -575,4 +637,5 @@ def assert_not_cookie_header(expected)
assert_not_equal expected.split("\n"), header
end
end
-end
+
+end
Something went wrong with that request. Please try again.