Email 
Skip Navigation LinksHome > Computing > Methods of Transparent Web Application Encryption in ASP.NET

Methods of Transparent Web Application Encryption in ASP.NET

©Copyright 2009 Matt Kellerman

Introduction

In this article I'd like to spend some time illustrating techniques of encryption in web applications. We will transparently secure querystrings, cookies, and other aspects of web applications which can transmit sensitive information. Lastly, we will look at the deployment of web application security on individual websites, individual servers, and web farms.

Although this article is applicable to websites of all type, the code is implemented in Microsoft .NET 3.5.

Background

E n c r y p t i o n. Uh oh, looks like we already lost a few readers. For those of you who fell asleep after the first incandation of the word--which judging from my experience is about 98% of the population--wake back up and give me 2 minutes of your time.

Now, where were we. Ah yes, encryption. Isn't encryption some esoteric, time consuming, complex and entirely unncessary programatical task?

Why spend precious time designing and developing a custom encryption solution for your sensitive web application when you can just as easily omit it entirely and get the job done? Why not just modulo your application of security and move up your pay day by a week? After all, those in management who commissioned the web application never said anything about security and they probably don't know the difference between a bit and a byte. Then again, the old way of thought, "Security, we'll worry about it later", can come back to bite you later!

// Lets take the easy path
Path.Easy = Application % Security

// Cool, update time to payday to one week sooner!
PayDay.Date = Payday.AddDays(-7);

// Take a vacation and have a cocktail
Self.Go(Wolrd.GreatLocations.Hawaii);
Self.Drink(new MaiTai());

Of course, without the proper security, returning from your vacation won't be much fun:

// Get back from vacation
Airport.Arrive();
if (Email.Check.ProblemExists == true)
{
	foreach(Problem p in Email.Problems)
		Console.WriteLine("Hosed...");
}

MILTON'S INBOX:
[10:40 AM] ATTN: MILTON - SOMEBODY GOT HOLD OF ALL OF OUR CLIENT'S INFORMATION AND PASSWORDS...
[01:21 PM] ATTN: MILTON - SYSTEM COMPROMISED, REPORT IMMEDIATELY...
[03:55 PM] ATTN: MILTON - CREDIT CARD NUMBERS STOLEN...
[04:07 PM] ATTN: MILTON - REPORT IMMEDIATELY, COMPETITORS ARE CALLING OUR CLIENTS...
....

What a bummer, your web application was just compromised and now management is furious.

But who's fault is it really? After all, the web application requirements didn't mention anything about encryption. And we had a lot of work to get done in very little time on a small budget!

Hey, I didn't buy 12 Super Margaritaville Mixers at Costco last Sunday for $2,800!

The reality of web application development today is that most data which should be secured simply isn't. Many back-end data stores holding credit card numbers, social secuirty and driver's license numbers, un-hashed passwords and other confidential business information are floating around on out-of-date, un-patched servers with lax or non-existant firewall rulesets. The same information is transmitted insecurely by "dump trucks" on this great big technological marvel we call the Internet.

I have seen some pretty eye-opening security holes as a web developer. Credit card numbers sent by email in clear text or stored on a database server without encryption, passwords transmitted in email and stored in cookies. Client ID numbers visible in hidden form inputs. Logins without SSL. The list goes on and on.

To boot, most conversations with the commissioner's of web applications with regards to security and its importantce is familiarly akin to an irresistable force meeting an immovable object. Encryption may be a fundamental pillar to good business application design, yet it is often overlooked; an afterthought if you will.

Spare Some Change?

The mantra of "Security, We'll Worry About it Later", needs change if you are serious about deploying a web application that is safe for both your clients and your company. Given that your clients implicitly trust your web application, without security it is simply a function of time until the innevitable occurs (as Milton's Inbox shows). It is the responsibility of your company to safeguard confidential data and keep your client's trust for the long term. Once that trust is lost it may be impossible to regain.

Design

For this task, transparency is going to be key (last pun, I promise). Our goal is to design an encryption system that is plug-and-play. Just modify a web.config or machine.config, import a secret key or two into the server, and off we go.

Our first library will house the EncryptionHelper class, responsible for performing symmetrical encryption and decryption of data. It will also house the configuration class for defining encryption keys. We will store these keys later in our configuration files.

My symmetrical encryption algorthm of choice is AES-256, a block cipher developed by Joan Daemen and Vincent Rijmen and adopted by the NSA for top-secret information. Since .NET supports many different symmetrical encryption algorithms, feel free to use the one that suites you best.

Encryption Helper

/// <summary>
/// Provides static methods for symmetrical AES encryption, as well as asymmetrical MD5 hashing.
/// </summary>
public class EncryptionHelper
{
	/// <summary>
	/// Encrypts a string using AES encryption. A key and vector must be provided.
	/// </summary>
	public static string EncryptStringUsingAES(string text, string key, string vector)
	{
		// Turns string into bytes and feeds it to the encryptor, returning encrypted bytes.
		RijndaelAES aes = new RijndaelAES(Convert.FromBase64String(key), Convert.FromBase64String(vector));
		byte[] encryptedBytes = aes.Encrypt(text);

		return Convert.ToBase64String(encryptedBytes);
	}

	/// <summary>
	/// Dencrypts a string using AES encryption. A key and vector must be provided.
	/// </summary>
	public static string DecryptStringUsingAES(string encryptedText, string key, string vector)
	{
		// Turns string into bytes and feeds it to the decryptor, returning decrypted bytes.
		RijndaelAES aeshelper = new RijndaelAES(Convert.FromBase64String(key), Convert.FromBase64String(vector));
		byte[] encryptedBytes = Convert.FromBase64String(encryptedText); // utf.GetBytes(encryptedText);					
		string plaintext = aeshelper.Decrypt(encryptedBytes);

		return plaintext;
	}

	/// <summary>
	/// Perfroms an MD5 hash using the provided salt. 
	/// </summary>
	/// <remarks>
	/// An MD5 hash takes a plain text string and encrypts it in a one-way encryption algorithm. 
	/// A unique salt is utilized for additional security. MD5 is faster than SHA-1 hashes.
	/// Returns a 16 byte array (to be used as a binary(16) datatype with SQL)
	/// </remarks>
	public static byte[] HashTextUsingMD5(string clearText, string salt)
	{
		// Feel free to implement your own salt obfuscation algorithm here for additional security.
		return (new MD5CryptoServiceProvider()).ComputeHash((new UTF8Encoding()).GetBytes(clearText + salt));
	}
	
	/// <summary>
	/// Checks if a string is base-64 encoded.
	/// </summary>
	/// <param name="s"></param>
	/// <returns>True if the string is base-64 encoded. False otherwise.</returns>
	/// <remarks>If checking querystrings, ensure to URL decode the string prior to passing it to this method.</remarks>
	public static bool IsBase64Encoded(string s)
	{
		bool isencoded = false;

		// Converts the value of an array of 8-bit unsigned integers to its 
		// equivalent string representation that is encoded with base-64 digits.
		// The base 64 digits in ascending order from zero are the uppercase characters 
		// 'A' to 'Z', the lowercase characters 'a' to 'z', the numerals '0' to '9', 
		// and the symbols '+' and '/'. The valueless character '=' is used for trailing padding.
		// The input is divided into groups of three bytes (24 bits) each. Consequently, each 
		// group consists of four 6-bit numbers where each number ranges from decimal 0 to 63. 
		// 2^6 = 64. Each 3 bytes in the array will have 4 chars represented in base 64.
		// Ie. {0, 1, 2, 4, 5} = 00000000 00000001 00000010 00000100 00000101 
		// or 000000 000000 000100 000010 000001 000000 0101[00], [zeros within brackets represents a placeholder '=']
		// represented in 6-bit groups. This equates to 004210 in decimal, which is AAECBAU= in base-64.

		// Per the MSDN documentation, UTF8 is faster, more secure, and the
		// preferred method of text encoding/decoding over ASCII.
		// http://msdn.microsoft.com/en-us/library/ms404377.aspx
		// System.Text.ASCIIEncoding enc = new System.Text.ASCIIEncoding();
		// UTF8Encoding utf = new System.Text.UTF8Encoding();

		// Step #1: Regular expression to check if charset is valid (base-64 charset)			
		// Step #2: Iterate through all of the characters:
		//
		// Represents an int?                
		// 
		// A  A  E  C  B  A  U  =
		// 0  0  4  2  1  0  20 ' shift?
		//
		// A          A          E 
		// 0000 0000  0000 0000  0000 0100
		//
		// >>2, <<4, & operator, etc.
		//
		// Convert from char to 6-bit, bit shift 2 left << 2 each padding (doubles the value, and doubles the result), 
		// convert entire array to bytes. 0001 (1) << 2 = 0100 (4).
		//
		// We could do this labor intensitive process, or just run a try {} statement on Convert.FromBase64String().

		// Step #2: Test the decode and see if an exception is thrown.
		bool isMatch = Regex.IsMatch(s, "^([A-Za-z0-9=+/]+)$");

		// Console.WriteLine("IsMatch: " + isMatch.ToString());

		if (isMatch)
		{
			// Attempt to URL decode
			try
			{
				byte[] b = Convert.FromBase64String(s);
				isencoded = true;
				//Console.WriteLine("Valid character string.");
			}
			catch (Exception)
			{
				//Console.WriteLine("Invalid base-64 character string.");
				isencoded = false;
			}
		}
		else
		{
			isencoded = false;
		}

		return isencoded;
	}
}

In this class, the method EncryptStringUsingAES handles encryption and DecryptStringUsingAES. Thanks to System.Security.Cryptography, this process is straight forward and saves time.

Next, we need the actual implementation of the symmetrical algorithm consumed by the EncryptionHelper class. This class is simply called RijndaelAES.

	/// <summary>
	/// A simple AES encryption/Decryption class
	/// </summary>
	public class RijndaelAES
	{
		private byte[] key, vector;
		private ICryptoTransform encryptorTransform, decryptorTransform;
		private System.Text.UTF8Encoding utfEncoder;

		/// <summary>
		/// Initializes the RijndaelAESHelper class. Pronounced 'Rhine Dahl'.
		/// </summary>
		/// <param name="Key">The encryption key.</param>
		/// <param name="Vector">The initialization vector (IV).</param>
		/// <remarks>
		/// About RijndaelManaged():
		/// Managed class for the Rijndael algorithm. 
		/// Fun fact: Assuming that one could build a machine that could recover a DES key in a second 
		/// (i.e., could test 2^55 keys per second) it would take that machine approximately 149 
		/// thousand-billion (149 trillion) years to crack a 128-bit AES key. 
		/// To put that into perspective, the universe is believed to be less than 20 billion years old.
		/// 
		/// About IVs:
		/// Since encryption of the same plain text with the same key results in the same ciphertext,
		/// an initialization vector (IV) is used, which is XORed with the first block of the plaintext.
		/// As long as the IV is different, the same plain text and same algorithm will result in
		/// unique ciphertext. This is especially important when storing ciphertext in a database, so
		/// that deductions cannot be made that user A's password is the same as user B's, since they
		/// would have identical cyphertext if using the same IV. Therefore, each user should have a
		/// different IV.
		/// 
		/// About AES:
		/// AES has three different key lengths: 128, 192, and 256 bits. Cipher block size is 128-bits.
		/// 
		/// </remarks>
		public RijndaelAES(Byte[] key, Byte[] vector)
		{
			this.key = key;
			this.vector = vector;

			RijndaelManaged rm = new RijndaelManaged();

			// Create an encryptor and a decryptor for the cryptostream
			this.encryptorTransform = rm.CreateEncryptor(this.key, this.vector);
			this.decryptorTransform = rm.CreateDecryptor(this.key, this.vector);

			// Used to translate bytes to text and vice versa
			this.utfEncoder = new System.Text.UTF8Encoding();
		}

		/// <summary>
		/// Generates a random 256-bit (32 byte) encryption key for use with the Rijndael algorithm.
		/// </summary>
		/// <returns>byte[] encryption key. Store it some place safe.</returns>
		static public byte[] GenerateKey()
		{
			RijndaelManaged rm = new RijndaelManaged();
			rm.GenerateKey();
			return rm.Key;
		}

		/// <summary>
		/// Generates a random 128-bit (16 byte) encryption vector for use with the Rijndael algorithm.
		/// </summary>
		/// <returns>byte[] vector value</returns>
		static public byte[] GenerateVector()
		{
			RijndaelManaged rm = new RijndaelManaged();
			rm.GenerateIV();
			return rm.IV;
		}
		
		public void ResetIVandKey(Byte[] key, Byte[] vector)
		{
			this.key = key;
			this.vector = vector;

			// Create an encryptor and a decryptor for the cryptostream
			RijndaelManaged rm = new RijndaelManaged();
			this.encryptorTransform = rm.CreateEncryptor(this.key, this.vector);
			this.decryptorTransform = rm.CreateDecryptor(this.key, this.vector);
		}

		/// <summary>
		/// Takes a string and returns encrypted bytes using the Rijndael symmetrical encryption algorithm.
		/// </summary>
		public byte[] Encrypt(string plainText)
		{
			// Byte array to hold the encrypted bytes.
			byte[] encryptedBytes = null;

			// Translates string into a byte array using UTF-8 charset char-byte values.
			Byte[] bytes = this.utfEncoder.GetBytes(plainText);

			//Used to stream the data in and out of the CryptoStream.
			MemoryStream memoryStream = new MemoryStream();

			// Instantiate the cryptostream object, instructing it to write bytes to the memory stream
			// using ICryptoTransform.
			CryptoStream crypto = new CryptoStream(memoryStream, this.encryptorTransform, CryptoStreamMode.Write);

			try
			{
				// Write the unencrypted bytes to the memory stream as encrypted bytes
				crypto.Write(bytes, 0, bytes.Length);
				crypto.FlushFinalBlock();

				// Reset memory stream position
				memoryStream.Position = 0;
				
				encryptedBytes = new byte[memoryStream.Length];

				// Read the encrypted bytes from the memory stream to a byte array
				memoryStream.Read(encryptedBytes, 0, encryptedBytes.Length);
			}
			catch (Exception)
			{
				throw;
			}
			finally
			{
				crypto.Close();
				memoryStream.Close();
			}

			return encryptedBytes;
		}

		/// <summary>
		/// Descryptes the provided byte[] by using the Rijndael algorithm, and convertes the unencrypted bytes 
		/// to a string using UTF-8.
		/// </summary>
		public string Decrypt(byte[] EncryptedBytes)
		{
			Byte[] decryptedBytes = null;
			MemoryStream memoryStream = new MemoryStream();			
			CryptoStream crypto = new CryptoStream(memoryStream, decryptorTransform, CryptoStreamMode.Write);

			try
			{
				// Unencrypt the bytes by writing encrypted bytes to crypostream
				crypto.Write(EncryptedBytes, 0, EncryptedBytes.Length);
				crypto.FlushFinalBlock();

				// Reset memory stream position
				memoryStream.Position = 0;

				decryptedBytes = new Byte[memoryStream.Length];

				// Read the unencrypted bytes from the memory stream and write them to the byte array
				memoryStream.Read(decryptedBytes, 0, decryptedBytes.Length);
			}
			catch (Exception)
			{
				throw;
			}
			finally
			{
				// Per the .NET framework, close methods for streams should always be called in the finally block.
				// This is because some exceptions thrown by the cryptostream class may not close the stream.
				// For example, if the byte length to be decoded is invalid, an exception will be thrown by the
				// FlushFinalBlock() method and the stream will remain open.
				memoryStream.Close();
				crypto.Close();
			}

			// Translate decrypted bytes into UTF-8 charset string
			return this.utfEncoder.GetString(decryptedBytes);
		}
	}

Encryption using Protected Configuration Sections

We need to store the symmetrical encryption keys somewhere, preferrably in the application configuration file. Before we can do this, we need to define a custom configuration class SecretSymmetricKey to hold the key, initialization vector, and a boolean value indicating whether encryption should be enabled or disabled.

public class SecretSymmetricKey : ConfigurationSection
{
	[ConfigurationProperty("Enabled", DefaultValue = "false", IsRequired = false)]
	public bool Enabled
	{
		get
		{
			return (bool)this["Enabled"];
		}
		set
		{
			this["Enabled"] = value;
		}
	}

	[ConfigurationProperty("Key", DefaultValue = "false", IsRequired = true)]
	public string Key
	{
		get
		{
			return (string)this["Key"];
		}
		set
		{
			this["Key"] = value;
		}
	}

	[ConfigurationProperty("IV", DefaultValue = "false", IsRequired = true)]
	public string IV
	{
		get
		{
			return (string)this["IV"];
		}
		set
		{
			this["IV"] = value;
		}
	}
}

Next, we can store the key information in the application's configuration file. We do this by defining a custom configuration section, and then adding the key information to the file. We also add a protected data section, which we will use later to encrypt the symmetrical encryption key so that the key information is not stored in the application configuration file in plaintext. This way, if unauthorized access is gained on the web.config or machine.config, the key and data will remain secret.

<configuration>
	<configSections>
		<sectionGroup name="EncryptionKeys">
			<section name="QuerystringSymmetricKey" type="Demo.Encryption.SecretSymmetricKey, 
					Demo.Encryption, 
					Version=1.0.3529.20938, 
					Culture=neutral, PublicKeyToken=397b97c7ad3ebee8"/>
		</sectionGroup>
	</configSections>
	<configProtectedData>
		<providers>
			<add name="DemoKey" 
				type="System.Configuration.RsaProtectedConfigurationProvider, 
					System.Configuration, Version=2.0.0.0, Culture=neutral, 
					PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" 
					keyContainerName="DemoKey" useMachineContainer="true"/>
		</providers>
	</configProtectedData>
	<EncryptionKeys>
			<QuerystringSymmetricKey
				Enabled="true"
				Key="super_secret_string"
				IV="super_secret_string"
				/>
	</EncryptionKeys>
<configuration>

HttpModule for Handling Request Events

The second and final library for our design is a simple HTTP Module. This module will be used to automatically encrypt and ecrypt sensitive data in HTTP headers such as querystrings and cookies.

Deriving from IHttpModule, the .BeginRequest event and .EndRequest events of the request lifecycle can be intercepted, and cryptography becomes transparent to the application. Requests are automatically decrypted, and responses are encrypted before being sent back to the client over the wire.

namespace Demo.Encryption
{
	public class EncryptionHttpModule : IHttpModule
	{
		public bool IsReusable
		{
			get { return true; }
		}

		public void Init(HttpApplication context)
		{
			// Assign handler for the begin request event
			context.BeginRequest += new EventHandler(this.BeginRequest);
			context.EndRequest += new EventHandler(this.EndRequest);
		}

		public void Dispose(){}

		public void BeginRequest(object sender, EventArgs e)
		{
			ProcessRequest((HttpApplication)sender, true);
		}
		
		public void EndRequest(object sender, EventArgs e)
		{
			ProcessRequest((HttpApplication)sender, false);
		}
		
		public void ProcessRequest(HttpApplication app, bool beginRequest)
		{
			// Perform translation on querystring
			QuerystringTranslator request = new QuerystringTranslator(app, beginRequest);
			request.ProcessRequest();

			// Perform translation on HTTP cookies using a custom configuration section
			CookieTranslator cookies = new CookieTranslator(app, beginRequest);
			cookies.configSectionName = ConfigurationSettings.AppSettings["cookieConfigSectionName"].ToString();
			cookies.ProcessRequest();
		}
	}
}
Here, the ProcessRequest method passes the application context to the appropriate translator for encryption/decryption. Tranlators are explained next.

Building the Translators

For flexibility we create modular "translators" to perform encryption and decryption of sensitive data. The translators are only worried about managing sensitive application data, and consume the SecretSymmetricKey and EncryptionHelper classes shown above.

For example, the QuerystringTranslator is responsible for handling the decryption of querystrings. A beginRequest of true indicates the application is handling the BeginRequest event of an HTTP request, and the querystring should be decrypted. A value of false corresponds to the EndRequest event, and the querystring is encrypted before the response is sent to the client.

using Demo.Encryption;
using System;
using System.Configuration;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;

namespace Demo.Utilities.EncryptionHttpModule
{
	public abstract class RequestTranslator
	{
		protected HttpApplication app;
		protected HttpContext context;
		protected bool beginRequest;
		protected bool translationEnabled;
		protected string key, iv;
		public string configSectionName = string.Empty;
		public bool configSettingsLoaded = false;

		public RequestTranslator(HttpApplication app, bool beginRequest)
		{
			this.app = app;
			this.context = app.Context;
			this.beginRequest = beginRequest;
			this.configSectionName = ConfigurationSettings.AppSettings["defaultConfigSectionName"].ToString();
		}

		/// <summary>
		/// Loads default configuration information for this translator.
		/// </summary>
		/// <remarks>
		/// The key, vector, and bit indicating whether or not to perform translation are 
		/// read from the application configuration file. 
		/// Note: The config file for the application consuming this module module is also required 
		/// to have a name/value key pair indicating configuration section is to be used. 
		/// 
		/// ex: <add key="defaultConfigSection" value="EncryptionKeys/QuerystringSymmetricKey">
		/// 
		/// This method can be overridden to pull a key/value/enabled trio by any other means available.
		/// </remarks>
		protected virtual void LoadConfigSettings()
		{
			if (this.configSectionName == String.Empty)
				throw new ArgumentNullException("this.configSectionName is null.");

			SecretSymmetricKey symkey = (SecretSymmetricKey)System.Configuration.ConfigurationManager.GetSection(this.configSectionName);

			if (symkey != null)
			{
				this.key = symkey.Key;
				this.iv = symkey.IV;
				this.translationEnabled = symkey.Enabled;
			}
			else
			{
				throw new ApplicationException(String.Format("Failed to load section '{0}' of " +
					"application configuration file.", this.configSectionName));
			}
			
			this.configSettingsLoaded = true;
		}
		
		public abstract void ProcessRequest();
	}

	public class QuerystringTranslator : RequestTranslator
	{
		public QuerystringTranslator(HttpApplication app, bool beginRequest) : base(app, beginRequest) {}

		public override void ProcessRequest()
		{
			if (!this.configSettingsLoaded)
				this.LoadConfigSettings();

			if (!this.translationEnabled)
				return;

			// Special note about the .NET Querystring object.
			// The ToString() method of the QueryString class returns the entire raw 
			// encoded querystring. In contrast, the 
			// System.Collections.Specialized.NameValueCollection
			// contains only decoded values. So we need to be careful to make sure
			// to decode the raw string before passing it to the Base64 checking
			// function as well as the decryption function.
			string querystring = this.context.Request.QueryString.ToString();
			
			if (querystring != string.Empty)
			{
				if (base.beginRequest)
				{
					// At the beginning of the HTTP request, we assume check if the querystring
					// is encrypted. If it is, we decrypt it for use by the application.
					//if (EncryptionHelper.IsBase64Encoded(HttpUtility.UrlDecode(querystring)))

					querystring = HttpUtility.UrlDecode(querystring); // Make sure we decode our querystring

					if (EncryptionHelper.IsBase64Encoded(querystring))
					{
						querystring = EncryptionHelper.DecryptStringUsingAES(querystring, this.key, this.iv);
					}
				}
				else
				{
					// At the end of the HTTP request, we automatically encrypt the querystring
					// as defined by our security policy. The user will only see an encrypted 
					// querystring in the HTTP Response Header.
					querystring = EncryptionHelper.EncryptStringUsingAES(querystring, this.key, this.iv);
				}

				// Rewrite path
				this.context.RewritePath(HttpContext.Current.Request.Path + "?" + querystring);
			}
		}
	}

	public class CookieTranslator : RequestTranslator
	{
		public CookieTranslator(HttpApplication app, bool beginRequest) : base(app, beginRequest) {}

		public override void ProcessRequest()
		{
			if (!this.configSettingsLoaded)
				this.LoadConfigSettings();

			if (!this.translationEnabled)
				return;

			if (base.beginRequest)
			{
				if (this.context.Request.Cookies.Count > 0)
				{
					// Automatically decrypt all cookies and invalidate those which are not encrypted
					// or cannot be decrypted using the key provided to this module.
					for (int i = 0; i < context.Request.Cookies.Count; i++)
					{
						try
						{
							if (EncryptionHelper.IsBase64Encoded(this.context.Request.Cookies[i].Value))
							{
								this.context.Request.Cookies[i].Value = EncryptionHelper.DecryptStringUsingAES(
									this.context.Request.Cookies[i].Value,
									this.key,
									this.iv);
							}
							else
							{
								// Unencrypted cookies are not allowed
								this.context.Request.Cookies.Remove(this.context.Request.Cookies[i].Name);
								i--;
							}
						}
						catch (Exception)
						{
							// Remove corrupt or invalid cookie
							this.context.Request.Cookies.Remove(this.context.Request.Cookies[i].Name);
							i--;
						}
					}
				}
			}
			else
			{
				// Encrypt all cookies we are sending to the client
				if (this.context.Response.Cookies.Count > 0)
				{
					for (int i = 0; i < context.Response.Cookies.Count; i++)
					{
						this.context.Response.Cookies[i].Value = EncryptionHelper.EncryptStringUsingAES(
							this.context.Response.Cookies[i].Value,
							this.key,
							this.iv);
					}
				}
			}
		}
	}
}

Since the LoadConfigSettings method of the abstract class RequestTranslator is marked as virtual, it can be overridden to use any custom configuration section, rather than the default.

For example, say we want to the querystring symmetric key for security of the querystring and all other data, while using a separate key for cookies. In our application configuration file we can specify this using an app setting:

<appSettings>
	<add key="defaultConfigSectionName" value="EncryptionKeys/QuerystringSymmetricKey" />
	<add key="cookieConfigSectionName" value="EncryptionKeys/CookieSymmetricKey" />
</appSettings>

In our EncryptionHttpModule above we can then specify the custom configuration section as follows:

// Perform translation on HTTP cookies using a custom configuration section
CookieTranslator cookies = new CookieTranslator(app, beginRequest);
cookies.configSectionName = ConfigurationSettings.AppSettings["cookieConfigSectionName"].ToString();
cookies.ProcessRequest();

Once our module is built, the application configuration file should be modified as follows:

<configuration>
	<system.web>
		<httpMmodules>
			<add type="Demo.Encryption.EncryptionHttpModule, Demo.Encryption.EncryptionHttpModule" name="EncryptionHttpModule" />
		</httpMmodules>
	</system.web>	
</configuration>	

This will pass all requests to the EncryptionHttpModule, which will perform the translation for the HttpApplication object.

A Special Note About Form Postbacks

Up to this point, our automatic encryption and decryption of querystrings is seamless to the ASP.NET application. However, you will soon find that after your first postback the cleartext URL will be plainly visible in the browser's URL window.

How did this happen? Is this a bug? Actually it isn't; those of you who have had to deal with Url rewriting are likely to have encountered this situation before.

The problem arises from the rendering of the <form> element by ASP.NET, which inserts the postback Url, including the querystring that is now stored in cleartext by the Request object.

<form name="form1" method="post" action="/mypage.aspx?favorite+episode=encounter+at+farpoint" id="form1">
There are several ways to the postback problem [2], such as building a custom form control and changing your <asp:form> references, but my preferred choice is to use Control Adapters because no changes to existing ASP.NET web pages is required.

Control Adapters

"These adapters take advantage of a new extensibility feature in ASP.NET 2.0 that we call the 'Control Adapter Architecture', and which enables developers to override, modify and/or tweak the rendering output logic of an existing server control (without changing any of its properties, supported events, or programming model)."

namespace Demo.Encryption
{
	public class FormRewriterControlAdapter : ControlAdapter
	{
		protected override void Render(HtmlTextWriter writer)
		{
			base.Render(new RewriteFormHtmlTextWriter(writer));
		}
	}

	public class RewriteFormHtmlTextWriter : HtmlTextWriter
	{
		// Subsclass constructor
		public RewriteFormHtmlTextWriter(HtmlTextWriter writer)
			: base(writer) // call constructor in base class (ControlAdapter), passing the form control
		{
			base.InnerWriter = writer; // set base class html text writer 
		}

		public override void WriteAttribute(string name, string value, bool fEncode)
		{
			if (name.ToLower() == "action") // Look for the postback attribute of the form element
			{
				value = HttpContext.Current.Request.RawUrl;
			}

			base.WriteAttribute(name, value, fEncode);
		}
	}
}

Next, a .broswer file must be added to your web application. This file instructs ASP.NET to overwride the WriteAttribute when HtmlForm elements are rendered, giving us the opportunity to insert the raw unencrypted Url into the form element for secured postbacks.

<browsers>
	<browser refID="Default">
		<controlAdapters>
			<adapter controlType="System.Web.UI.HtmlControls.HtmlForm"
               adapterType="Demo.Encryption.FormRewriterControlAdapter" />
		</controlAdapters>
	</browser>

</browsers>

Securing Cookies

Cookies can often times contain sensitive information, such as usernames or shopping cart idenftifiers for return visits to websites. Its arguably bad practice to store web passwords in cookies, but much like the 4th slice of pumpkin pie on Thanksgiving, it happens.

Storing such sensitive information in cleartext can be less than desirable--for example, you wouldn't want malware on an office PC sending cookies.txt containing cleartext login credentials for the Intranet along with the browser history to a data dump in Crackerville, USA.

Interestingly, .NET has an obscure HttpCookie.Secure property. By setting the .Secure property of an HttpCookie to true, .NET will flag set-cookie as "secure". At first glance, one would think that this cookie would be encrypted to maintain its security. However, set-cookies flagged as "secure" are handled according to RFC 2109, indicating that it is in the user agent's (read browser's) interest to "protect" the cookie. What exactly "protecting" the cookie means is left up to the implementation of the user agent. Firefox for example will only send secure cookies over an SSL connection[1]. However, the cookie is stored in cleartext on the client's machine by Firefox, and the set-cookie is initially sent over the wire in cleartext as part of the HTTP Response from IIS:

Set-Cookie	enterprise_master_password=ecounterNC1701D; path=/; secure

Rather than leaving the security of the cookie up to the implementation of the user agent, secure cookies can also be encrypted using the BeginRequest and EndRequest events in the Http Module we have already created. Cookies are automatically decrypted during the BeginRequest event for use by the HttpApplication, and set-cookies sent in the response are automatically encrypted during the EndRequest before they are sent over the wire to the client.

Set-Cookie	enterprise_master_password=BFTegpiOwOs1QjhI2oo5eDbNRIRUjLdcdcOYxg==; path=/; secure

Deployment

Before deployment we need to consider scope requirements. One of the big benefits of the module design we have outlined so far is scalability. We can use transparent encryption for either a single application, the entire server, or all servers on a web farm. Since deployment for a single web application is the easiest, we'll take a look at that method first. Later, we will analyze the special requirements for use at the machine level and web farms.

The first thing you will need to do is generate an RSA public/private key pair. RSA keys over 512 bits require the Microsoft Enhanced Cryptographic Provider to be installed, which most boxes have now. This key will be used to encrypt the sensitive symmetric key information stored in the application's configuration file. Without this step, the symmetric key will be exposed in plaintext, and anyone who has access to the source code server, deployment folder, or who may be sniffing the network during staging would be able to compromise the security of the application.

Asymmetrical RSA keys can be generated using aspnet_regiis or programmatically via the System.Security.Cryptography namespace in the .NET framework.

Using aspnet_regiis

rem Navigate to your .NET folder, ie. "C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_regiis.exe"

rem Create a key called "DemoKey" and mark it for exporting
aspnet_regiis -c "DemoKey" -exp

rem Export the key in xml and give to your sysadmin
aspnet_regiis -px "DemoKey" "c:\DemoKey.xml" -pri

rem Note the key container is stored on disk for XP and 2000 and up machines at: 
rem C:\Documents and settings\All Users\Application Data\Microsoft\Crypto\RSA\Machinekeys

Using System.Security.Cryptography

// Microsoft Enhanced Cryptographic Provider is required to generate keys over 512 bits in length.
using (System.Security.Cryptography.RSACryptoServiceProvider RSA = new System.Security.Cryptography.RSACryptoServiceProvider(1024))
{
	Console.WriteLine("Private key: \r\n" + RSA.ToXmlString(true)); // Show private key
	Console.WriteLine("Public key: \r\n" + RSA.ToXmlString(false)); // Show public key
}

Importing the RSA Public/Private Key Pair

Next you will want to import the key into the cryptoservice provider key database on the server(s) and workstation(s) your application will be running on.

To the best of my knowledge, keys must be imported using aspnet_regiis. If anyone knows how to do this programmatically I would appreciate you letting me know.

@echo off
rem This needs to be done on each server performing encryption/decryption:

echo importing RSA private key into key container
"C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_regiis.exe" -pi DemoKey DemoKey-Private.xml

echo Granting key container access to network service
"C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_regiis.exe" -pa "DemoKey" "NT AUTHORITY\NETWORK SERVICE"

pause

Next, the application's configuration file can be modified to specify a protected configuration provider for protected sections, and we add our custom configuration section with our super secret symmetric key information. Note that we can put as many keys as we want into this section--one for querystrings, another for cookies, another for user passwords, or any other specific uses.

<configuration>
	<configSections>
		<sectionGroup name="EncryptionKeys">
			<section name="QuerystringSymmetricKey" type="Demo.Encryption.SecretSymmetricKey, 
					Demo.Encryption, 
					Version=1.0.1234.54321, 
					Culture=neutral, PublicKeyToken=000b97c7ad3ebdd8"/>
		</sectionGroup>
	</configSections>
	<configProtectedData>
		<providers>
			<add name="DemoKey" 
				type="System.Configuration.RsaProtectedConfigurationProvider, 
					System.Configuration, Version=2.0.0.0, Culture=neutral, 
					PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" 
					keyContainerName="DemoKey" useMachineContainer="true"/>
		</providers>
	</configProtectedData>
	<EncryptionKeys>
			<QuerystringSymmetricKey
				Enabled="true"
				Key="super_secret_string"
				IV="super_secret_string"
				/>
	</EncryptionKeys>
<configuration>

Then we can use aspnet_regiis to encrypt the custom configuration section holding our super secret symmetric key information. However, before we can do this, we need to add our signed Demo.Encryption library with the Demo.Encryption.SecretSymmetricKey class definition to the GAC. The assembly must be added to the GAC for use aspnet_regiis.exe as well as machine.config later on. If you recall, either gacutil.exe or windows explorer can be used to add files to the GAC. For a refresher on assembly signing and adding assemblies to the GAC, see Adding Assemblies to GAC at devhood.

"C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_regiis.exe" -pef
EncryptionKeys/QuerystringSymmetricKey "C:\myweb" -prov "DemoKey".

Note we use the -prov option to specify our custom configuration provider.

<configuration>
   <EncryptionKeys>
	   <QuerystringSymmetricKey configProtectionProvider="DemoKey">
		   <EncryptedData Type="http://www.w3.org/2001/04/xmlenc#Element"
			xmlns="http://www.w3.org/2001/04/xmlenc#">
				<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc" />
				<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
				 <EncryptedKey xmlns="http://www.w3.org/2001/04/xmlenc#">
				  <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5" />
				  <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
				   <KeyName>Rsa Key</KeyName>
				  </KeyInfo>
				  <CipherData>
					<CipherValue>somegobblygook=</CipherValue>
				  </CipherData>
				 </EncryptedKey>
				</KeyInfo>
				<CipherData>
					<CipherValue>somemoregobblygook==</CipherValue>
				</CipherData>
		   </EncryptedData>
	  </QuerystringSymmetricKey>
</configuration>
Now that the plaintext has been replaced with ciphertext, our configuration file is secure for deployment.

Modifying the Machine.Config

One of the benefits of using Http Modules is that automatic encryption and decryption can be applied at the machine level. Once deployed, all .NET websites on the server will feature the enhanced security benefits. Note that .NET 1.1 uses a different machine.config file than .NET 2.0-3.5, so you may need to modify both machine.config files if you are running both versions of the CLR. It looks as though the upcoming .NET 4.0 will also have its own machine.config since it is using a different CLR version [3].

To begin, locate the machine.config file in the \CONFIG folder of the .NET framework that you are using. It is a good idea to back this file up before you modify it.

// Note: .NET versions 2.0, 3.0, and 3.5 use the same config folder.
C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\CONFIG\machine.config

At this point you will want to make the same changes to the machine.config as you did to the web.config.

Add the configuration section group:
<configuration>
	<configSections>
		<sectionGroup name="EncryptionKeys">
			...
	</configSections>
</configuration>

When adding the protected configuration provider, take note that the RsaProtectedConfigurationProvider and DataProtectionConfigurationProvider providers are already in use by default. Just add the new provider below them:

Add the protected configuration provider:
	<configProtectedData defaultProvider="RsaProtectedConfigurationProvider">
		<providers>
			<add name="RsaProtectedConfigurationProvider" .......
			<add name="DataProtectionConfigurationProvider" .......
			<add name="DemoKey"
				 type="System.Configuration.RsaProtectedConfigurationProvider, 
					System.Configuration, Version=2.0.0.0,
					Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
					processorArchitecture=MSIL"
		</providers>
	</configProtectedData>
Add the encryption key information:
<configuration>
   <EncryptionKeys>
	   <QuerystringSymmetricKey configProtectionProvider="DemoKey">
		   <EncryptedData.....
		   </EncryptedData>
	  </QuerystringSymmetricKey>
</configuration>
Add the HTTP Module:
<configuration>
	<system.web>
		<httpMmodules>
			<add type="Demo.Encryption.EncryptionHttpModule, Demo.Encryption.EncryptionHttpModule" name="EncryptionHttpModule" />
		</httpMmodules>
	</system.web>	
</configuration>	

Finally you will want to add a setting for defaultConfigSectionName as follows:

<configuration>
	<appSettings>
         <add key="defaultConfigSectionName" value="EncryptionKeys/QuerystringSymmetricKey" />
	</appSettings>
</configuration>

Web Farm Deployment

References

  1. FireFox - Using Privacy Features
  2. ScottGu's Blog
  3. Clean Web.Config Files
  1. Extreme ASP.NET: Control Adapters
  2. Using Tag Mapping to Fix the Form Control for URL Rewriting

History

  1. v1.0 - Written Oct 8th, 2009.
  2. v1.1 - Published Feb 4, 2010.



©Copyright 2009 Matt Kellerman. Enjoyed this site? Send Me an Email