class Rack::Session::EncryptedCookie
Constants
- NOT_FOUND
Public Class Methods
@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
# File lib/rack/session/encryptedcookie.rb, line 52 def call(env) dup.call!(env) end
# 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
# 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
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
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 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
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