class Rack::Session::EncryptedCookie

Constants

NOT_FOUND

Public Class Methods

new(app, opts={}) click to toggle source

@param [Hash] opts Session options @option opts [String] :cookie_name Cookie name @option opts [String] :domain Domain for the cookie @option opts [Boolean] :http_only HttpOnly for the cookie @option opts [Integer] :expires Cookie expiry (in seconds) @option opts [String] :cipher OpenSSL cipher to use @option opts [String] :salt Salt for the IV @optons opts [Integer] :rounds Number of salting rounds @option opts [String] :key Encryption key for the data @option opts [Integer] :tag_len Tag length (for GCM/CCM ciphers) @option opts [Boolean] :clear_cookies Clear response cookies

If :domain is nil, the Host header from the request will be used to determine the domain sent for the cookie.

# File lib/rack/session/encryptedcookie.rb, line 34
def initialize(app, opts={})
  @app  = app
  @hash = {}
  @host = nil
  @opts = {
    cookie_name:   'rack.session',
    domain:        nil,
    http_only:     false,
    expires:       (15 * 60),
    cipher:        'aes-256-cbc',
    salt:          '3@bG>B@J5vy-FeXJ',
    rounds:        2000,
    key:           'r`*BqnG:c^;AL{k97=KYN!#',
    tag_len:       16,
    clear_cookies: false
  }.merge(opts)
end

Public Instance Methods

call(env) click to toggle source
# File lib/rack/session/encryptedcookie.rb, line 52
def call(env)
  dup.call!(env)
end
call!(env) click to toggle source
# File lib/rack/session/encryptedcookie.rb, line 56
def call!(env)
  @cb = env['async.callback']
  env['async.callback'] = method(:save_session) if @cb
  env['rack.session']   = self
  load_session(env)

  if @app
    @cb ? @app.call(env) : save_session(@app.call(env))
  else
    @cb ? @cb.call(NOT_FOUND) : NOT_FOUND
  end
end
method_missing(method, *args, &block) click to toggle source
# File lib/rack/session/encryptedcookie.rb, line 69
def method_missing(method, *args, &block)
  if @hash.respond_to?(method)
    @hash.send(method, *args, &block)
  else
    raise ArgumentError.new("Method `#{method}` doesn't exist.")
  end
end

Private Instance Methods

cipher(mode, str) click to toggle source

Handle en/de-cryption @param [Symbol] :mode :encrypt or :decrypt @param [String] :str Data to en/de-crypt @return [String, nil] Encrypted data

# File lib/rack/session/encryptedcookie.rb, line 139
def cipher(mode, str)
  return nil if @opts[:key].nil? || str.nil?
  begin
    cipher = OpenSSL::Cipher.new(@opts[:cipher])
    cipher.send(mode)
  rescue
    return cipher_failed($!.message)
  end

  # Set the key and IV
  if @opts[:salt].nil?
    cipher.key = @opts[:key]
  else
    cipher.key = OpenSSL::PKCS5.pbkdf2_hmac_sha1(
      @opts[:key], @opts[:salt],
      @opts[:rounds], cipher.key_len
    )
  end

  # Setup the auth data for GCM/CCM
  if cipher.name.match(%r{/[CG]CM$/i})
    cipher.auth_data = ''
    cipher.tag_len   = @opts[:tag_len]
  end

  iv   = cipher.random_iv
  xstr = str

  if mode == :decrypt
    str = str.unpack('m').first

    # Set the tag for GCM/CCM
    if cipher.name.match(%r{/[CG]CM$/i})
      cipher.auth_tag = str.slice!(0, cipher.tag_len).unpack('C*')
    end

    # Extract the iv
    iv_len   = iv.length
    str_b,iv = Array[str[0 ... iv_len << 1].unpack('C*')].transpose.
               partition.with_index { |x,i| (i&1).zero? }
    iv.flatten! ; str_b.flatten!

    # Set the IV and buffer
    iv   = iv.pack('C*')
    xstr = str_b.pack('C*') + str[iv_len << 1 ... str.length]
  end

  # Call the cipher
  r         = nil
  cipher.iv = iv
  begin
    r = cipher.update(xstr) + cipher.final
    if mode == :encrypt
      d = r.bytes.zip(iv.bytes).flatten.compact
      if cipher.name.match(%r{/[CG]CM$/i})
        d = cipher.tag.bytes + d.bytes
      end
      r = [d.pack('C*')].pack('m').chomp
    end
  rescue OpenSSL::Cipher::CipherError
    return cipher_failed($!.message)
  end
  r
end
cipher_failed(e='<no message>') click to toggle source

Warn the user that en/de-cryption failed @param [String] e Exception message @return nil

# File lib/rack/session/encryptedcookie.rb, line 128
    def cipher_failed(e='<no message>')
        warn (<<-XXX.gsub(/^\s*/, ''))
        SECURITY WARNING: Session cipher failed: #{e}
        XXX
        return nil
    end
load_session(env) click to toggle source

Load the sesssion data from the cookie @return [Hash, nil] Session data

# File lib/rack/session/encryptedcookie.rb, line 81
def load_session(env)
  @hash.clear unless @hash.empty?
  r = Rack::Request.new(env)
  @host = r.host if @opts[:domain].nil?
  cookie = r.cookies[@opts[:cookie_name]]
  return if cookie.nil?
  @hash = Marshal.load(cipher(:decrypt, cookie)) rescue {}
end
save_session(r) click to toggle source

Add our cookie to the response

@param [Array] r Upstream Rack response @return [Array] Rack response + our cookie

# File lib/rack/session/encryptedcookie.rb, line 94
def save_session(r)
  return r if !r.is_a?(Array) || (r.is_a?(Array) && r[0] == -1)

  unless @hash.empty?
    data = cipher(:encrypt, Marshal.dump(@hash)) rescue nil
    c = {
      value: data,
      path:  '/',
    }

    if !@opts[:domain].nil?
      c[:domain] = @opts[:domain]
    elsif @host&.match(%r{^[a-zA-Z]})
      c[:domain] = @host
    end

    c[:httponly] = @opts[:http_only] === true
    if @opts.has_key?(:expires)
      c[:expires] = Time.at(Time.now + @opts[:expires])
    end

    r[1]['Set-Cookie'] = nil if @opts[:clear_cookies]
    r[1]['Set-Cookie'] = Rack::Utils.add_cookie_to_header(
      r[1]['Set-Cookie'], @opts[:cookie_name], c
    ) unless data.nil?
  end

  @cb.call(r) if @cb
  r
end