class Rack::Protection::EncryptedCookie
})
}.new
def decode(str); str.reverse; end
def encode(str); str.reverse; end
:coder => Class.new {
Rack::Protection::EncryptedCookie.new(application, {
Example of a cookie with custom encoding:
})
:coder => Rack::Protection::EncryptedCookie::Identity.new
Rack::Protection::EncryptedCookie.new(application, {
Example of a cookie with no encoding:
})
legacy_hmac: OpenSSL::Digest::SHA256
# legacy_hmac will default to OpenSSL::Digest::SHA1
legacy_hmac_coder: Rack::Protection::EncryptedCookie::Identity.new,
# legacy_hmac_coder will default to Rack::Protection::EncryptedCookie::Base64::Marshal
legacy_hmac_secret: ‘legacy secret’,
# The secret used for legacy HMAC cookies
Rack::Protection:EncryptedCookie.new(application, {
Example using legacy HMAC options
All parameters are optional.
:old_secret => ‘old_secret’
:secret => ‘change_me’,
:expire_after => 2592000,
:path => ‘/’,
:domain => ‘foo.com’,
:key => ‘rack.session’,
use Rack::Protection::EncryptedCookie,
Example:
coder was used for legacy session cookies.
There is also a legacy_hmac_coder option which can be set if a non-default
sessions to the new encryption scheme.
A legacy_hmac_secret is also accepted and is used to upgrade existing
The old_secret key is also accepted and allows graceful secret rotation.
When the secret key is set, cookie data is checked for data integrity.
Both methods must take a string and return a string.
session data is configurable and must respond to encode
and decode
.
data set to :key (default: rack.session). The object that encodes the
By default, the session is a Ruby Hash stored as base64 encoded marshalled
Rack::Protection::EncryptedCookie provides simple cookie based session management.
def delete_session(_req, _session_id, options)
def delete_session(_req, _session_id, options) # Nothing to do here, data is in the client generate_sid unless options[:drop] end
def digest_match?(data, digest)
def digest_match?(data, digest) return false unless data && digest Rack::Utils.secure_compare(digest, generate_hmac(data)) end
def extract_session_id(request)
def extract_session_id(request) unpacked_cookie_data(request)['session_id'] end
def find_session(req, _sid)
def find_session(req, _sid) data = unpacked_cookie_data(req) data = persistent_session_id!(data) [data['session_id'], data] end
def generate_hmac(data)
def generate_hmac(data) OpenSSL::HMAC.hexdigest(@legacy_hmac.new, @legacy_hmac_secret, data) end
def initialize(app, options = {})
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
def persistent_session_id!(data, sid = nil)
def persistent_session_id!(data, sid = nil) data ||= {} data['session_id'] ||= sid || generate_sid data end
def secure?(options)
def secure?(options) @secrets.size >= 1 || (options[:coder] && options[:let_coder_handle_secure_encoding]) end
def unpacked_cookie_data(request)
def unpacked_cookie_data(request) request.fetch_header(RACK_SESSION_UNPACKED_COOKIE_DATA) do |k| session_data = cookie_data = request.cookies[@key] # Try to decrypt with the first secret, if that returns nil, try # with old_secret unless @secrets.empty? session_data = Rack::Protection::Encryptor.decrypt_message(cookie_data, @secrets.first) session_data ||= Rack::Protection::Encryptor.decrypt_message(cookie_data, @secrets[1]) if @secrets.size > 1 end # If session_data is still nil, are there is a legacy HMAC # configured, try verify and parse the cookie that way if !session_data && @legacy_hmac digest = cookie_data.slice!(-@legacy_hmac_length..-1) cookie_data.slice!(-2..-1) # remove double dash session_data = cookie_data if digest_match?(cookie_data, digest) # Decode using legacy HMAC decoder request.set_header(k, @legacy_hmac_coder.decode(session_data) || {}) else request.set_header(k, coder.decode(session_data) || {}) end end end
def write_session(req, session_id, session, _options)
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