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

coder[R]

Public Class Methods

new(app, options = {}) click to toggle source
Calls superclass method
# 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

delete_session(_req, _session_id, options) click to toggle source
# 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
digest_match?(data, digest) click to toggle source
# 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
extract_session_id(request) click to toggle source
# File lib/rack/protection/encrypted_cookie.rb, line 200
def extract_session_id(request)
  unpacked_cookie_data(request)['session_id']
end
find_session(req, _sid) click to toggle source
# 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
generate_hmac(data) click to toggle source
# File lib/rack/protection/encrypted_cookie.rb, line 263
def generate_hmac(data)
  OpenSSL::HMAC.hexdigest(@legacy_hmac.new, @legacy_hmac_secret, data)
end
persistent_session_id!(data, sid = nil) click to toggle source
# 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
secure?(options) click to toggle source
# File lib/rack/protection/encrypted_cookie.rb, line 267
def secure?(options)
  @secrets.size >= 1 ||
    (options[:coder] && options[:let_coder_handle_secure_encoding])
end
write_session(req, session_id, session, _options) click to toggle source
# 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