Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VB

Encrypting a dataset using AES, including compression

4.98/5 (49 votes)
21 Aug 2015CPOL7 min read 134K   4.9K  
Article describes how to encrypt a dataset using AES. Optionally the dataset is compressed before the encryption.

Introduction   

Many applications store data locally on the client computer hard disk using a DataSet.
Depending on the application the data may be even very sensitive. While the saved dataset may be protected in various ways like folder and file privileges, these mechanisms don't offer protection if the file is for example mistakenly copied to a wrong location and so on.

One way to enhance the security is to encrypt the saved dataset while saving it and then again decrypt it when reading. This article demonstrates using AES to encrypt the dataset with 128 bit encryption. Additionally the encrypted dataset can be compressed before the encryption. 

The application

Before jumping into the code, let's have a look at the application. The demo project is provided in both C# and VB.NET. The demo project stores contact information, which includes:

  • First name of the person
  • Last name
  • Telephone number
  • E-mail address

The main window is used for manipulating the data. The window is very simple. First you need to create an encrypted dataset. After that you can add contacts and save or cancel changes. Optionally the file can be compressed. 

At any time you can open another existing dataset or create a new one. 

Image 1

When creating or opening an encrypted dataset, the authentication window is shown.

This window asks for the credentials to be used with encryption/decryption and the name of the file in which the dataset is stored.

The authentication window also shows a rough estimate for password strength. That's explained more thoroughly later. So the authentication looks like

Image 2

Writing and reading the dataset

The dataset is written and read using WriteXml and ReadXml methods. The encryption (or decryption) is done while writing or reading the data. To provide a consistent way of using these methods I've defined extension methods for both operations which require three parameters:

  • fileName, name of the file to be written or read
  • userName, the user name used in encryption or decryption
  • password, the password to be used
  • compress, should the data be compressed before the encryption 

The extension methods are located in the static Cryptography class. The extension method for WriteXml is as follows:

C#
/// <summary>
/// Extension method for a dataset to define WriteXml method with encryption
/// </summary>
/// <param name="dataSet">The dataset</param>
/// <param name="fileName">File name to read</param>
/// <param name="userName">Username for encryption</param>
/// <param name="password">Password for encryption</param>
/// <param name="compress">Should the file be compressed</param>
public static void WriteXml(this System.Data.DataSet dataSet, 
                            string fileName, 
                            string userName, 
                            string password,
                            bool compress) {
   // Check the parameters
   if (dataSet == null
      || string.IsNullOrEmpty(userName)
      || string.IsNullOrEmpty(password)
      || string.IsNullOrEmpty(fileName)) {
      throw new System.ArgumentNullException("All arguments must be supplied.");
   }
 
   // Encrypt and save the dataset
   Cryptography.EncryptDataSet(dataSet, userName, password, fileName, compress);
} 
VB.NET
''' <summary>
''' Extension method for a dataset to define WriteXml method with encryption
''' </summary>
''' <param name="dataSet">The dataset</param>
''' <param name="fileName">File name to read</param>
''' <param name="userName">Username for encryption</param>
''' <param name="password">Password for encryption</param>
''' <param name="compress">Should the file be compressed</param>
<System.Runtime.CompilerServices.Extension()>
Public Sub WriteXml(dataSet As System.Data.DataSet, 
                    fileName As String, 
                    userName As String, 
                    password As String,
                    compress As Boolean)
   ' Check the parameters
   If dataSet Is Nothing _
      Or String.IsNullOrEmpty(userName) _
      Or String.IsNullOrEmpty(password) _
      Or String.IsNullOrEmpty(fileName) Then
      Throw New System.ArgumentNullException("All arguments must be supplied.")
   End If
 
   ' Encrypt and save the dataset
   Cryptography.EncryptDataSet(dataSet, userName, password, fileName, compress)
End Sub

And the extension method for ReadXml looks quite similar

C#
/// <summary>
/// Extension method for a dataset to define ReadXml method with decryption
/// </summary>
/// <param name="dataSet">The dataset</param>
/// <param name="fileName">File name to read</param>
/// <param name="userName">Username for decryption</param>
/// <param name="password">Password for decryption</param>
/// <param name="compressed">Is the file compressed</param>
/// <returns>XmlReadMode used for reading</returns>
public static System.Data.XmlReadMode ReadXml(this System.Data.DataSet dataSet, 
                                              string fileName, 
                                              string userName, 
                                              string password,
                                              bool compressed) {
   // Check the parameters
   if (dataSet == null
      || string.IsNullOrEmpty(userName)
      || string.IsNullOrEmpty(password)
      || string.IsNullOrEmpty(fileName)) {
      throw new System.ArgumentNullException("All arguments must be supplied.");
   }
 
   // Decrypt the saved dataset
   return Cryptography.DecryptDataSet(dataSet, userName, password, fileName, compressed);
}
VB.NET
''' <summary>
''' Extension method for a dataset to define ReadXml method with decryption
''' </summary>
''' <param name="dataSet">The dataset</param>
''' <param name="fileName">File name to read</param>
''' <param name="userName">Username for decryption</param>
''' <param name="password">Password for decryption</param>
''' <param name="compressed">Is the file compressed</param>
''' <returns>XmlReadMode used for reading</returns>
<System.Runtime.CompilerServices.Extension()>
Public Function ReadXml(dataSet As System.Data.DataSet, 
                        fileName As String, 
                        userName As String, 
                        password As String,
                        compressed As Boolean) As System.Data.XmlReadMode
   ' Check the parameters
   If dataSet Is Nothing _
      Or String.IsNullOrEmpty(userName) _
      Or String.IsNullOrEmpty(password) _
      Or String.IsNullOrEmpty(fileName) Then
      Throw New System.ArgumentNullException("All arguments must be supplied.")
   End If
 
   ' Decrypt the saved dataset
   Return Cryptography.DecryptDataSet(dataSet, userName, password, fileName, compressed)
End Function

As seen the actual work is done inside other methods in the Cryptography class. Let's investigate those next. 

Encrypting and decrypting the data

InitAes method 

First, let's go through the InitAes method. This method creates the instance of AesManaged class which handles the encryption. The method receives both user name and password and uses them Key and IV.

This method is as follows

C#
/// <summary>
/// This method initializes the Aes used to encrypt or decrypt the dataset.
/// </summary>
/// <param name="username">Username to use for the encryption</param>
/// <param name="password">Password to use for the encryption</param>
/// <returns>New instance of Aes</returns>
private static System.Security.Cryptography.Aes InitAes(string username, string password) {
   System.Security.Cryptography.Aes aes = new System.Security.Cryptography.AesManaged();
   System.Security.Cryptography.Rfc2898DeriveBytes rfc2898 
      = new System.Security.Cryptography.Rfc2898DeriveBytes(
        password, 
        System.Text.Encoding.Unicode.GetBytes(username));
 
   aes.Padding = System.Security.Cryptography.PaddingMode.PKCS7;
   aes.KeySize = 128;
   aes.Key = rfc2898.GetBytes(16);
   aes.IV = rfc2898.GetBytes(16);
 
   return aes;
}
VB.NET
''' <summary>
''' This method initializes the Aes used to encrypt or decrypt the dataset.
''' </summary>
''' <param name="username">Username to use for the encryption</param>
''' <param name="password">Password to use for the encryption</param>
''' <returns>New instance of Aes</returns>
Private Function InitAes(username As String, password As String)
As System.Security.Cryptography.Aes
   Dim aes As System.Security.Cryptography.Aes 
      = New System.Security.Cryptography.AesManaged()
   Dim rfc2898 As System.Security.Cryptography.Rfc2898DeriveBytes 
      = New System.Security.Cryptography.Rfc2898DeriveBytes(
        password, 
        System.Text.Encoding.Unicode.GetBytes(username))
 
   aes.Padding = System.Security.Cryptography.PaddingMode.PKCS7
   aes.KeySize = 128
   aes.Key = rfc2898.GetBytes(16)
   aes.IV = rfc2898.GetBytes(16)
 
   Return aes
End Function

The key size used for encryption is 128 bits, which means 16 bytes. Both the Key and the initialization vector (IV) are derived from the password supplied using Rfc2898DeriveBytes. The user name is used as a salt to the derivation.

The key is assigned the first 16 bytes of the derived password and the IV is the next 16 bytes.

In case that the length of the key doesn't match 16 bytes, a padding is used to fill in necessary data. In this case PKCS7 is used which means that the end of the data is filled with repeating the number of 'missing' bytes.

This instatiated Aes class is used in both EncryptDataSet and DecryptDataSet methods.

EncryptDataSet method

As the method name suggests, this method is responsible for writing the encrypted data to the disk.

The method looks like this:

C#
/// <summary>
/// Saves the dataset encrypted in specified file
/// </summary>
/// <param name="dataset">Dataset to save</param>
/// <param name="username">Username for encryption</param>
/// <param name="password">Password for encryption</param>
/// <param name="fileName">File name where to save</param>
/// <param name="compress">Should the file be compressed</param>
internal static void EncryptDataSet(System.Data.DataSet dataset, 
                                    string username, 
                                    string password, 
                                    string fileName, 
                                    bool compress) {
   // Check the parameters
   if (dataset == null
      || string.IsNullOrEmpty(username)
      || string.IsNullOrEmpty(password)
      || string.IsNullOrEmpty(fileName)) {
      throw new System.ArgumentNullException("All arguments must be supplied.");
   }

   // Save the dataset as encrypted
   using (System.Security.Cryptography.Aes aes 
   = Cryptography.InitAes(username, 
                          password)) {
      using (System.IO.FileStream fileStream 
      = new System.IO.FileStream(fileName, 
                                 System.IO.FileMode.Create, 
                                 System.IO.FileAccess.ReadWrite, 
                                 System.IO.FileShare.None)) {
         using (System.Security.Cryptography.CryptoStream cryptoStream 
         = new System.Security.Cryptography.CryptoStream(fileStream, 
                                                         aes.CreateEncryptor(),
                                                         System.Security.Cryptography.CryptoStreamMode.Write)) {
            if (compress) {
               // when compression is requested, use GZip
               using (System.IO.Compression.GZipStream zipStream 
               = new System.IO.Compression.GZipStream(cryptoStream, 
                                                      System.IO.Compression.CompressionMode.Compress)) {
                  dataset.WriteXml(zipStream, System.Data.XmlWriteMode.WriteSchema);
                  zipStream.Flush();
               }
            } else {
               dataset.WriteXml(cryptoStream, System.Data.XmlWriteMode.WriteSchema);
               cryptoStream.FlushFinalBlock();
            }
         }
      }
   }
}
VB.NET
''' <summary>
''' Saves the dataset encrypted in specified file
''' </summary>
''' <param name="dataSet">Dataset to save</param>
''' <param name="username">Username for encryption</param>
''' <param name="password">Password for encryption</param>
''' <param name="fileName">File name where to save</param>
''' <param name="compress">Should the file be compressed</param>
Friend Sub EncryptDataSet(dataSet As System.Data.DataSet, username As String, password As String, fileName As String, compress As Boolean)
   ' Check the parameters
   If dataSet Is Nothing _
      Or String.IsNullOrEmpty(username) _
      Or String.IsNullOrEmpty(password) _
      Or String.IsNullOrEmpty(fileName) Then
      Throw New System.ArgumentNullException("All arguments must be supplied.")
   End If

   ' Save the dataset as encrypted
   Using aes As System.Security.Cryptography.Aes 
   = Cryptography.InitAes(username, 
                          password)
      Using fileStream As System.IO.FileStream 
      = New System.IO.FileStream(fileName, 
                                 System.IO.FileMode.Create, 
                                 System.IO.FileAccess.ReadWrite, 
                                 System.IO.FileShare.None)
         Using cryptoStream As System.Security.Cryptography.CryptoStream 
         = New System.Security.Cryptography.CryptoStream(fileStream, 
                                                         aes.CreateEncryptor(), 
                                                         System.Security.Cryptography.CryptoStreamMode.Write)
            If (compress) Then
               ' when compression is requested, use GZip
               Using zipStream As System.IO.Compression.GZipStream 
               = New System.IO.Compression.GZipStream(cryptoStream, 
                                                      System.IO.Compression.CompressionMode.Compress)
                  dataSet.WriteXml(zipStream, System.Data.XmlWriteMode.WriteSchema)
                  zipStream.Flush()
               End Using
            Else
               dataSet.WriteXml(cryptoStream, System.Data.XmlWriteMode.WriteSchema)
               cryptoStream.FlushFinalBlock()
            End If
         End Using
      End Using
   End Using
End Sub
Now this first initializes a FileStream in order to write the contents of the dataset to a file.

The writing itself is done via CryptoStream. The CryptoStream handles the encrypting of the stream using the previously created AesManaged instance. 

If compression is requested, then a GZipStream is created in order to compress the data sent to the CryptoStream. I chose GZip instead of deflate because of the CRC check (Cyclic Redundancy Check). This helps to detect potential problems in the code or in the file itself. 

And to actually write the contents of the dataset to a file, the native WriteXml is called. Depending on the parameters the dataset is written either into the CryptoStream or the GZipStream

So what happens is that: 

 

  1. The WriteXml writes the contents of the dataset in XML into the CryptoStream or into the GZipStream
  2. If GZipStream is involved it compresses the data in the XML format and sends the contents of this stream into the CryptoStream. 
  3. The CryptoStream encrypts the data in the stream using AES 
  4. The FileStream reads the CryptoStream and writes the encrypted data to the disk

On thing to notice is that I call FlushFinalBlock (or Flush with GZipStream) after writing the data. This is because otherwise the last bytes of the stream won't be written into the file and this would result into a situation where the encrypted data couldn't be decrypted. 

The optional compression is done for the XML data. This results into much better compression rate than if the compression would be done on encrypted data. 

 

DecryptDataSet method 

So this method handles the decryption. Basically it's using the same idea as EncryptDataSet. The code:

C#
  /// <summary>
/// Reads and decrypts the dataset from the given file
/// </summary>
/// <param name="dataset">Dataset to read</param>
/// <param name="username">Username for decryption</param>
/// <param name="password">Password for decryption</param>
/// <param name="fileName">File name to read</param>
/// <param name="compressed">Is the file compressed</param>
/// <returns>XmlReadMode used for reading</returns>
internal static System.Data.XmlReadMode DecryptDataSet(System.Data.DataSet dataset, 
                                                       string username, 
                                                       string password, 
                                                       string fileName, 
                                                       bool compressed) {
   System.Data.XmlReadMode xmlReadMode;

   // Check the parameters
   if (dataset == null
      || string.IsNullOrEmpty(username)
      || string.IsNullOrEmpty(password)
      || string.IsNullOrEmpty(fileName)) {
      throw new System.ArgumentNullException("All arguments must be supplied.");
   }

   // Read the dataset and encrypt it
   using (System.Security.Cryptography.Aes aes 
   = Cryptography.InitAes(username, 
                          password)) {
      using (System.IO.FileStream fileStream 
      = new System.IO.FileStream(fileName, 
                                 System.IO.FileMode.Open)) {
         using (System.Security.Cryptography.CryptoStream cryptoStream 
         = new System.Security.Cryptography.CryptoStream(fileStream, 
                                                         aes.CreateDecryptor(), 
                                                         System.Security.Cryptography.CryptoStreamMode.Read)) {
            if (compressed) {
               // when decompression is requested, use GZip
               using (System.IO.Compression.GZipStream zipStream 
               = new System.IO.Compression.GZipStream(cryptoStream, 
                                                      System.IO.Compression.CompressionMode.Decompress)) {
                  xmlReadMode = dataset.ReadXml(zipStream, System.Data.XmlReadMode.ReadSchema);
               }
            } else {
               xmlReadMode = dataset.ReadXml(cryptoStream, System.Data.XmlReadMode.ReadSchema);
            }
         }
      }
   }

   return xmlReadMode;
} 
VB.NET
''' <summary>
''' Reads and decrypts the dataset from the given file
''' </summary>
''' <param name="dataset">Dataset to read</param>
''' <param name="username">Username for decryption</param>
''' <param name="password">Password for decryption</param>
''' <param name="fileName">File name to read</param>
''' <param name="compressed">Is the file compressed</param>
''' <returns>XmlReadMode used for reading</returns>
Friend Function DecryptDataSet(dataset As System.Data.DataSet, 
                               username As String, 
                               password As String, 
                               fileName As String, 
                               compressed As Boolean) As System.Data.XmlReadMode
   Dim xmlReadMode As System.Data.XmlReadMode

   ' Check the parameters
   If dataset Is Nothing _
      Or String.IsNullOrEmpty(username) _
      Or String.IsNullOrEmpty(password) _
      Or String.IsNullOrEmpty(fileName) Then
      Throw New System.ArgumentNullException("All arguments must be supplied.")
   End If

   ' Read the dataset and encrypt it
   Using aes As System.Security.Cryptography.Aes 
   = Cryptography.InitAes(username, 
                          password)
      Using fileStream As System.IO.FileStream 
      = New System.IO.FileStream(fileName, 
                                 System.IO.FileMode.Open)
         Using cryptoStream As System.Security.Cryptography.CryptoStream 
         = New System.Security.Cryptography.CryptoStream(fileStream, 
                                                         aes.CreateDecryptor(), 
                                                         System.Security.Cryptography.CryptoStreamMode.Read)
            If (compressed) Then
               ' when decompression is requested, use GZip
               Using zipStream As System.IO.Compression.GZipStream 
               = New System.IO.Compression.GZipStream(cryptoStream, 
                                                      System.IO.Compression.CompressionMode.Decompress)
                  xmlReadMode = dataset.ReadXml(zipStream, System.Data.XmlReadMode.ReadSchema)
               End Using
            Else
               xmlReadMode = dataset.ReadXml(cryptoStream, System.Data.XmlReadMode.ReadSchema)
            End If
         End Using
      End Using
   End Using

   Return xmlReadMode
End Function 

So the workflow is the same as in EncryptDataSet method:

 

  1. The dataset is read using the native ReadXml method. 
  2. The ReadXml reads from a CryptoStream that uses the previously created AesManaged instance 
  3. If the file was compressed, the GZipStream is used before the CryptoStream
  4. The CryptoStream gets the data from a FileStream which reads the file on the disk. 

 

PasswordStrength method

One important thing is that the strength of the password is tested. The test of the strength returns an integer between 0 and 100. The higher the number, the stronger the password. This number is visually shown in the authentication window.

The test for the password strength is very simple so adjust it to meet your needs. I've used the following logic: 

 

Test 

Points added 
Password length greater than 5 characters
Password length greater than 10 characters 15 
Password contains at least one digit 
Password contains at least three digits  15 
Password contains at least one special character
Password contains at least three special characters   15 
Password contains at least one uppercase character 
Password contains at least three uppercase characters  15 
Password contains at least one lowercase character
Password contains at least three lowercase characters  15 

Most of the test are done by checking the amount of RegEx matches. So the code looks like

C#
private static System.Text.RegularExpressions.Regex passwordDigits 
        = new System.Text.RegularExpressions.Regex(@"\d", 
          System.Text.RegularExpressions.RegexOptions.Compiled);
private static System.Text.RegularExpressions.Regex passwordNonWord 
        = new System.Text.RegularExpressions.Regex(@"\W", 
          System.Text.RegularExpressions.RegexOptions.Compiled);
private static System.Text.RegularExpressions.Regex passwordUppercase 
        = new System.Text.RegularExpressions.Regex(@"[A-Z]", 
          System.Text.RegularExpressions.RegexOptions.Compiled);
private static System.Text.RegularExpressions.Regex passwordLowercase 
        = new System.Text.RegularExpressions.Regex(@"[a-z]", 
          System.Text.RegularExpressions.RegexOptions.Compiled);

/// <summary>
/// This method calculates the strength of the password
/// </summary>
/// <param name="password">Password to check</param>
/// <returns>Password strength between 0 and 100</returns>
internal static int PasswordStrength(string password) {
   int strength = 0;

   if (password.Length > 5) strength += 5;
   if (password.Length > 10) strength += 15;
   if (passwordDigits.Matches(password).Count >= 1) strength += 5;
   if (passwordDigits.Matches(password).Count >= 3) strength += 15;
   if (passwordNonWord.Matches(password).Count >= 1) strength += 5;
   if (passwordNonWord.Matches(password).Count >= 3) strength += 15;
   if (passwordUppercase.Matches(password).Count >= 1) strength += 5;
   if (passwordUppercase.Matches(password).Count >= 3) strength += 15;
   if (passwordLowercase.Matches(password).Count >= 1) strength += 5;
   if (passwordLowercase.Matches(password).Count >= 3) strength += 15;

   return strength;
} 
VB.NET
Private passwordDigits As System.Text.RegularExpressions.Regex 
        = New System.Text.RegularExpressions.Regex("\d", 
          System.Text.RegularExpressions.RegexOptions.Compiled)
Private passwordNonWord As System.Text.RegularExpressions.Regex 
        = New System.Text.RegularExpressions.Regex("\W", 
          System.Text.RegularExpressions.RegexOptions.Compiled)
Private passwordUppercase As System.Text.RegularExpressions.Regex 
        = New System.Text.RegularExpressions.Regex("[A-Z]", 
          System.Text.RegularExpressions.RegexOptions.Compiled)
Private passwordLowercase As System.Text.RegularExpressions.Regex 
        = New System.Text.RegularExpressions.Regex("[a-z]", 
          System.Text.RegularExpressions.RegexOptions.Compiled)

''' <summary>
''' This method calculates the strength of the password
''' </summary>
''' <param name="password">Password to check</param>
''' <returns>Password strength between 0 and 100</returns>
Friend Function PasswordStrength(password As String) As Integer
   Dim strength As Integer = 0

   If (password.Length > 5) Then strength += 5
   If (password.Length > 10) Then strength += 15
   If (passwordDigits.Matches(password).Count >= 1) Then strength += 5
   If (passwordDigits.Matches(password).Count >= 3) Then strength += 15
   If (passwordNonWord.Matches(password).Count >= 1) Then strength += 5
   If (passwordNonWord.Matches(password).Count >= 3) Then strength += 15
   If (passwordUppercase.Matches(password).Count >= 1) Then strength += 5
   If (passwordUppercase.Matches(password).Count >= 3) Then strength += 15
   If (passwordLowercase.Matches(password).Count >= 1) Then strength += 5
   If (passwordLowercase.Matches(password).Count >= 3) Then strength += 15

   Return strength
End Function

Other possibly interesting pieces of code 

Some other things that may be interesting in the demo project.

When the dataset is created for the first time, the application first writes the newly created dataset into the file and then reads the dataset from the file. While this unnecessary since the dataset is already in memory, I wanted to do the initialization like this, because this ensures that the written information can be read properly. It wouldn't be a nice situation if the encrypted dataset would contain a lot of data and would be for example corrupted. 

The progress bar for password strength: The progress bar uses a LinearGradientBrush to indicate the strength of the password. The colors change from red to yellow to green. When writing  this, I used a converter to calculate the offsets for the colors and which colors are used. If I would have used simply a gradient brush with three colors, the progress bar would always have shown all of the colors even if the password isn't strong. I wanted to keep the offsets for color changes constant even if the value of the progress bar is changing. You can have a look at the converter in ForegroundConverter class. The colors are still changing a bit awkward, so perhaps I'll fix that someday.

Note: When opening the demo projects, remember to compile them first since the downloads contain no binaries. Otherwise you'll get unnecessary errors when opening the windows in design mode etc. 

References 

Some references you may find interesting while reading this article: 

 

 

That's it for this time. As always comments are very welcome Smile | <img src= " /> 

History  

  • 9th September, 2012: Created.
  • 14th September, 2012: Compression added.  
  • 4th December, 2012: Typo corrected.
  • 21th August, 2015: Code blocks formatted.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)