17 June 2023

C# - Communicating with Ruby on Rails using signed cookies

Overview

When making a C# program that talks to an existing Ruby on Rails program, the following is helpful for working with signed cookies.

C# Code

using Newtonsoft.Json.Linq;
using System.Security.Cryptography;
using System.Text;

String getValue(string cookie, string secret)
{
    var values = cookie.Split("--");
    var returnValue = "";

    Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(Encoding.ASCII.GetBytes(secret),
    Encoding.ASCII.GetBytes("signed cookie"), 1000, HashAlgorithmName.SHA256);

    var key = pbkdf2.GetBytes(64);
    HMACSHA256 digest = new HMACSHA256(key);
    var data = digest.ComputeHash(Encoding.ASCII.GetBytes(values[0]));

    if (values[1] == ToHexString(data))
    {
        JObject jsonValue = JObject.Parse(Encoding.ASCII.GetString(Convert.FromBase64String(values[0])));
        var _rails = jsonValue["_rails"];
        if (_rails != null)
        {
            var message = _rails["message"].ToString();
            if (message != null)
            {
                returnValue = Encoding.ASCII.GetString(Convert.FromBase64String(message));
            }
        }
    }

    return returnValue;
}

Ruby on Rails signed cookies

Signed cookies are used when the data stored is harmless for the end user to view but not modify. These can be user id’s that would be seen anyway or other values that aren’t sensitive.

The basic way this works is a hash is created of the message using a chosen cipher with a private key on the server with salt. This is then applied to the cookie after the “–”. Once that happens, you can ensure the message hasn’t been tampered with otherwise the hash would change.

The data is then Base64 encoded for easier transport.

Defaults

:signed_cookie_salt=>"signed cookie",
:encrypted_cookie_salt=>"encrypted cookie",
:encrypted_signed_cookie_salt=>"signed encrypted cookie",
:authenticated_encrypted_cookie_salt=>"authenticated encrypted cookie",

To view them

irb(main):001:0> Rails.application.config.action_dispatch

Normally stored as _app_session_data

def verify_and_decrypt_session_cookie(cookie, secret_key_base)
  data, iv, auth_tag = cookie.split("--").map do |v|
    Base64.strict_decode64(v)
  end
  # encrypted_cookie_cipher
  cipher = OpenSSL::Cipher.new("aes-256-gcm")
  # secret_key_base, authenticated_encrypted_cookie, rounds, key_size, signed_cookie_digest
  secret = OpenSSL::PKCS5.pbkdf2_hmac(secret_key_base, "authenticated encrypted cookie", 1000, 32, OpenSSL::Digest::SHA256.new)

  cipher.decrypt
  cipher.key = secret
  cipher.iv = iv
  cipher.auth_tag = auth_tag
  cipher.auth_data = ""

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

  decoded_stored_value = Base64.decode64 cookie_payload["_rails"]["message"]
  stored_value = JSON.parse decoded_stored_value
end
cookies.signed[:appdata] = 'Joseph'
def verify_cookie(cookie, secret)
  values = cookie.split("--").first
  # secret_key_base, signed_cookie_salt, rounds, key_length, hash
  key = OpenSSL::PKCS5.pbkdf2_hmac(secret, "signed cookie", 1000, 64, OpenSSL::Digest::SHA256.new)
  digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, key, values)
  digest == cookie.split("--")[1]
end
def cookie_data
  data = JSON.parse Base64.decode64(cookie.split('--')[0])
  Base64.decode64(data['_rails']['message'])
end
tags: c# - ruby on rails - cookies