Search code examples
ruby-on-railsencryptioncookiesruby-on-rails-5session-cookies

How to decrypt a Rails 5 session cookie manually?


I have access to

  • config.action_dispatch.encrypted_cookie_salt
  • config.action_dispatch.encrypted_signed_cookie_salt
  • secrets.secret_key_base
  • the full cookie string (including --)

I see ways to do this in Rails 4 (Rails 4: How to decrypt rails 4 session cookie (Given the session key and secret)), but these don't seem to work in Rails 5.


Solution

  • I have had the same problem the other day and figured out that the generated secret was 64 bytes long (on my mac), but Rails ensures that the key is 32 bytes long (source).

    This has worked for me:

    require 'cgi'
    require 'json'
    require 'active_support'
    
    def verify_and_decrypt_session_cookie(cookie, secret_key_base)
    
      cookie = CGI::unescape(cookie)
      salt         = 'encrypted cookie'
      signed_salt  = 'signed encrypted cookie'
      key_generator = ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000)
      secret = key_generator.generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len]
      sign_secret = key_generator.generate_key(signed_salt)
      encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON)
    
      encryptor.decrypt_and_verify(cookie)
    
    end
    

    Or without ActiveSupport:

    require 'openssl'
    require 'base64'
    require 'cgi'
    require 'json'
    
    def verify_and_decrypt_session_cookie(cookie, secret_key_base)
      cookie = CGI.unescape(cookie)
    
      #################
      # generate keys #
      #################
      encrypted_cookie_salt = 'encrypted cookie' # default: Rails.application.config.action_dispatch.encrypted_cookie_salt
      encrypted_signed_cookie_salt = 'signed encrypted cookie' # default: Rails.application.config.action_dispatch.encrypted_signed_cookie_salt
      iterations = 1000
      key_size = 64
      secret = OpenSSL::PKCS5.pbkdf2_hmac_sha1(secret_key_base, encrypted_cookie_salt, iterations, key_size)[0, OpenSSL::Cipher.new('aes-256-cbc').key_len]
      sign_secret = OpenSSL::PKCS5.pbkdf2_hmac_sha1(secret_key_base, encrypted_signed_cookie_salt, iterations, key_size)
    
      ##########
      # Verify #
      ##########
      data, digest = cookie.split('--')
      raise 'invalid message' unless digest == OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, sign_secret, data)
      # you better use secure compare instead of `==` to prevent time based attact,
      # ref: ActiveSupport::SecurityUtils.secure_compare
    
      ###########
      # Decrypt #
      ###########
      encrypted_message = Base64.strict_decode64(data)
      encrypted_data, iv = encrypted_message.split('--').map{|v| Base64.strict_decode64(v) }
      cipher = OpenSSL::Cipher.new('aes-256-cbc')
      cipher.decrypt
      cipher.key = secret
      cipher.iv  = iv
      decrypted_data = cipher.update(encrypted_data)
      decrypted_data << cipher.final
    
      JSON.load(decrypted_data)
    end
    

    Feel free to comment on the gist: https://gist.github.com/mbyczkowski/34fb691b4d7a100c32148705f244d028