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.
This is pretty sweet. Have you done any benchmarks comparing this to db-store or memcached?
January 21st, 2008 at 09:42 AM
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.
January 21st, 2008 at 04:42 PM
Probably would not notice any latency until you load test it with many requests/second.
January 22nd, 2008 at 08:09 PM
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.
January 28th, 2008 at 07:34 AM
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.
January 28th, 2008 at 07:40 AM
I placed your code into environment.rb and nothing happened. Am I missing something?
January 28th, 2008 at 12:54 PM
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!
January 29th, 2008 at 07:13 PM
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
January 29th, 2008 at 07:16 PM