Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save wildjcrt/6359713fa770d277927051fdeb30ebbf to your computer and use it in GitHub Desktop.
Save wildjcrt/6359713fa770d277927051fdeb30ebbf to your computer and use it in GitHub Desktop.
Decrypt Rails 6.0 beta session cookies
require 'cgi'
require 'active_support'
def verify_and_decrypt_session_cookie(cookie, secret_key_base = Rails.application.secret_key_base)
config = Rails.application.config
cookie = CGI::unescape(cookie)
salt = config.action_dispatch.authenticated_encrypted_cookie_salt
encrypted_cookie_cipher = config.action_dispatch.encrypted_cookie_cipher || 'aes-256-gcm'
# serializer = ActiveSupport::MessageEncryptor::NullSerializer # use this line if you don't know your serializer
serializer = ActionDispatch::Cookies::JsonSerializer
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)
session_key = config.session_options[:key].freeze
encryptor.decrypt_and_verify(cookie, purpose: "cookie.#{session_key}")
end
@lisbethw1130
Copy link

lisbethw1130 commented Dec 14, 2021

this helps a lot, thanks a lot 👍

@aizotov
Copy link

aizotov commented Dec 27, 2021

This is perfect 🙏 blessing be upon you

@LeKhoa
Copy link

LeKhoa commented Jan 12, 2022

I use this method but it shows below error when I call encryptor.decrypt_and_verify

*** ActiveSupport::MessageEncryptor::InvalidMessage Exception: ActiveSupport::MessageEncryptor::InvalidMessage

@theblang
Copy link

theblang commented Jan 27, 2022

@LeKhoa Ditto, did you ever figure it out?


Update: Ahh, I realized that I was calling the method with request.cookies['cookie_name'], which is an unescaped cookie value, when really the logic wants the escaped value (i.e. the one you can copy from browser devtools). See the line: cookie = CGI::unescape(cookie).

After realizing that, I still had an error (a different one) when trying to decrypt. It ended up being the fact that our old Rails app is using :marshal as the cookies_serializer config (see this doc), for which I needed to instead use the line: serializer = ActiveSupport::MessageEncryptor::NullSerializer.

Note that you'll also want to either JSON.parse or Marshal.restore (again, depending on your serializer) the value returned from decrypt_and_verify.

@Eric-Guo
Copy link

Eric-Guo commented May 6, 2022

I confirm above function verify_and_decrypt_session_cookie works perfect in Rails 6.1.5, but meet Rails 7 error as below.

irb(main):013:0> encryptor.decrypt_and_verify(cookie, purpose: "cookie.#{session_key}")
/var/www/matlib/shared/bundle/ruby/3.1.0/gems/activesupport-7.0.2.4/lib/active_support/message_encryptor.rb:208:in `rescue in _decrypt': ActiveSupport::MessageEncryptor::InvalidMessage (ActiveSupport::MessageEncryptor::InvalidMessage)
/var/www/matlib/shared/bundle/ruby/3.1.0/gems/activesupport-7.0.2.4/lib/active_support/message_encryptor.rb:203:in `final': OpenSSL::Cipher::CipherError

Rails.application.config.action_dispatch.cookies_serializer is :json and CGI::unescape(cookie) also called.

@Eric-Guo
Copy link

Eric-Guo commented May 6, 2022

For anyone search and reach here, Demystifying cookie security in Rails 6 works both Rails 6 and 7 and below code snippets is copy from the article and backup purpose in case URL is break in future.

cookie = "aDkxgmW4kaxoXBGnjxAaBY7D47WUOveFdeai5kk2hHlYVqDo7xtzZJup5euTdH5ja5iOt37MMS4SVXQT5RteaZjvpdlA%2FLQi7IYSPZLz--2A6LCUu%2F5AsLfSez--QD%2FwiA2t8QQrKk6rrROlPQ%3D%3D"
cookie = CGI.unescape(cookie)
data, iv, auth_tag = cookie.split("--").map do |v| 
  Base64.strict_decode64(v)
end
cipher = OpenSSL::Cipher.new("aes-256-gcm")

# Compute the encryption key
secret_key_base = Rails.application.secret_key_base
secret = OpenSSL::PKCS5.pbkdf2_hmac_sha1(secret_key_base, "authenticated encrypted cookie", 1000, cipher.key_len)

# Setup cipher for decryption and add inputs
cipher.decrypt
cipher.key = secret
cipher.iv  = iv
cipher.auth_tag = auth_tag
cipher.auth_data = ""

# Perform decryption
cookie_payload = cipher.update(data)
cookie_payload << cipher.final
cookie_payload = JSON.parse cookie_payload
# => {"_rails"=>{"message"=>"InRva2VuIg==", "exp"=>nil, "pur"=>"cookie.remember_token"}}

# Decode Base64 encoded stored data
decoded_stored_value = Base64.decode64 cookie_payload["_rails"]["message"]
stored_value = JSON.parse decoded_stored_value
# => "token"

@ShadowCrafter011
Copy link

ShadowCrafter011 commented Oct 14, 2022

@Eric-Guo I use Rails version 7.0.4 and when trying to decrypt a cookie I get this error: /usr/src/app/lib/utils/cookie_utils.rb:22:in 'final': OpenSSL::Cipher::CipherError any idea what I might be doing wrong?

@Duleja
Copy link

Duleja commented Oct 19, 2022

@Eric-Guo I use Rails version 7.0.4 and when trying to decrypt a cookie I get this error: /usr/src/app/lib/utils/cookie_utils.rb:22:in 'final': OpenSSL::Cipher::CipherError any idea what I might be doing wrong?

Working example for Rails 7.0.4 (using a session cookie from an example Rails 7.0.4 app):

cookie = "doYBgZ9M%2Fr%2FDwKflSXIop3LwI3w5yKLqf4essonyRfGZnnYH5Q%2FK%2F9tvo2ZaBlGpa%2FyPxXvFDL0%2Bp0hzBmcC4abUSs0PpbQT7u9ji52zh9501EEuR9zZYbGNAZUS621mzPzTbKnRDhS%2BwluojVkZtAKDaeYrfqb62kM0fVdSvMLmVO9S6lyjMAFdFi5uUDsKlLN1VwjXCvGuYvlhL4zeynIfPMPbt93XfZ5O6V0WMsT%2BI7lEB%2Fc6XqwNI9CjYqs%2FfsSPWMWoyR3KSOHOpTKpLNV1r1pbEaJdDh8G--Ek70j0h5S5o16gCa--buwvef3zj3J%2FUMCAtqJyYA%3D%3D"
cookie = CGI.unescape(cookie)
data, iv, auth_tag = cookie.split("--").map do |v| 
  Base64.strict_decode64(v)
end
cipher = OpenSSL::Cipher.new("aes-256-gcm")

# Compute the encryption key
secret_key_base = Rails.application.secret_key_base
secret = OpenSSL::PKCS5.pbkdf2_hmac(secret_key_base, "authenticated encrypted cookie", 1000, cipher.key_len, OpenSSL::Digest::SHA256.new)

# Setup cipher for decryption and add inputs
cipher.decrypt
cipher.key = secret
cipher.iv  = iv
cipher.auth_tag = auth_tag
cipher.auth_data = ""

# Perform decryption
cookie_payload = cipher.update(data)
cookie_payload << cipher.final
cookie_payload = JSON.parse(cookie_payload)

# Decode Base64 encoded stored data
decoded_stored_value = Base64.decode64(cookie_payload["_rails"]["message"])
stored_value = JSON.parse(decoded_stored_value)

When using .pbkdf2_hmac with OpenSSL::Digest::SHA256 insted of pbkdf2_hmac_sha1 method for computing the encryption key in Rails 7 it works.

@Duleja
Copy link

Duleja commented Oct 19, 2022

Also I just checked the original solution and it works as well...
it_works

@KevinTriplett
Copy link

KevinTriplett commented Dec 17, 2022

To verify the cookie, can use the following (from the Rails code):

purpose = "cookie.#{session_key}"
...
cookie_payload = cipher.update(data)
cookie_payload << cipher.final
message = ActiveSupport::Messages::Metadata.verify(cookie_payload, purpose)
...

Where session_key is the name given to the cookie. If message is returned as nil, raise an error that the cookie is not verifiable (eg, is being spoofed).

@KevinTriplett
Copy link

KevinTriplett commented Dec 17, 2022

Also, I had to remove cookie = CGI.unescape(cookie) otherwise Base64.strict_decode64(v) throws an ArgumentError: invalid base64 error. Also, I allow the salt to be configurable and check for invalid auth_tag as warned by this comment.

So my method which works with Rails 7.0.4 is:

  def verify_and_decrypt_cookie(key, secret_key_base = Rails.application.secret_key_base, purpose = nil)
    raise "no cookie to decrypt" unless cookies[key]
    data, iv, auth_tag = cookies[key].split("--").map { |v| ::Base64.strict_decode64(v) }
    raise "auth_tag is invalid" if auth_tag.nil? || auth_tag.bytes.length != 16
    purpose ||= "cookie.#{key}"
    
    cipher = OpenSSL::Cipher.new("aes-256-gcm")

    # Compute the encryption key
    salt = Rails.configuration.action_dispatch.authenticated_encrypted_cookie_salt
    secret = OpenSSL::PKCS5.pbkdf2_hmac(secret_key_base, salt, 1000, cipher.key_len, OpenSSL::Digest::SHA256.new)

    # Setup cipher for decryption and add inputs
    cipher.decrypt
    cipher.key = secret
    cipher.iv  = iv
    cipher.auth_tag = auth_tag
    cipher.auth_data = ""

    # Perform decryption and verification
    cookie_payload = cipher.update(data)
    cookie_payload << cipher.final
    message = ActiveSupport::Messages::Metadata.verify(cookie_payload, purpose)
    raise "cannot verify cookie" if message.nil?
    cookie_payload = JSON.parse(cookie_payload)

    # Decode Base64 encoded stored data
    decoded_stored_value = ::Base64.decode64(cookie_payload["_rails"]["message"])
    JSON.parse(decoded_stored_value)
  end

@lcmen
Copy link

lcmen commented May 11, 2023

If any of you is trying this in application upgraded from Rails 6 to Rails 7 and still and getting: 'final': OpenSSL::Cipher::CipherError error then you need to use pbkdf2_hmac_sha1. Here is an updated (and more flexible) version that should work on both: Rails 6 and Rails 7.

def decrypt_cookie(cookie)
  cookie = CGI.unescape(cookie)
  data, iv, auth_tag = cookie.split("--").map { |v| Base64.strict_decode64(v) }
  raise InvalidMessage if (auth_tag.nil? || auth_tag.bytes.length != 16)

  cipher = OpenSSL::Cipher.new("aes-256-gcm")
  secret = OpenSSL::PKCS5.pbkdf2_hmac(
    Rails.application.secret_key_base,
    Rails.configuration.action_dispatch.authenticated_encrypted_cookie_salt,
    1000,
    cipher.key_len,
    Rails.configuration.active_support.hash_digest_class.new
  )

  # Setup cipher for decryption and add inputs
  cipher.decrypt
  cipher.key = secret
  cipher.iv  = iv
  cipher.auth_tag = auth_tag
  cipher.auth_data = ""

  # Perform decryption
  cookie_payload = cipher.update(data)
  cookie_payload << cipher.final
  cookie_payload = JSON.parse(cookie_payload)

  message = ActiveSupport::Messages::Metadata.verify(cookie_payload, "decrypt")
  JSON.parse(Base64.decode64(cookie_payload["_rails"]["message"]))
end

CGI.unescape is used so cookie can be copied directly from a browser.

@dirkjonker
Copy link

If anyone has issues decrypting cookies outside of Rails in development after updating to Rails 7.1: this might be because the location of the secret_key_base was moved from tmp/development_secret.txt to tmp/local_secret.txt
so a simple cp tmp/development_secret.txt tmp/local_secret.txt might fix your issues

@bgvo
Copy link

bgvo commented Dec 21, 2023

In case anyone is interested, I put together a gem that makes it easy to incorporate session cookies decryption/encryption into any Rails' project: https://github.com/bgvo/rails_session_cipher

You can read about the motivation in my blog

@felipecsl
Copy link

I got this to work with Rails 7.1 by just removing the line message = ActiveSupport::Messages::Metadata.verify(cookie_payload, "decrypt") which wasn't working since ActiveSupport::Messages::Metadata.verify no longer exists

@felipecsl
Copy link

felipecsl commented Jan 25, 2024

Also wrote a port of this in Typescript for anyone interested https://gist.github.com/felipecsl/a6959e54caf2e53238306e2167e90ba2

@tonekk
Copy link

tonekk commented Jun 21, 2024

In case anyone ever needs this, pre Rails 5.2 session cookies are decoded like this:

  def self.decrypt_cookie(cookie, app_secret)
    token_hashed = OpenSSL::PKCS5.pbkdf2_hmac_sha1(app_secret, "encrypted cookie", 1000, 32)
    encrypted_message = Base64.decode64(cookie).split("--")[0]
    decoded_cookie = Base64.strict_decode64(encrypted_message)
    cipher = OpenSSL::Cipher.new("aes-256-cbc")
    cipher.key = token_hashed
    cipher.update(decoded_cookie)
  end

Rails 5.2 introduced use_authenticated_cookie_encryption, which changed the algorithm from aes-256-cbc (old) to aes-256-gcm (new). See here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment