.NET developers are familiar with using serialized DataSets to store local application data; this technique is used extensively in the WinForms development arena. The problems with this method are that anyone with access to the file can easily read the data and possibly even write to the file. The application would pick up the modified data and treat it as valid.
These issues caused me to see how I could secure local data held in serialized DataSets. There are many options for securing the data — from encrypting each field to hashing the data and comparing hashes when the data is deserialized. The most direct approach is to encrypt or decrypt the serialized DataSet before writing to it or after reading it from disk. This method provides a suitable amount of security and protects against anyone tampering with the file.
Serializing and encrypting the DataSet
The first step to encrypting the DataSet is to serialize it in memory. To do this, I will use the XmlSerializer object to serialize the DataSet into a StringWriter object. The StringWriter object allows you to treat a StringBuilder as if it was a normal Stream. This is what gives you the ability to serialize the DataSet in memory without writing it to disk.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public void WriteEncryptedDataSet(string filepath, DataSet set) StringBuilder xmlData= new StringBuilder(); using (StringWriter writer= new StringWriter(xmlData)) { set.writeXml(writer); } string encryptedData = EncryptedString(myKey, xmlData.ToString()); using (StreamWriter writer= new StreamWriter(filepath)) { writer.Write(encryptedData); } |
In the code, there’s a call to the EncryptString function after the DataSet is serialized. This function is a wrapper around the DES encryption mechanisms that are included with .NET. EncryptString takes a plain-text string of data (along with a key value) and returns a string that has been DES encrypted using the supplied key value. The code for EncryptString is below
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
public static string EncryptString(string key, string data) { if (key.Length != 8) throw new Exception ("Key Length must equal to 8"); if (data.Length<=0) throw new Exception ("Length of string to encrypt should be greater than zero"); byte[] inputByteArray = new Byte[data.Length]; byte[] keyByteArray = new Byte[key.Length]; string result =""; DESCryptoServiceProvider des = new DESCryptoServiceProvider(); using(MemoryStream ms = new MemoryStream()) { keyByteArray = Encoding.UTF8.GetBytes(key); inputByteArray = Encoding.UTF8.GetBytes(data); using(CryptoStream cs = new CryptoStream(ms, des.CreateEncryptor(keyByteArray, inputVector), CryptoStreamMode.Write)) { cs.Write(inputByteArray, 0, inputByteArray.Length); cs.FlushFinalBlock(); } result = Convert.ToBase64String(ms.ToArray()); } return result; } |
After the EncryptString function returns the encrypted data, the StreamWriter object is used to write the data to disk. At this point, the data is securely on the local disk and ready to be decrypted and read. Since the data is encrypted, anyone opening the file will see a jumble of characters and will not be able to get information out of the file or write usable information to the file.
Deserializing and decrypting the DataSet
Reading the encrypted DataSet is just as easy as writing it, except it is done in the opposite order. First, the encrypted data is read from disk using the StreamReader object as shown below
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
private string myKey = "password"; private static byte[] inputVector = (0x12, 0x36, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF); public DataSet ReadEncryptedDataSet(string filepath) { DataSet set = new DataSet(); string encryptedData = ""; using(StreamReader reader = new StreamReader(filepath)) { encryptedData = reader.ReadToEnd(); } string decryptedData = DecryptedString(myKey, encryptedData); set.ReadXml(new StringReader(decryptedData)); return set; } |
After the data is read, DecryptString is called. This function decrypts the data sent into it using the “key” parameter as the decryption key. If the key that is sent into DecryptString does not match the key that was used when EncryptString was called, the data will successfully decrypt. Following code shows the code for DecryptString.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public static string DecryptString(string key, string data) { if (key.Length != 8) throw new Exception ("Key Length must equal to 8"); if (data.Length<=0) throw new Exception ("Length of string to encrypt should be greater than zero"); byte[] inputByteArray = new Byte[data.Length]; byte[] keyByteArray = new Byte[key.Length]; string result =""; DESCryptoServiceProvider des = new DESCryptoServiceProvider(); using(MemoryStream ms = new MemoryStream()) { keyByteArray = Convert.FromBase64String(data); inputByteArray = Encoding.UTF8.GetBytes(key); using(CryptoStream cs = new CryptoStream(ms, des.CreateDecryptor(keyByteArray, inputVector), CryptoStreamMode.Write)) { cs.Write(inputByteArray, 0, inputByteArray.Length); cs.FlushFinalBlock(); } result = Encoding.UTF8.GetString(ms.ToArray()); } return result; } |
After the data is decrypted, an XmlSerializer object is used to convert the raw data back into a usable DataSet. Note that the WriteEncryptedDataSet and ReadEncryptedDataSet functions are completely generic and do not rely on any specific schema of the DataSet. This allows you to use the same functions to read or write any DataSet in a secure manner.