By default, Ruby on Rails stores session data in cookies. This has many advantages, such as the lack of need to setup any persistence layers on the server side. However, the session data is not encrypted, and the Rails app that I'm writing puts potentially sensitive data in the session. I'd like to avoid storing session data server-side if possible, and the only existing encrypted cookie store implementation for Rails seems to be abandoned, so I'm writing my own encrypted cookie store.
The Rails cookie session store works as follows:
- It serializes the session data into a byte string.
- The serialized data is converted to base64.
- The base64 data is appended by an HMAC. Rails requires the HMAC secret key to be at least 30 bytes; by default Rails generates a 128 byte random string as secret key, obtained from /dev/urandom. The default hashing algorithm used for the HMAC is SHA-1.
- The base64 data + HMAC is sent to the HTTP client as a cookie.
- When the client performs a request, Rails first checks whether HMAC verification succeeds. If not then it will silently discard the session data in the cookie, as if the user didn't sent any session data at all. This also means that if the administrator changes the HMAC secret key, then all old sessions are automatically invalidated.
- The base64 data is de-base64'ed and unmarshalled into Ruby data structures.
The encrypted cookie session store that I'm writing subclasses the normal cookie store class. It works as follows:
- It inserts a step 3.5: after step 3 it will encrypt the data.
- It modifies step 5: before checking for the HMAC, it decrypts the data.
- The encryption algorithm is AES-256 in CFB mode. I understand that EBC will expose repeating patterns.
- The code requires the administrator to specify an encryption key of exactly 32 bytes. The encryption key is not hashed. By default it suggests a randomly generated encryption key of exactly 32 bytes, obtained from /dev/urandom.
- It also requires the administrator to specify an initialization vector of exactly 16 bytes. The IV is not hashed, and by default it suggests a randomly generated IV, obtained from /dev/urandom.
The reason why I HMAC before encrypting (instead of HMAC after encrypting) is because I want to be able to detect changes to the encryption key or the IV. I want the software to automatically invalidate old sessions if the encryption key or IV is changed. If I HMAC after encrypting then HMAC verification will pass if I change the encryption key or IV, which is undesirable.
Is my approach secure? If not, then what's lacking?
A few notes:
- I want to use CTR, as recommended by http://www.daemonology.net/blog/2009-06-11-cryptographic-right-answers.html, but unfortunately OpenSSL (which is included in the Ruby standard library) doesn't support CTR, and I don't want to require my users to install separate third party crypto libraries.
- I want to use SHA-256 for the HMAC. daemonology.net recommends against SHA-512 because of potential "side-channel attacks on 32-bit systems" (whatever that means). However, SHA-256 as hashing algorithm for HMAC is not supported on every platform. Most notably, Ruby as shipped on OS X doesn't support HMAC+SHA256. I want my code to work out of the box on OS X as well.