Baking those "Special" Kind of Cookies

January 13th, 2008

Following up to my first post on Rails 2 sessions I want to follow up and provide a decent solution to the problem. Let me first go on the record that you should default to the database session store, but if you still really want to go the cookie route for some reason there should be a safe way. That being said let's dig a little deeper into the cause and see if we can't duck punch our way right in to the session creation methods. If we attack this problem at the point where it falls down we are likely to have success encrypting the data and not have to tamper with the internals of rails too much. Let's take a look and the cookie_store file inside of rails. The two methods we are after are the marshal and unmarshal methods. Here they are in their natural state.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

private
    # Marshal a session hash into safe cookie data. Include an integrity hash.
    def marshal(session)
      data = ActiveSupport::Base64.encode64(Marshal.dump(session)).chop
      CGI.escape "#{data}--#{generate_digest(data)}"
    end

    # Unmarshal cookie data to a hash and verify its integrity.
    def unmarshal(cookie)
      if cookie
        data, digest = CGI.unescape(cookie).split('--')
        unless digest == generate_digest(data)
          delete
          raise TamperedWithCookie
        end
        Marshal.load(ActiveSupport::Base64.decode64(data))
      end
    end
You can see here that Base64 encoding is the only thing going on here. The digest at the end after the -- is the verification signature of the cookie to prevent tampering. It would be great if we could just do this to the entire contents of the cookie, but since we need to be able to decrypt it later we can't rely on a one way hash. So now we need to find a decent two way encryption algorithm that we can use to encrypt and then decrypt the contents of the cookie. This is a good problem for the Ruby OpenSSL library to solve.

Let's take a look at a couple of methods to encrypt and decrypt some data.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

  def encrypt(cookie)
    cipher = OpenSSL::Cipher::Cipher.new("des-ecb")
    cipher.encrypt
    cipher.key = "insert key here"
    cipher.iv = "insert initialization vector here"
    encrypted_cookie = cipher.update(cookie)
    encrypted_cookie << cipher.final
    return encrypted_cookie
  end
  
  def decrypt(cookie)
    cipher = OpenSSL::Cipher::Cipher.new("des-ecb")
    cipher.decrypt
    cipher.key = "insert above key"
    cipher.iv = "insert above iv"
    decrypted_cookie = cipher.update(cookie)
    decrypted_cookie << cipher.final
    return decrypted_cookie
  end
end
This will allow you to solve the encryption problem. There is still one small problem here. The DES encryption algoriithm is very crackable. The OpenSSL cipher has a lot of algorithms, so just pick one that isn't as easily crackable. Now that we have figured the hard part out let's put together a duck punch for Rails. For now it will work if you put this code into your environment.rb file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

class CGI::Session::CookieStore
  
  def marshal(session)
    data = encrypt(Base64.encode64(Marshal.dump(session)).chop)
    CGI.escape "#{data}--#{generate_digest(data)}"
  end
  
  def unmarshal(cookie)
    if cookie
      data, digest = CGI.unescape(cookie).split('--')
      unless digest == generate_digest(data)
        delete
        raise TamperedWithCookie
      end
      Marshal.load(Base64.decode64(decrypt(data)))
    end
  end
  
  def encrypt(cookie)
    cipher = OpenSSL::Cipher::Cipher.new("des-ecb")
    cipher.encrypt
    cipher.key = "insert key here"
    cipher.iv = "insert initialization vector here"
    encrypted_cookie = cipher.update(cookie)
    encrypted_cookie << cipher.final
    return encrypted_cookie
  end
  
  def decrypt(cookie)
    cipher = OpenSSL::Cipher::Cipher.new("des-ecb")
    cipher.decrypt
    cipher.key = "insert above"
    cipher.iv = "insert above"
    decrypted_cookie = cipher.update(cookie)
    decrypted_cookie << cipher.final
    return decrypted_cookie
  end
end
Viola! You now have encrypted cookies. This code is still pretty rough, and I plan on refactoring it a bit and then turning it into a plugin for easier consumption.

8 Responses to “Baking those "Special" Kind of Cookies”

  1. Duncan Beevers Says:
    This is pretty sweet. Have you done any benchmarks comparing this to db-store or memcached?
  2. Aaron Bedra Says:
    I haven't done any actual benchmarking, but I did not notice any changes in latency in any of the applications I tried this on. I would like to benchmark it, but I still need to choose the right encryption algorithm and pull the code out / clean it up a bit.
  3. jason Says:
    Probably would not notice any latency until you load test it with many requests/second.
  4. David Vrensk Says:
    Wonderful! This is exactly what I need. I'm writing a simple time reporting/invoicing app that proxies to Basecamp to pick up time reported there. I obviously do not want to store any Basecamp passwords on disk, so the cookie-base session store is the way to go. I have made some small changes to your code: 1. Encrypt the raw marshalled data and base64-encode the result rather than encrypting the base64-encoded data. The latter approach (which you use) means that the data will be CGI-escaped which has an overhead of ~100% as opposed to 33% with base64. 2. Instead of specifying a separate encryption key and initialisation vector, I use the cookie-store "secret. This means that my code can be deployed as-is, which means it's easier to make a plugin out of it. Of course it can be argued that it lowers security (same string used for encrypting and signing), but I this is a good trade-off. 3. I read the comments on http://snippets.dzone.com/posts/show/576 and made sure my key and iv generation didn't leave a lot of empty bits. Using a printable password means that every byte only has 6.6 bits instead of 8. You can find my code at http://pastie.textmate.org/144367. I'm interested in seeing my version as a plugin, but if you want to do your own thing (TMTOWTDI!), I'll roll my own.
  5. David Vrensk Says:
    BTW, any hints on how to make your blog engine honour line breaks and other formatting would be useful ;) I actually checked the page source for hints before posting, but gave up. I hope you can read what I wrote anyway.
  6. jna Says:
    I placed your code into environment.rb and nothing happened. Am I missing something?
  7. Aaron Bedra Says:
    David, Thanks for the suggestions! I have done some refactoring and released the plugin, but would like to continue to make more changes. Your bits about performance and empty bits is also very good stuff. Thanks for the info, I will include it in my next refactoring!
  8. Aaron Bedra Says:
    jna, The code won't announce that it is doing anything, but you should see the cookie value change. I released a refactored version of this code as a plugin. You can get more information over at http://opensource.thinkrelevance.com

Sorry, comments are closed for this article.

-U:**- index.html.erb   (Ruby RoR RHTML)
M-x visit-site http://aaronbedra.com