When passing variables between pages in ASP.NET, you have a few techniques you can choose from. One of the simplest is to use query arguments (e.g. http://www.domain.com/page.aspx?arg1=val1&arg2=val2). In ASP.NET, query arguments are easy to implement and use.
If you spend time browsing sites like Amazon.com, you'll see these query arguments causing the URLs to grow quite long. Long URLs don't generally cause a problem; however, there are some potential problems with query arguments.
For one thing, they are completely visible to the user. If you need to pass sensitive variables, then this could cause problems. For another thing, users can easily modify these values. For example, let's say you have a page that displays the current user's information. If a user ID is passed as a query argument, the user could easily edit that ID, possibly causing information for another user to be displayed. The potential security concerns here are pretty obvious.
Still, query arguments can be so convenient that I decided to throw together a class that allows me to use them without the potential issues described above. In order to prevent the arguments from being seen by the user, the arguments are encrypted into a single argument. And in order to prevent the user from tampering with the values, the encrypted value includes a checksum that can detect if the data has been tampered with or corrupted.
Listing 1 shows my EncryptedQueryString
class. By inheriting from Dictionary<string, string>
, my class is a dictionary
class. You can add any number of key/value items to the dictionary
and then call ToString()
to produce an encrypted string
that contains all the values and a simple checksum. The string
returned can then be passed to a page as a single query argument.
To restore the values, you can call the constructor that accepts an encrypted string
. This constructor extracts the data from the encrypted string
and adds it to the dictionary
. Note that if this constructor finds an invalid or missing checksum, nothing is added to the dictionary
. This prevents the calling code from working with questionable data.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Web;
public class EncryptedQueryString : Dictionary<string, string>
{
protected byte[] _keyBytes =
{ 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18 };
protected string _keyString = "ABC12345";
protected string _checksumKey = "__$$";
public EncryptedQueryString()
{
}
public EncryptedQueryString(string encryptedData)
{
string data = Decrypt(encryptedData);
string checksum = null;
string[] args = data.Split('&');
foreach (string arg in args)
{
int i = arg.IndexOf('=');
if (i != -1)
{
string key = arg.Substring(0, i);
string value = arg.Substring(i + 1);
if (key == _checksumKey)
checksum = value;
else
base.Add(HttpUtility.UrlDecode(key), HttpUtility.UrlDecode(value));
}
}
if (checksum == null || checksum != ComputeChecksum())
base.Clear();
}
public override string ToString()
{
StringBuilder content = new StringBuilder();
foreach (string key in base.Keys)
{
if (content.Length > 0)
content.Append('&');
content.AppendFormat("{0}={1}", HttpUtility.UrlEncode(key),
HttpUtility.UrlEncode(base[key]));
}
if (content.Length > 0)
content.Append('&');
content.AppendFormat("{0}={1}", _checksumKey, ComputeChecksum());
return Encrypt(content.ToString());
}
protected string ComputeChecksum()
{
int checksum = 0;
foreach (KeyValuePair<string, string> pair in this)
{
checksum += pair.Key.Sum(c => c - '0');
checksum += pair.Value.Sum(c => c - '0');
}
return checksum.ToString("X");
}
protected string Encrypt(string text)
{
try
{
byte[] keyData = Encoding.UTF8.GetBytes(_keyString.Substring(0, 8));
DESCryptoServiceProvider des = new DESCryptoServiceProvider();
byte[] textData = Encoding.UTF8.GetBytes(text);
MemoryStream ms = new MemoryStream();
CryptoStream cs = new CryptoStream(ms,
des.CreateEncryptor(keyData, _keyBytes), CryptoStreamMode.Write);
cs.Write(textData, 0, textData.Length);
cs.FlushFinalBlock();
return GetString(ms.ToArray());
}
catch (Exception)
{
return String.Empty;
}
}
protected string Decrypt(string text)
{
try
{
byte[] keyData = Encoding.UTF8.GetBytes(_keyString.Substring(0, 8));
DESCryptoServiceProvider des = new DESCryptoServiceProvider();
byte[] textData = GetBytes(text);
MemoryStream ms = new MemoryStream();
CryptoStream cs = new CryptoStream(ms,
des.CreateDecryptor(keyData, _keyBytes), CryptoStreamMode.Write);
cs.Write(textData, 0, textData.Length);
cs.FlushFinalBlock();
return Encoding.UTF8.GetString(ms.ToArray());
}
catch (Exception)
{
return String.Empty;
}
}
protected string GetString(byte[] data)
{
StringBuilder results = new StringBuilder();
foreach (byte b in data)
results.Append(b.ToString("X2"));
return results.ToString();
}
protected byte[] GetBytes(string data)
{
byte[] results = new byte[data.Length / 2];
for (int i = 0; i < data.Length; i += 2)
results[i / 2] = Convert.ToByte(data.Substring(i, 2), 16);
return results;
}
}
Listing 1: EncryptedQueryString class.
So, for example, a page that sends encrypted arguments to another page could contain code something like what is shown in Listing 2. This code constructs an empty EncryptedQueryString
object, adds a couple of values to the dictionary
, and then passes the resulting string
as a single query argument to page.aspx.
protected void Button1_Click(object sender, EventArgs e)
{
EncryptedQueryString args = new EncryptedQueryString();
args["arg1"] = "val1";
args["arg2"] = "val2";
Response.Redirect(String.Format("page.aspx?args={0}", args.ToString()));
}
Listing 2: Code that passes encrypted query arguments.
Finally, Listing 3 shows code that could go in page.aspx to extract the encrypted values from the single argument.
protected void Page_Load(object sender, EventArgs e)
{
EncryptedQueryString args =
new EncryptedQueryString(Request.QueryString["args"]);
Label1.Text = string.Format("arg1={0}, arg2={1}", args["arg1"], args["arg2"]);
}
Listing 3: Code to extract encrypted query arguments.
And that's all there is to it. Be sure to add error checking in case the dictionary
objects are not there (either because they were not provided, or because an invalid checksum caused the EncryptedQueryString
class to clear all items from the dictionary
).
Also, be sure to customize the two keys near the top of Listing 1 so that people who read this article won't be able to decrypt your values.
Query arguments aren't always the best choice. As mentioned, you may choose to use Session
variables or other techniques, depending on your requirements. But query arguments are straight forward and easy to implement. Using the class I've presented here, they can also be reasonably secure.