Posted on 2017-04-25 by Philip Cunningham

At work I was given the opportunity to write a Haskell web application that had to co-exist with an existing Ruby on Rails application. In order to do this we leveraged some of my existing work on the ruby-marshal package and created a package called rails-session to decrypt Rails cookies. This allowed our Haskell application to effectively piggyback on the Rails application’s authentication mechanism.

This tutorial assumes you are using at least Rails 4.0.0. If you are using an older version of Rails then I would recommend that you upgrade as soon as possible. If this isn’t an option then I would be happy to review a pull request that adds this functionality to rails-session.

Decryption

There are two steps required to decrypt a Rails cookie. First we have to prepare our cookie by unwrapping the different encoding layers in order to access the encrypted data and initialisation vector before finally decrypting it.

But before looking at any Haskell code, you may find it helpful to visualise the structure.

┌─────────────────────────── URL encoding ────────────────────────────┐ │ ┌────────────────── base64 ─────────────────────┬────┬─────────────┐ │ │ │ │ │ │ │ │ │ ┌──── base64 ─────┬────┬────── base64 ────────┤ │ │ │ │ │ │ encrypted data │"--"│initialisation vector │"--"│ signature │ │ │ │ └─────────────────┴────┴──────────────────────┤ │ │ │ │ │ │ │ │ │ │ └───────────────────────────────────────────────┴────┴─────────────┘ │ └──────────────────────────────────────────────────────────────────────┘

We start by defining some newtype wrappers so we don’t accidentally use our bytestrings in the wrong place by accident. These come with a set of smart constructors that are exported to consumers of our package in case we decide to use a different representation internally at a later stage.

newtype Cookie = Cookie ByteString deriving ( Show , Ord , Eq ) newtype EncryptedData = EncryptedData ByteString deriving ( Show , Ord , Eq ) newtype InitVector = InitVector ByteString deriving ( Show , Ord , Eq )

Then we define a function that creates a pipeline using the & operator to transform each layer in turn.

prepare :: Cookie -> ( EncryptedData , InitVector ) prepare ( Cookie cookie) = -- Unwrap the first layer by removing URL encoding. urlDecode True cookie -- Split on the string "--" and throw away the signature. & (fst . split) -- Remove Base64 encoding. & base64decode -- Split on the string "--" & split -- Base64 decode the encrypted data 'x' and the initialisation vector 'y'. & (\(x, y) -> ( EncryptedData (base64decode x), InitVector (base64decode y))) where base64decode :: ByteString -> ByteString base64decode = B64.decodeLenient separator :: ByteString separator = "--" split :: ByteString -> ( ByteString , ByteString ) split = BS.breakSubstring separator

Next we define a function to generate our secret using PBKDF2 with SHA-1 as a hash function. As before, we define newtype wrappers and smart constructors for Salt , SecretKeyBase and SecretKey .

generateSecret :: Salt -> SecretKeyBase -> SecretKey generateSecret ( Salt salt) ( SecretKeyBase secret) = -- Rails uses 1000 as the default number of iterations (https://git.io/v9q5i) and -- 64 as the default key size (https://git.io/v9q56). SecretKey $ sha1PBKDF2 secret salt 1000 64

Finally we can put these together to access the raw unencrypted bytestring we need using the cryptonite package.

decrypt :: Maybe Salt -> SecretKeyBase -> Cookie -> Either String DecryptedData decrypt mbSalt secretKeyBase cookie = -- Prepare the salt. let salt = fromMaybe defaultSalt mbSalt -- Generate our secret. ( SecretKey secret) = generateSecret salt secretKeyBase -- Unwrap the different encoding layers. ( EncryptedData encData, InitVector initVec) = prepare cookie in case makeIV initVec of Nothing -> Left $ "Failed to build init. vector for: " <> show initVec Just initVec' -> do -- Since Rails uses AES-256 by default we take 32 bytes of our secret. -- https://git.io/v9q5w let key = BS.take 32 secret -- Use AES-256. case (cipherInit key :: CryptoFailable AES256 ) of CryptoFailed errorMessage -> Left (show errorMessage) CryptoPassed cipher -> -- Decrypt the encrypted data. Right . DecryptedData $ cbcDecrypt cipher initVec' encData where defaultSalt :: Salt defaultSalt = Salt "encrypted cookie"

Deserialisation

Marshal was the default cookie serialiser in Rails until version 4.1, after which JSON serialisation became the default. The decode function in rails-session uses the ruby-marshal package to decrypt and then deserialise your cookie but you could just as easily use decrypt in conjunction with aeson.

More details about ruby-marshal can be found here.

Infrastructure

In order to ensure that the Rails cookie gets submitted on each request from the browser you should put a reverse proxy, e.g. nginx, in front of both applications.

┌────────────────────────────┐ ┌────────────────────────────┐ │ │ │ │ │ │ │ │ │ Haskell web app │ │ Ruby on Rails app │ │ │ │ │ │ │ │ │ │ │ │ │ └────▲──────────────────┬────┘ └────▲──────────────────┬────┘ │ │ │ │ ┌────┴──────────────────▼─────────────┴──────────────────▼────┐ │ │ │ Reverse proxy e . g . nginx │ │ │ └───────────────────────▲─────────────┬───────────────────────┘ │ │ │ │ │ │ │ │ ┌──┴─────────────▼──┐ │ │ │ │ │ │ │ Browser │ │ │ │ │ │ │ └───────────────────┘

Example

In our Haskell application, we have an authorisation hook to check that the current user is still authenticated in a backend service. I’ve stubbed it out below to give you an idea of how you might go about using rails-session in a web framework like Spock.

import Data.String.Conv (toS) import qualified Web.Rails.Session as Rails import qualified Web.Spock as Spock authHook secretKeyBase = do -- Get the Rails cookie from the request. railsCookie <- Spock.cookie "_app_name_session" case railsCookie of Nothing -> do -- Handle failure. Just cookie -> do -- Decrypt and deserialise the cookie using rails-session and ruby-marshal. case Rails.decode Nothing secretKeyBase (Rails.mkCookie . toS $ cookie) of Nothing -> do -- Handle failure. Just session -> do -- Use the Rails session in your Haskell application!

Conclusion

By writing the rails-session package, we were able to create a Haskell web application that coexisted with a Rails application. This approach has been a success for us and we hope that you too might be able to apply what we’ve learned to a Rails application in the near future.