class Rack::Protection::EncryptedCookie
Rack::Protection::EncryptedCookie
provides simple cookie based session management. By default, the session is a Ruby Hash stored as base64 encoded marshalled data set to :key (default: rack.session). The object that encodes the session data is configurable and must respond to encode
and decode
. Both methods must take a string and return a string.
When the secret key is set, cookie data is checked for data integrity. The old_secret key is also accepted and allows graceful secret rotation. A legacy_hmac_secret is also accepted and is used to upgrade existing sessions to the new encryption scheme.
There is also a legacy_hmac_coder option which can be set if a non-default coder was used for legacy session cookies.
Example:
use Rack::Protection::EncryptedCookie, :key => 'rack.session', :domain => 'foo.com', :path => '/', :expire_after => 2592000, :secret => 'change_me', :old_secret => 'old_secret' All parameters are optional.
Example using legacy HMAC options
Rack::Protection:EncryptedCookie.new(application, { # The secret used for legacy HMAC cookies legacy_hmac_secret: 'legacy secret', # legacy_hmac_coder will default to Rack::Protection::EncryptedCookie::Base64::Marshal legacy_hmac_coder: Rack::Protection::EncryptedCookie::Identity.new, # legacy_hmac will default to OpenSSL::Digest::SHA1 legacy_hmac: OpenSSL::Digest::SHA256 })
Example of a cookie with no encoding:
Rack::Protection::EncryptedCookie.new(application, { :coder => Rack::Protection::EncryptedCookie::Identity.new })
Example of a cookie with custom encoding:
Rack::Protection::EncryptedCookie.new(application, { :coder => Class.new { def encode(str); str.reverse; end def decode(str); str.reverse; end }.new })
Attributes
Public Class Methods
# File lib/rack/protection/encrypted_cookie.rb, line 143 def initialize(app, options = {}) # Assume keys are hex strings and convert them to raw byte strings for # actual key material @secrets = options.values_at(:secret, :old_secret).compact.map do |secret| [secret].pack('H*') end warn <<-MSG unless secure?(options) SECURITY WARNING: No secret option provided to Rack::Protection::EncryptedCookie. This poses a security threat. It is strongly recommended that you provide a secret to prevent exploits that may be possible from crafted cookies. This will not be supported in future versions of Rack, and future versions will even invalidate your existing user cookies. Called from: #{caller[0]}. MSG warn <<-MSG if @secrets.first && @secrets.first.length < 32 SECURITY WARNING: Your secret is not long enough. It must be at least 32 bytes long and securely random. To generate such a key for use you can run the following command: ruby -rsecurerandom -e 'p SecureRandom.hex(32)' Called from: #{caller[0]}. MSG if options.key?(:legacy_hmac_secret) @legacy_hmac = options.fetch(:legacy_hmac, OpenSSL::Digest::SHA1) # Multiply the :digest_length: by 2 because this value is the length of # the digest in bytes but session digest strings are encoded as hex # strings @legacy_hmac_length = @legacy_hmac.new.digest_length * 2 @legacy_hmac_secret = options[:legacy_hmac_secret] @legacy_hmac_coder = (options[:legacy_hmac_coder] ||= Base64::Marshal.new) else @legacy_hmac = false end # If encryption is used we can just use a default Marshal encoder # without Base64 encoding the results. # # If no encryption is used, rely on the previous default (Base64::Marshal) @coder = (options[:coder] ||= (@secrets.any? ? Marshal.new : Base64::Marshal.new)) super(app, options.merge!(cookie_only: true)) end
Private Instance Methods
# File lib/rack/protection/encrypted_cookie.rb, line 252 def delete_session(_req, _session_id, options) # Nothing to do here, data is in the client generate_sid unless options[:drop] end
# File lib/rack/protection/encrypted_cookie.rb, line 257 def digest_match?(data, digest) return false unless data && digest Rack::Utils.secure_compare(digest, generate_hmac(data)) end
# File lib/rack/protection/encrypted_cookie.rb, line 200 def extract_session_id(request) unpacked_cookie_data(request)['session_id'] end
# File lib/rack/protection/encrypted_cookie.rb, line 194 def find_session(req, _sid) data = unpacked_cookie_data(req) data = persistent_session_id!(data) [data['session_id'], data] end
# File lib/rack/protection/encrypted_cookie.rb, line 263 def generate_hmac(data) OpenSSL::HMAC.hexdigest(@legacy_hmac.new, @legacy_hmac_secret, data) end
# File lib/rack/protection/encrypted_cookie.rb, line 230 def persistent_session_id!(data, sid = nil) data ||= {} data['session_id'] ||= sid || generate_sid data end
# File lib/rack/protection/encrypted_cookie.rb, line 267 def secure?(options) @secrets.size >= 1 || (options[:coder] && options[:let_coder_handle_secure_encoding]) end
# File lib/rack/protection/encrypted_cookie.rb, line 236 def write_session(req, session_id, session, _options) session = session.merge('session_id' => session_id) session_data = coder.encode(session) unless @secrets.empty? session_data = Rack::Protection::Encryptor.encrypt_message(session_data, @secrets.first) end if session_data.size > (4096 - @key.size) req.get_header(RACK_ERRORS).puts('Warning! Rack::Protection::EncryptedCookie data size exceeds 4K.') nil else session_data end end