Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 938
Eli Rose Avatar asked Jan 04 '17 22:01

Eli Rose


2 Answers

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

like image 154
matb Avatar answered Sep 18 '22 13:09

matb


Here's a Rails 5.2 variant of @matb's answer, which handles the revised configuration, encryption and serialization:

require 'cgi'
require 'active_support'

def verify_and_decrypt_session_cookie(cookie, secret_key_base = Rails.application.secret_key_base)
  cookie = CGI::unescape(cookie)
  salt   = 'authenticated encrypted cookie'
  encrypted_cookie_cipher = 'aes-256-gcm'
  serializer = ActiveSupport::MessageEncryptor::NullSerializer

  key_generator = ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000)
  key_len = ActiveSupport::MessageEncryptor.key_len(encrypted_cookie_cipher)
  secret = key_generator.generate_key(salt, key_len)
  encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: encrypted_cookie_cipher, serializer: serializer)

  encryptor.decrypt_and_verify(cookie)
end

Also up at https://gist.github.com/inopinatus/e523f36b468f94cf6d34410b73fef15e.

like image 21
inopinatus Avatar answered Sep 20 '22 13:09

inopinatus