module Roda::RodaPlugins::Sessions::RequestMethods
def _deserialize_rack_session(data)
serialized session using the default Rack::Session::Cookie
Interpret given cookie data as a Rack::Session::Cookie
def _deserialize_rack_session(data) opts = roda_class.opts[:sessions] data, digest = data.split("--", 2) unless digest return _session_serialization_error("Not decoding Rack::Session::Cookie session: invalid format") end unless Rack::Utils.secure_compare(digest, OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, opts[:upgrade_from_rack_session_cookie_secret], data)) return _session_serialization_error("Not decoding Rack::Session::Cookie session: HMAC invalid") end begin session = Marshal.load(data.unpack('m').first) rescue return _session_serialization_error("Error decoding Rack::Session::Cookie session: not base64 encoded marshal dump") end # Mark rack session cookie for deletion on success env[SESSION_DELETE_RACK_COOKIE] = true # Delete the session id before serializing it. Starting in rack 2.0.8, # this is an object and not just a string, and calling to_s on it raises # a RuntimeError. session.delete("session_id") # Convert the rack session by roundtripping it through # the parser and serializer, so that you would get the # same result as you would if the session was handled # by this plugin. env[SESSION_SERIALIZED] = data = opts[:serializer].call(session) env[SESSION_CREATED_AT] = Time.now.to_i opts[:parser].call(data) end
def _deserialize_session(data)
def _deserialize_session(data) opts = roda_class.opts[:sessions] begin data = Base64_.urlsafe_decode64(data) rescue ArgumentError return _session_serialization_error("Unable to decode session: invalid base64") end case version = data.getbyte(0) when 1 per_cookie_secret = true # minimum length (1+32+16+12+32) (version+random_data+cipher_iv+minimum session+hmac) # 1 : version # 32 : random_data (if per_cookie_cipher_secret) # 16 : cipher_iv # 12 : minimum_session # 2 : bitmap for gzip + padding info # 4 : creation time # 4 : update time # 2 : data # 32 : HMAC-SHA-256 min_data_length = 93 when 0 per_cookie_secret = false # minimum length (1+16+12+32) (version+cipher_iv+minimum session+hmac) min_data_length = 61 when nil return _session_serialization_error("Unable to decode session: no data") else return _session_serialization_error("Unable to decode session: version marker unsupported") end length = data.bytesize if data.length < min_data_length return _session_serialization_error("Unable to decode session: data too short") end encrypted_data = data.slice!(0, length-32) unless Rack::Utils.secure_compare(data, OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, opts[:hmac_secret], encrypted_data+opts[:key])) if opts[:old_hmac_secret] && Rack::Utils.secure_compare(data, OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, opts[:old_hmac_secret], encrypted_data+opts[:key])) use_old_cipher_secret = true else return _session_serialization_error("Not decoding session: HMAC invalid") end end # Remove version encrypted_data.slice!(0) cipher_secret = opts[use_old_cipher_secret ? :old_cipher_secret : :cipher_secret] if per_cookie_secret cipher_secret = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, cipher_secret, encrypted_data.slice!(0, 32)) end cipher_iv = encrypted_data.slice!(0, 16) cipher = OpenSSL::Cipher.new("aes-256-ctr") # Not rescuing cipher errors. If there is an error in the decryption, that's # either a bug in the plugin that needs to be fixed, or an attacker is already # able to forge a valid HMAC, in which case the error should be raised to # alert the application owner about the problem. cipher.decrypt cipher.key = cipher_secret cipher.iv = cipher_iv data = cipher.update(encrypted_data) << cipher.final bitmap, created_at, updated_at = data.unpack('vVV') padding_bytes = bitmap & PADDING_MASK now = Time.now.to_i if (max = opts[:max_seconds]) && now > created_at + max return _session_serialization_error("Not returning session: maximum session time expired") end if (max = opts[:max_idle_seconds]) && now > updated_at + max return _session_serialization_error("Not returning session: maximum session idle time expired") end data = data.slice(10+padding_bytes, data.bytesize) if bitmap & DEFLATE_BIT > 0 data = Zlib::Inflate.inflate(data) end env = @env env[SESSION_CREATED_AT] = created_at env[SESSION_UPDATED_AT] = updated_at env[SESSION_SERIALIZED] = data env[SESSION_VERSION_NUM] = version opts[:parser].call(data) end
def _load_session
Load the session by looking for the appropriate cookie, or falling
def _load_session opts = roda_class.opts[:sessions] cs = cookies if data = cs[opts[:key]] _deserialize_session(data) elsif (key = opts[:upgrade_from_rack_session_cookie_key]) && (data = cs[key]) _deserialize_rack_session(data) end || {} end
def _serialize_session(session)
def _serialize_session(session) opts = roda_class.opts[:sessions] env = @env now = Time.now.to_i json_data = opts[:serializer].call(session).force_encoding('BINARY') if (serialized_session = env[SESSION_SERIALIZED]) && (opts[:session_version_num] == env[SESSION_VERSION_NUM]) && (updated_at = env[SESSION_UPDATED_AT]) && (now - updated_at < opts[:skip_within]) && (serialized_session == json_data) return end bitmap = 0 json_length = json_data.bytesize gzip_over = opts[:gzip_over] if gzip_over && json_length > gzip_over json_data = Zlib.deflate(json_data) json_length = json_data.bytesize bitmap |= DEFLATE_BIT end # When calculating padding bytes to use, include 10 bytes for bitmap and # session create/update times, so total size of encrypted data is a # multiple of pad_size. if (pad_size = opts[:pad_size]) && (padding_bytes = (json_length+10) % pad_size) != 0 padding_bytes = pad_size - padding_bytes bitmap |= padding_bytes padding_data = SecureRandom.random_bytes(padding_bytes) end session_create_time = env[SESSION_CREATED_AT] serialized_data = [bitmap, session_create_time||now, now].pack('vVV') serialized_data << padding_data if padding_data serialized_data << json_data cipher_secret = opts[:cipher_secret] if opts[:per_cookie_cipher_secret] version = "\1" per_cookie_secret_base = SecureRandom.random_bytes(32) cipher_secret = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, cipher_secret, per_cookie_secret_base) else version = "\0" end cipher = OpenSSL::Cipher.new("aes-256-ctr") cipher.encrypt cipher.key = cipher_secret cipher_iv = cipher.random_iv encrypted_data = cipher.update(serialized_data) << cipher.final data = String.new data << version data << per_cookie_secret_base if per_cookie_secret_base data << cipher_iv data << encrypted_data data << OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, opts[:hmac_secret], data+opts[:key]) data = Base64_.urlsafe_encode64(data) if data.bytesize >= 4096 raise CookieTooLarge, "attempted to create cookie larger than 4096 bytes" end data end
def _session_serialization_error(msg)
This is used for errors that shouldn't be raised as exceptions,
If 'rack.errors' is set, write the error message to it.
def _session_serialization_error(msg) return unless error_stream = @env['rack.errors'] error_stream.puts(msg) nil end
def persist_session(headers, session)
Rack::Session::Cookie, mark the related cookie for expiration so it isn't
Persist the session data as a cookie. If transparently upgrading from
def persist_session(headers, session) opts = roda_class.opts[:sessions] if session.empty? if env[SESSION_SERIALIZED] # If session was submitted and is now empty, remove the cookie Rack::Utils.delete_cookie_header!(headers, opts[:key], opts[:remove_cookie_options]) # else # If no session was submitted, and the session is empty # then there is no need to do anything end elsif cookie_value = _serialize_session(session) cookie = Hash[opts[:cookie_options]] cookie[:value] = cookie_value cookie[:secure] = true if !cookie.has_key?(:secure) && ssl? Rack::Utils.set_cookie_header!(headers, opts[:key], cookie) end if env[SESSION_DELETE_RACK_COOKIE] Rack::Utils.delete_cookie_header!(headers, opts[:upgrade_from_rack_session_cookie_key], opts[:upgrade_from_rack_session_cookie_options]) end nil end
def session
this method stores the session in 'rack.session' in the request environment,
For maximum compatibility with other software that uses rack sessions,
trying to access the session directly through the request environment.
plugin, you must call this method to get the session, instead of
Load the session information from the cookie. With the sessions
def session @env['rack.session'] ||= _load_session end
def session_created_at
def session_created_at session Time.at(@env[SESSION_CREATED_AT]) if @env[SESSION_SERIALIZED] end
def session_updated_at
def session_updated_at session Time.at(@env[SESSION_UPDATED_AT]) if @env[SESSION_SERIALIZED] end