Login


Encrypting Query Arguments

By Jonathan Wood on 12/1/2010 (Updated on 12/12/2010)
Language: C#
Technology: ASP.NET
Platform: Windows
License: CPOL
Views: 22,538
Web Development » Security » General » Encrypting Query Arguments

Demo Program Screenshot

Download Source Code Download Source Code

Introduction

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.

Encrypting Query Arguments

Still, query arguments can be so convenient 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.

Listing 1: EncryptedQueryString class

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>
{
  // Change the following keys to ensure uniqueness
  // Must be 8 bytes
  protected byte[] _keyBytes =
    { 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18 };

  // Must be at least 8 characters
  protected string _keyString = "ABC12345";

  // Name for checksum value (unlikely to be used as arguments by user)
  protected string _checksumKey = "__$$";

  /// <summary>
  /// Creates an empty dictionary
  /// </summary>
  public EncryptedQueryString()
  {
  }

  /// <summary>
  /// Creates a dictionary from the given, encrypted string
  /// </summary>
  /// <param name="encryptedData"></param>
  public EncryptedQueryString(string encryptedData)
  {
    // Descrypt string
    string data = Decrypt(encryptedData);

    // Parse out key/value pairs and add to dictionary
    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));
      }
    }

    // Clear contents if valid checksum not found
    if (checksum == null || checksum != ComputeChecksum())
      base.Clear();
  }

  /// <summary>
  /// Returns an encrypted string that contains the current dictionary
  /// </summary>
  /// <returns></returns>
  public override string ToString()
  {
    // Build query string from current contents
    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]));
    }

    // Add checksum
    if (content.Length > 0)
      content.Append('&');
    content.AppendFormat("{0}={1}", _checksumKey, ComputeChecksum());

    return Encrypt(content.ToString());
  }

  /// <summary>
  /// Returns a simple checksum for all keys and values in the collection
  /// </summary>
  /// <returns></returns>
  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");
  }

  /// <summary>
  /// Encrypts the given text
  /// </summary>
  /// <param name="text">Text to be encrypted</param>
  /// <returns></returns>
  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;
    }
  }

  /// <summary>
  /// Decrypts the given encrypted text
  /// </summary>
  /// <param name="text">Text to be decrypted</param>
  /// <returns></returns>
  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;
    }
  }

  /// <summary>
  /// Converts a byte array to a string of hex characters
  /// </summary>
  /// <param name="data"></param>
  /// <returns></returns>
  protected string GetString(byte[] data)
  {
    StringBuilder results = new StringBuilder();

    foreach (byte b in data)
      results.Append(b.ToString("X2"));

    return results.ToString();
  }

  /// <summary>
  /// Converts a string of hex characters to a byte array
  /// </summary>
  /// <param name="data"></param>
  /// <returns></returns>
  protected byte[] GetBytes(string data)
  {
    // GetString() encodes the hex-numbers with two digits
    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;
  }
}

Using the 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.

Listing 2: Code that passes encrypted query arguments

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()));
}

Finally, Listing 3 shows code that could go in page.aspx to extract the encrypted values from the single argument.

Listing 3: Code to extract encrypted query arguments

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"]);
}

Conclusion

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.

End-User License

Use of this article and any related source code or other files is governed by the terms and conditions of The Code Project Open License.

Author Information

Jonathan Wood

I'm a software and website developer working out of the greater Salt Lake City area of Utah. I've developed many websites including Black Belt Coder, Trail Calendar, and others.

I hike each week with my dogs Suki and Sasha. You can see my hiking blog at Hiking Salt Lake.