TexasJetter
.NET Development Examined

Encrypt C# Object


Wednesday, December 30, 2015

One of the things I've been doing a lot of lately is taking a C# class and encrypting it into a string suitable for storage as a cookie. Typically the process has been to write a helper method to encrypt and another to decrypt for each class object. The process is fairly simple, build a string representation of the class then encrypt it. It looks something like this:

public static byte[] PrivateKey = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24 };
public static string EncryptCustomer(Customer customer)
{
	if (customer == null){ return string.Empty;	}
	var customerString = new StringBuilder();
	customerString.AppendFormat("|FirstName:{0}", customer.FirstName);
	customerString.AppendFormat("|LastName:{0}", customer.LastName);
	...
	return SecurityHelper.EncryptString(customerString.ToString(), PrivateKey);
}

To get the encrypted string back to a class we first decrypted the string, then parsed the array. That typically looked like this:

public static Customer DecryptCustomer(string encryptedString)
{
	var customer = new Customer();
	if (string.IsNullOrEmpty(encryptedString)){	return customer;}
	var decryptedString = SecurityHelper.DecryptString(encryptedString, PrivateKey);
	var filterArray = decryptedString.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
	foreach (var item in filterArray)
	{
		var iArray = item.Split(':');
		if (!iArray.Any()) continue;
		if (iArray[0] == "FirstName") { customer.FirstName = iArray[1]; continue; }
		if (iArray[0] == "LastName") { customer.LastName = iArray[1]; continue; }
		...
	}
	return accessFilterInformation;
}

This worked fine, but there are a few problems. On any given project it was necessary to keep the encrypting and decrypting methods in sync with respect to the items being stored. So if you added a property to the class it was necessary to update the class, the encryption method to serialize it, and the decryption method to de-serialize it. Not a herculean task, but one of those task the can cause a great deal of ... um, mystery :) if not performed.

Additionally the methods have to be written for each project. While typically this involved a simple copy paste/modify its still a tedious task. After the third project requiring this I decided it was worth the effort to create a generic encrypt method that would accept in a class object and perform the encryption of all the properties automatically. To complement this method a generic decrypt method that accepted the encrypted string and returned a class object would be perfect.

The thought was simple, when encrypting simply iterate the supplied object's properties and build the string. That ended up something like this:

public static string EncryptObject(object obj)
{
	var t = obj.GetType();
	var properties = t.GetProperties();
	foreach (PropertyInfo property in properties)
	{
		informationString = informationString + "|" + property.Name + ";" + property.GetValue(obj);
	}
	return SecurityHelper.EncryptString(informationString, PrivateKey); 
}

While that worked I wasn't too happy with the code. It just didn't feel right, but I had uncovered another problem. Once the encrypted string was sent to the decryption method how would we know what type the object was? I considered making the decryption method require the user to pass in the type of object they expected. While that worked I really wanted just a simple one parameter method.

The solution was to grab the object's name and put it in the serialized string. This meant that the encrypted string was self contained and had everything necessary to decrypt back to an object. So this code was added:

public static string EncryptObject(object obj)
{
	var t = obj.GetType();
	if (t.AssemblyQualifiedName == null){ return string.Empty;}
	var objName = t.AssemblyQualifiedName
	      .Substring(0, t.AssemblyQualifiedName.IndexOf(",", StringComparison.Ordinal));
	var informationString = "ObjectName;" + objName;
	var properties = t.GetProperties();
	foreach (PropertyInfo property in properties)
	{
		informationString = informationString + "|" + property.Name + ";" + property.GetValue(obj);
	}
	return SecurityHelper.EncryptString(informationString, PrivateKey); 
}

Next I turned to the decryption. This turned out to be tricky, as you had to find the object type from it's string name, create an instance of the object, decrypt the string, parse the array, and finally assign the array item to the class property. It took a lot of research, but here is what ended up working:

public static object DecryptObject(string encryptedString)
{
	if (string.IsNullOrEmpty(encryptedString)) { return null; }
	var decryptString = SecurityHelper.DecryptString(encryptedString, PrivateKey);
	var infoArray = decryptString.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
	var objNameItem = infoArray[0].Split(';');
	var fullyQuallifiedClassName = objNameItem[1];
	object obj = null;
	//see if the request class is in the current assembly
	var type = Type.GetType(fullyQuallifiedClassName);
	if (type != null)
	{ obj = Activator.CreateInstance(type);	}
	else
	{
		//if not look in all the assemblies currently loaded
		foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
		{
			type = asm.GetType(fullyQuallifiedClassName);
			if (type != null) obj = Activator.CreateInstance(type);
		}
	}
	if (obj != null)
	{
		foreach (var item in infoArray)
		{
			var iArray = item.Split(';');
			if (!iArray.Any()) continue;
			var prop = obj.GetType().GetProperty(iArray[0]);
			if (prop != null)
			{
				switch (prop.PropertyType.Name)
				{
					case "Guid":
						Guid gValue;
						Guid.TryParse(iArray[1], out gValue);
						prop.SetValue(obj, gValue);
						break;
					case "int":
						int iValue;
						int.TryParse(iArray[1], out iValue);
						prop.SetValue(obj, iValue);
						break;
					case "DateTime":
						DateTime dtValue;
						DateTime.TryParse(iArray[1], out dtValue);
						prop.SetValue(obj, dtValue);
						break;
					case "Boolean":
						bool bValue;
						bool.TryParse(iArray[1], out bValue);
						prop.SetValue(obj, bValue);
						break;
					default:
						prop.SetValue(obj, iArray[1]);
						break;
				}
			}
		}
	}
	return obj;
}

So at this point the code was basically working and could replace the existing custom methods with a generic equivalent. However I didn't like the nasty switch necessary to map the string representation of a type to the a strongly typed property. Additionally I realized that this only worked for properties with known types (i.e. string, int, ect.). If the property was a custom class all bets were off. Also custom code for dealing with properties that were a list of items had to be custom coded. While not part of my initial requirements this was a great chance to enhance the capabilities of the system.

I briefly considered some sort of recursion that would detect if a property was a custom type and if so run the same serialization code. This would also mean dealing with it on the decryption side. While technically feasible, it just didn't sound like a lot of fun. After a bit of complementation I realized there were already some good serialization helpers out there. So instead of re-inventing the wheel I decided to use JSON serialization. It so happens that the CODE Framework has some built in JSON serialization (currently based on a fork of NewtonSoft). This greatly simplified my code and solved all concerns about complex property types and properties that are lists of items, what more could you ask for?

The final encryption code ended up like this:

public static byte[] PrivateKey = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24 };
public static bool InTestMode = false;
public static string EncryptObjectJson(object obj)
{
	var type = obj.GetType();
	if (type.AssemblyQualifiedName == null) { return string.Empty; }
	//get the objects class name and stuff it into the custom string object
	var objClassName = type.AssemblyQualifiedName.Substring(0, type.AssemblyQualifiedName.IndexOf(",", StringComparison.Ordinal));
	var informationString = "ObjectName:" + objClassName + "|ObjectValue:" + JsonConvert.SerializeObject(obj);
	return InTestMode ? informationString : SecurityHelper.EncryptString(informationString, PrivateKey); 
}

And the decryption code is shown here:

public static object DecryptObjectJson(string encryptedString)
{
	if (string.IsNullOrEmpty(encryptedString)) { return null; }
	var decryptString = InTestMode ? encryptedString : SecurityHelper.DecryptString(encryptedString, PrivateKey);

	//expected string in syntax of decryptString: ObjectName:"class name as string"|ObjectValue:"{JSON data}"
	var infoArray = decryptString.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
	//the first key/pair is the class name
	var objNameItemArray = infoArray[0].Split(':');
	var fullyQuallifiedClassName = objNameItemArray[1];
	
	//see if the request class is in the current assembly
	var type = Type.GetType(fullyQuallifiedClassName);
	//if not look in all the assemblies currently loaded
	if (type == null)
	{
		foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
		{
			if(asm.GetType(fullyQuallifiedClassName) != null) { type = asm.GetType(fullyQuallifiedClassName);}
		}
	}
	if (type == null){ return null; }
	var objJson = infoArray[1].Substring(infoArray[1].IndexOf(":", StringComparison.Ordinal) + 1);
	return JsonConvert.DeserializeObject(objJson, type);
}

Note that in addition to the greatly simplified serialization there is also a 'InTestMode' property. If the users sets this to 'true' then the encryption will be bypassed and the object will be serialized to clear text. I found this to be a handy feature when trying to determine the values contained in the encrypted strings.

As a final note, this is using the CODE Framework Core.Utilities and Core.Newtonsoft namespaces for the SecurityHelper.EncryptString/DecriptString and JsonConvert.SerializeObject/DecryptObject functions. It would be simple to write your own encryption helper to replace the SecurityHelper, and import NewtonSoft directly if you wanted a version not dependent on the CODE Framework.


Comments

Add Comment