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.
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
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:
public static void WriteXml(this System.Data.DataSet dataSet,
string fileName,
string userName,
string password,
bool compress) {
if (dataSet == null
|| string.IsNullOrEmpty(userName)
|| string.IsNullOrEmpty(password)
|| string.IsNullOrEmpty(fileName)) {
throw new System.ArgumentNullException("All arguments must be supplied.");
}
Cryptography.EncryptDataSet(dataSet, userName, password, fileName, compress);
}
<System.Runtime.CompilerServices.Extension()>
Public Sub WriteXml(dataSet As System.Data.DataSet,
fileName As String,
userName As String,
password As String,
compress As Boolean)
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
Cryptography.EncryptDataSet(dataSet, userName, password, fileName, compress)
End Sub
And the extension method for ReadXml
looks quite similar
public static System.Data.XmlReadMode ReadXml(this System.Data.DataSet dataSet,
string fileName,
string userName,
string password,
bool compressed) {
if (dataSet == null
|| string.IsNullOrEmpty(userName)
|| string.IsNullOrEmpty(password)
|| string.IsNullOrEmpty(fileName)) {
throw new System.ArgumentNullException("All arguments must be supplied.");
}
return Cryptography.DecryptDataSet(dataSet, userName, password, fileName, compressed);
}
<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
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
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
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;
}
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:
internal static void EncryptDataSet(System.Data.DataSet dataset,
string username,
string password,
string fileName,
bool compress) {
if (dataset == null
|| string.IsNullOrEmpty(username)
|| string.IsNullOrEmpty(password)
|| string.IsNullOrEmpty(fileName)) {
throw new System.ArgumentNullException("All arguments must be supplied.");
}
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) {
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();
}
}
}
}
}
Friend Sub EncryptDataSet(dataSet As System.Data.DataSet, username As String, password As String, fileName As String, compress As Boolean)
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
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
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:
- The WriteXml writes the contents of the dataset in XML into the CryptoStream or into the GZipStream
- If GZipStream is involved it compresses the data in the XML format and sends the contents of this stream into the CryptoStream.
- The CryptoStream encrypts the data in the stream using AES
- 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:
internal static System.Data.XmlReadMode DecryptDataSet(System.Data.DataSet dataset,
string username,
string password,
string fileName,
bool compressed) {
System.Data.XmlReadMode xmlReadMode;
if (dataset == null
|| string.IsNullOrEmpty(username)
|| string.IsNullOrEmpty(password)
|| string.IsNullOrEmpty(fileName)) {
throw new System.ArgumentNullException("All arguments must be supplied.");
}
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) {
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;
}
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
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
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
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:
- The dataset is read using the native
ReadXml
method. - The
ReadXml
reads from a CryptoStream
that uses the previously created AesManaged
instance - If the file was compressed, the
GZipStream
is used before the CryptoStream
. - 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 | 5 |
Password length greater than 10 characters | 15 |
Password contains at least one digit | 5 |
Password contains at least three digits | 15 |
Password contains at least one special character | 5 |
Password contains at least three special characters | 15 |
Password contains at least one uppercase character | 5 |
Password contains at least three uppercase characters | 15 |
Password contains at least one lowercase character | 5 |
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
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);
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;
}
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)
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 " />
History
- 9th September, 2012: Created.
- 14th September, 2012: Compression added.
- 4th December, 2012: Typo corrected.
- 21th August, 2015: Code blocks formatted.