mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
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
This commit is contained in:
parent
e272000c80
commit
38c40dbbc1
4 changed files with 69 additions and 3 deletions
|
@ -1,5 +1,6 @@
|
||||||
require 'active_support/core_ext/hash/keys'
|
require 'active_support/core_ext/hash/keys'
|
||||||
require 'active_support/core_ext/module/attribute_accessors'
|
require 'active_support/core_ext/module/attribute_accessors'
|
||||||
|
require 'active_support/message_verifier'
|
||||||
|
|
||||||
module ActionDispatch
|
module ActionDispatch
|
||||||
class Request < Rack::Request
|
class Request < Rack::Request
|
||||||
|
@ -242,6 +243,22 @@ module ActionDispatch
|
||||||
@signed ||= SignedCookieJar.new(self, @key_generator)
|
@signed ||= SignedCookieJar.new(self, @key_generator)
|
||||||
end
|
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)
|
def write(headers)
|
||||||
@set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) if write_cookie?(v) }
|
@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) }
|
@delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) }
|
||||||
|
@ -341,6 +358,37 @@ module ActionDispatch
|
||||||
end
|
end
|
||||||
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)
|
def initialize(app)
|
||||||
@app = app
|
@app = app
|
||||||
end
|
end
|
||||||
|
|
|
@ -67,6 +67,11 @@ class CookiesTest < ActionController::TestCase
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_encrypted_cookie
|
||||||
|
cookies.encrypted[:foo] = 'bar'
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
def raise_data_overflow
|
def raise_data_overflow
|
||||||
cookies.signed[:foo] = 'bye!' * 1024
|
cookies.signed[:foo] = 'bye!' * 1024
|
||||||
head :ok
|
head :ok
|
||||||
|
@ -298,6 +303,16 @@ class CookiesTest < ActionController::TestCase
|
||||||
assert_equal 45, @controller.send(:cookies).signed[:user_id]
|
assert_equal 45, @controller.send(:cookies).signed[:user_id]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_encrypted_cookie
|
||||||
|
get :set_encrypted_cookie
|
||||||
|
cookies = @controller.send :cookies
|
||||||
|
assert_not_equal 'bar', cookies[:foo]
|
||||||
|
assert_raises TypeError do
|
||||||
|
cookies.signed[:foo]
|
||||||
|
end
|
||||||
|
assert_equal 'bar', cookies.encrypted[:foo]
|
||||||
|
end
|
||||||
|
|
||||||
def test_accessing_nonexistant_signed_cookie_should_not_raise_an_invalid_signature
|
def test_accessing_nonexistant_signed_cookie_should_not_raise_an_invalid_signature
|
||||||
get :set_signed_cookie
|
get :set_signed_cookie
|
||||||
assert_nil @controller.send(:cookies).signed[:non_existant_attribute]
|
assert_nil @controller.send(:cookies).signed[:non_existant_attribute]
|
||||||
|
|
|
@ -39,10 +39,13 @@ module ActiveSupport
|
||||||
# * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by
|
# * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by
|
||||||
# <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-cbc'.
|
# <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-cbc'.
|
||||||
# * <tt>:serializer</tt> - Object serializer to use. Default is +Marshal+.
|
# * <tt>:serializer</tt> - Object serializer to use. Default is +Marshal+.
|
||||||
def initialize(secret, options = {})
|
def initialize(secret, *signature_key_or_options)
|
||||||
|
options = signature_key_or_options.extract_options!
|
||||||
|
sign_secret = signature_key_or_options.first
|
||||||
@secret = secret
|
@secret = secret
|
||||||
|
@sign_secret = sign_secret
|
||||||
@cipher = options[:cipher] || 'aes-256-cbc'
|
@cipher = options[:cipher] || 'aes-256-cbc'
|
||||||
@verifier = MessageVerifier.new(@secret, :serializer => NullSerializer)
|
@verifier = MessageVerifier.new(@sign_secret || @secret, :serializer => NullSerializer)
|
||||||
@serializer = options[:serializer] || Marshal
|
@serializer = options[:serializer] || Marshal
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,7 @@ class MessageEncryptorTest < ActiveSupport::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_alternative_serialization_method
|
def test_alternative_serialization_method
|
||||||
encryptor = ActiveSupport::MessageEncryptor.new(SecureRandom.hex(64), :serializer => JSONSerializer.new)
|
encryptor = ActiveSupport::MessageEncryptor.new(SecureRandom.hex(64), SecureRandom.hex(64), :serializer => JSONSerializer.new)
|
||||||
message = encryptor.encrypt_and_sign({ :foo => 123, 'bar' => Time.utc(2010) })
|
message = encryptor.encrypt_and_sign({ :foo => 123, 'bar' => Time.utc(2010) })
|
||||||
assert_equal encryptor.decrypt_and_verify(message), { "foo" => 123, "bar" => "2010-01-01T00:00:00Z" }
|
assert_equal encryptor.decrypt_and_verify(message), { "foo" => 123, "bar" => "2010-01-01T00:00:00Z" }
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue