Saturday, August 9, 2008

Encrypting Cookie Data with ASP.NET

Overview of Cryptography in .NET
First, let's just acknowledge that encryption is really complicated. Frankly, it's supposed to be complicated because that is what makes it secure. And, by the way, I want to state here that I am in no way an encryption expert. There are probably only a handful of people that fully understand the intricacies of the math involved, and half of them probably wrote the cryptography classes for the .NET Framework.
Fortunately, we don't have to understand the intricacies to take advantage of encryption because the .NET Framework gives us a simple way to access the power of the best and most secure algorithms. Before we begin encrypting our cookie data, we should have a little discussion about the cryptography classes and which ones are appropriate for securing our cookies.

What Is Encryption Exactly?
In the simplest terms, encryption is the process of taking a target string of characters (bytes, more specifically) and converting it into another string of characters so that by examination, the original characters cannot be deciphered. This process is performed with a second string of characters known as a "key". In technical terms, this key is mathematically "mashed" into the original string over many complex and compounded iterations. In addition, sometimes another string of characters, known as an "initialization vector", is "mangled" into the target string before the key is mashed. This helps to prevent obvious patterns in the target string from being revealed by mashing alone. Of course, the mashing and mangling is not permanent. If the key and the initialization vector remain intact, then the algorithms can use them in reverse to unmangle and unmash the original data.
The effectiveness or "strength" of the encryption is determined by the size of the key used. The larger the key, the stronger the encryption. Typical key sizes are 64 bit (8 bytes), 128 bit (16 bytes), 192 bit (24 bytes), 256 bit (32 bytes) and 512 bit (64 bytes). The only way to crack most of these algorithms is by sheer brute force, where an attacker creates a program to test every single possible key combination. For even the 64-bit option, there are 72,057,594,037,927,936 possible combinations (2^56 - 8 bits are held for parity). So basically, unless you have a very powerful computer, you are going to be waiting awhile to crack even the weakest key.

Cryptography Patterns
There are two basic approaches for handling data encryption: symmetric (or private key) and asymmetric (or public key). In symmetric cryptography techniques, both sides of the data exchange (the encryptor and decryptor) must have access to a single secret private key.
In asymmetric encryption, one side (the decryptor) requests a public key from the encryptor. The encryptor creates a public key and sends it to the requestor. Then the encryptor uses the public key to create a second unique private key that it holds on to. This new private key is used to encrypt the message sent to the decryptor, who then uses the public key to decrypt the message. If encrypted data must be sent in the other direction then the other side will create a new public key and sends it along in a reciprocal fashion. This asymmetric technique is used by SSL to secure HTTP transmissions.
For our cookie encryption purposes, we will use the symmetric approach since both the encryption and decryption will take place in the same application on the server; therefore, we only need one private key that we will keep secure in the compiled code of our cryptographic utility class.

Cryptographic Service Providers
The .NET Framework provides 4 cryptographic algorithms that extend from the base SymmetricAlgorithm class:
1. System.Security.Cryptography.DES (Data Encryption Standard)
2. System.Security.Cryptography.TripleDES (Triple Data Encryption Standard)
3. System.Security.Cryptography.RC2
4. System.Security.Cryptography.Rijndael

Key Size Considerations
DES is one of the weaker encryption methods because its key size is limited to 64 bits. However, for our cookie purposes, this level of encryption is probably sufficient. TripleDES, which, believe it or not, performs the encryption three times, also has a larger key size. The length of the key must be either 128 or 192 bits - two to three times larger. TripleDES is significantly more secure. Deciding which algorithm to use should not only be based on your desire for stronger encryption but also by the size of the cookies you require. You will notice in our example application that encrypted data will grow in size. And of course, the larger the key, the larger the resulting data. This is an important consideration since cookie data is restricted to 4kb.
An alternative is to store data that you only need to persist temporarily in hidden input tags. Of course, this is exactly what ViewState does in ASP.NET. In addition, ViewState offers some built-in encryption. The ViewState of a Web form, by default, uses hash functions from some of the asymmetric algorithms to garble the content. This is not fully secure. However, you can configure your ASP.NET application or individual pages to use more secure encryption on the ViewState - specifying the same TripleDES algorithm we discussed earlier. Here is an article on MSDN on how to do just that:

http://msdn.microsoft.com/library/en-us/dnaspnet/html/asp11222001.asp?frame=true
if you are carrying that much data around in cookies, you really should reconsider you Web-application design. Remember, each request made of the server will need to pass this cookie blob back, unnecessarily chewing up bandwidth.
Finally, encryption is a processor intensive activity. The more data you are encrypting/decrypting or the stronger your algorithm, the more server resources you will require, potentially slowing down the entire site.

The CryptoStream Object
One of the things you may find hard to understand at first is that all encryption and decryption in .NET is handled through the CryptoStream object. I know you are thinking that all we need to do is convert a few strings. Why do we need to mess with complicated streams. Well, from the broader scope of cryptographic issues and from the design emphasis of the .NET Framework, using streams really makes sense. Nearly all access (IO) to external resources handled by the .NET Framework is done through the use of streams. While it is not in the System.IO namespace, the CryptoStream object inherits from the System.IO.Stream object.
The major advantage of using streams is that they can be chained together. This is particularly useful when performing file operations or accessing other network services. For instance, you can open a socket to a network service and stream data that is simultaneously being encrypted. The following link provides an example of DES provider encrypting a file through streams.
http://msdn.microsoft.com/library/en-us/cpref/html/frlrfSystemSecurityCryptographyDESClassTopic.asp?frame=true
So, in order to work with strings as a stream, we need to utilize a special class called a MemoryStream. This will allow us to handle strings as streams and flow them through the CryptoStream for processing.

Creating Cryptography Utility Class

Imports System.Diagnostics
Imports System.Security.Cryptography
Imports System.Text
Imports System.IO
Public Class CryptoUtil
'8 bytes randomly selected for both the Key and the Initialization Vector
'the IV is used to encrypt the first block of text so that any repetitive
'patterns are not apparent
Private Shared KEY_64() As Byte = {42, 16, 93, 156, 78, 4, 218, 32}
Private Shared IV_64() As Byte = {55, 103, 246, 79, 36, 99, 167, 3}
'24 byte or 192 bit key and IV for TripleDES
Private Shared KEY_192() As Byte = {42, 16, 93, 156, 78, 4, 218, 32, _
15, 167, 44, 80, 26, 250, 155, 112, _
2, 94, 11, 204, 119, 35, 184, 197}
Private Shared IV_192() As Byte = {55, 103, 246, 79, 36, 99, 167, 3, _
42, 5, 62, 83, 184, 7, 209, 13, _
145, 23, 200, 58, 173, 10, 121, 222}
'Standard DES encryption
Public Shared Function Encrypt(ByVal value As String) As String
If value <> "" Then
Dim cryptoProvider As DESCryptoServiceProvider = _
New DESCryptoServiceProvider()
Dim ms As MemoryStream = New MemoryStream()
Dim cs As CryptoStream = _
New CryptoStream(ms, cryptoProvider.CreateEncryptor(KEY_64, IV_64), _
CryptoStreamMode.Write)
Dim sw As StreamWriter = New StreamWriter(cs)
sw.Write(value)
sw.Flush()
cs.FlushFinalBlock()
ms.Flush()
'convert back to a string
Return Convert.ToBase64String(ms.GetBuffer(), 0, ms.Length)
End If
End Function
'Standard DES decryption
Public Shared Function Decrypt(ByVal value As String) As String
If value <> "" Then
Dim cryptoProvider As DESCryptoServiceProvider = _
New DESCryptoServiceProvider()
'convert from string to byte array
Dim buffer As Byte() = Convert.FromBase64String(value)
Dim ms As MemoryStream = New MemoryStream(buffer)
Dim cs As CryptoStream = _
New CryptoStream(ms, cryptoProvider.CreateDecryptor(KEY_64, IV_64), _
CryptoStreamMode.Read)
Dim sr As StreamReader = New StreamReader(cs)
Return sr.ReadToEnd()
End If
End Function
'TRIPLE DES encryption
Public Shared Function EncryptTripleDES(ByVal value As String) As String
If value <> "" Then
Dim cryptoProvider As TripleDESCryptoServiceProvider = _
New TripleDESCryptoServiceProvider()
Dim ms As MemoryStream = New MemoryStream()
Dim cs As CryptoStream = _
New CryptoStream(ms, cryptoProvider.CreateEncryptor(KEY_192, IV_192), _
CryptoStreamMode.Write)
Dim sw As StreamWriter = New StreamWriter(cs)
sw.Write(value)
sw.Flush()
cs.FlushFinalBlock()
ms.Flush()
'convert back to a string
Return Convert.ToBase64String(ms.GetBuffer(), 0, ms.Length)
End If
End Function
'TRIPLE DES decryption
Public Shared Function DecryptTripleDES(ByVal value As String) As String
If value <> "" Then
Dim cryptoProvider As TripleDESCryptoServiceProvider = _
New TripleDESCryptoServiceProvider()
'convert from string to byte array
Dim buffer As Byte() = Convert.FromBase64String(value)
Dim ms As MemoryStream = New MemoryStream(buffer)
Dim cs As CryptoStream = _
New CryptoStream(ms, cryptoProvider.CreateDecryptor(KEY_192, IV_192), _
CryptoStreamMode.Read)
Dim sr As StreamReader = New StreamReader(cs)
Return sr.ReadToEnd()
End If
End Function
End Class
Note that our keys are initialized at the top as an array of bytes. Also, note that I am using numeric constants in these arrays. If you choose to do the same, be sure to keep the values greater than or equal to zero and less than or equal to 255. This is the allowable range for a byte value.
In the class, we have 2 pairs of functions for encrypting and decrypting under both the DES and TripleDES providers. On the encrypt functions, the final byte array in the Memory Stream buffer is converted back to a string using the ToBase64String function. The reverse is done on the decrypt functions with the FromBase64String function.

Creating a Cookie Utility Class
Our next step is to create a simple class for setting and retrieving either plain cookies or those with encryption on top. Again we use series of shared methods to simplify the setting and getting of cookie data, allowing you to handle cookie data with one line of code. Imports System.Web


Public Class CookieUtil
'SET COOKIE FUNCTIONS *****************************************************
'SetTripleDESEncryptedCookie - key & value only
Public Shared Sub SetTripleDESEncryptedCookie(ByVal key As String, _
ByVal value As String)
'Convert parts
key = CryptoUtil.EncryptTripleDES(key)
value = CryptoUtil.EncryptTripleDES(value)
SetCookie(key, value)
End Sub
'SetTripleDESEncryptedCookie - overloaded method with expires parameter
Public Shared Sub SetTripleDESEncryptedCookie(ByVal key As String, _
ByVal value As String, ByVal expires As Date)
'Convert parts
key = CryptoUtil.EncryptTripleDES(key)
value = CryptoUtil.EncryptTripleDES(value)
SetCookie(key, value, expires)
End Sub
'SetEncryptedCookie - key & value only
Public Shared Sub SetEncryptedCookie(ByVal key As String, _
ByVal value As String)
'Convert parts
key = CryptoUtil.Encrypt(key)
value = CryptoUtil.Encrypt(value)
SetCookie(key, value)
End Sub
'SetEncryptedCookie - overloaded method with expires parameter
Public Shared Sub SetEncryptedCookie(ByVal key As String, _
ByVal value As String, ByVal expires As Date)
'Convert parts
key = CryptoUtil.Encrypt(key)
value = CryptoUtil.Encrypt(value)
SetCookie(key, value, expires)
End Sub
'SetCookie - key & value only
Public Shared Sub SetCookie(ByVal key As String, ByVal value As String)
'Encode Part
key = HttpContext.Current.Server.UrlEncode(key)
value = HttpContext.Current.Server.UrlEncode(value)
Dim cookie As HttpCookie
cookie = New HttpCookie(key, value)
SetCookie(cookie)
End Sub
'SetCookie - overloaded with expires parameter
Public Shared Sub SetCookie(ByVal key As String, _
ByVal value As String, ByVal expires As Date)
'Encode Parts
key = HttpContext.Current.Server.UrlEncode(key)
value = HttpContext.Current.Server.UrlEncode(value)
Dim cookie As HttpCookie
cookie = New HttpCookie(key, value)
cookie.Expires = expires
SetCookie(cookie)
End Sub
'SetCookie - HttpCookie only
'final step to set the cookie to the response clause
Public Shared Sub SetCookie(ByVal cookie As HttpCookie)
HttpContext.Current.Response.Cookies.Set(cookie)
End Sub
'GET COOKIE FUNCTIONS *****************************************************
Public Shared Function GetTripleDESEncryptedCookieValue(ByVal key As String) _
As String
'encrypt key only - encoding done in GetCookieValue
key = CryptoUtil.EncryptTripleDES(key)
'get value
Dim value As String
value = GetCookieValue(key)
'decrypt value
value = CryptoUtil.DecryptTripleDES(value)
Return value
End Function
Public Shared Function GetEncryptedCookieValue(ByVal key As String) As String
'encrypt key only - encoding done in GetCookieValue
key = CryptoUtil.Encrypt(key)
'get value
Dim value As String
value = GetCookieValue(key)
'decrypt value
value = CryptoUtil.Decrypt(value)
Return value
End Function
Public Shared Function GetCookie(ByVal key As String) As HttpCookie
'encode key for retrieval
key = HttpContext.Current.Server.UrlEncode(key)
Return HttpContext.Current.Request.Cookies.Get(key)
End Function
Public Shared Function GetCookieValue(ByVal key As String) As String
Try
'don't encode key for retrieval here
'done in the GetCookie function
'get value
Dim value As String
value = GetCookie(key).Value
'decode stored value
value = HttpContext.Current.Server.UrlDecode(value)
Return value
Catch
End Try
End Function
End Class


You'll notice most of the set functions are overloaded to provide an addition parameter for a cookie expiration date. Not setting the expiration will keep the cookie only for the browser session in memory. To set a permanent cookie, set an expiration date for a point in the future.
Also, note that the encryption functions handle the encryption of both the data and the key. It is important to encrypt the key as well since that can expose information about the nature of the data. For instance, if your key was "UserID", then it might be safe to assume that your encrypted value is a numeric string, giving an attacker an advantage.

Creating Web Forms To Set Cookies
In our sample application, I have two forms. The first (and the default form) is SetCookies.aspx, and the second is GetCookies.aspx

SetCookies.aspx Web Form

The set cookies form is quite simple. We have a single text box and a button to post the value back to the server.


When the Save Cookie button is clicked, the form returns to the server and initiates the following server-side event:


Protected Const COOKIE_KEYNAME = "MyKey"
Private Sub btnSaveCookie_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnSaveCookie.Click
CookieUtil.SetCookie(COOKIE_KEYNAME, _
txtCookieValue.Text, Now.AddDays(30))
CookieUtil.SetEncryptedCookie(COOKIE_KEYNAME, _
txtCookieValue.Text, Now.AddDays(30))
CookieUtil.SetTripleDESEncryptedCookie(COOKIE_KEYNAME, _
txtCookieValue.Text, Now.AddDays(30))
Response.Redirect("GetCookies.aspx")
End Sub


Using a constant for the cookie key name and the value submitted in the text box, we set three cookies. First, a standard unencrypted cookie. Second, a cookie encrypted with DES, and third, a cookie encrypted with Triple DES.


GetCookies.aspx Web Form
The Get Cookies Web Form is doing a little more work behind the scene to show you what happens to your cookie data.

In the Page_Load event, we retrieve the cookie data using our CookieUtil accessor functions. Then, finally we dump out the contents of the entire cookie collection, just to prove there was no funny business.


Protected Const COOKIE_KEYNAME = "MyKey"
Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
'Put user code to initialize the page here
Dim DESCookieKey As String = CryptoUtil.Encrypt(COOKIE_KEYNAME)
Dim TripleDESCookieKey As String = _
CryptoUtil.EncryptTripleDES(COOKIE_KEYNAME)
lblCookieName.Text = COOKIE_KEYNAME
lblStandardCookieValue.Text = _
CookieUtil.GetCookieValue(COOKIE_KEYNAME)
lblDESEncryptedValue.Text = _
CookieUtil.GetCookieValue(DESCookieKey)
lblDESDecryptedValue.Text = _
CookieUtil.GetEncryptedCookieValue(COOKIE_KEYNAME)
lblTripleDESEncryptedValue.Text = _
CookieUtil.GetCookieValue(TripleDESCookieKey)
lblTripleDESDecryptedValue.Text = _
CookieUtil.GetTripleDESEncryptedCookieValue(COOKIE_KEYNAME)
End Sub

Cautions about Cookies
So there you have it - a fairly simple yet powerful way to encrypt your cookie data. But before I leave you, I want to review a few issues to watch out for when using cookies.

  • Your entire cookie collection is limited to 4 kilobytes in size. This includes key, the space for key names, and other related data.
  • The size of your cookie data will increase when encrypted. Make sure you do not run over the limit.
  • Cookies are sent to the server every time a request is made to the server. Therefore, if you are lugging large cookies back and forth, you are using additional bandwidth and server processing resources.
  • Encrypting large amounts of data can impact application performance.
  • Any initial values supplied by your user cannot be encrypted unless you use SSL on your site. For instance, in our sample set cookies form, the initial value was passed in the clear to the server.
  • Cookies can be blocked or deleted at anytime by your users. If the use of cookies is critical to the operation of your application, you may need to find ways to work around users that can't or won't permit them.

No comments: