Symmetric Encryption in .Net:
Building an Encryption Library

by Jon Wojtowicz

Encryption
Cryptography involves the protection of data. The method or algorithm used can be as simple and old as the Caesar cipher (character shifting/replacement) to the current advanced encryption standard (AES) which is the Rinjdael algorithm.
There are two types of encryption: Symmetric encryption, which uses a single secret key to encrypt as well as decrypt the data, and Asymmetric encryption. This uses a public key to encrypt the data and a private key to decrypt. The public/private key pair is mathematically related but cannot be derived from each other. Since the key pair must be trusted, the keys are typically provided by a third party with the use of certificates. Symmetric encryption is typically used for encrypting large blocks of data as it is up to 100 time faster then asymmetric encryption.
.Net Encryption
.Net provides extensive support for cryptography. The classes are in the System.Security.Cryptography namespace. The classes included support several encryption algorithms. The primary classes you use for symmetric encryption are the Symmetric Algorithm, CryptoStream and the RandomNumberGenerator.


The biggest challenge in symmetric cryptography is key storage. To avoid storing keys, you can require the use of a password. The password should be sufficiently complex to produce a strong key. This becomes problematic as it become difficult for users to remember complex passwords. This can be resolved by combining the password with a cryptographically random number. The benefit of using a cryptographically random number generator is that the next random number cannot be guessed by looking at the previous sequence of numbers produced.
The first thing was to create a class that inherits from MenuItem. This allowed me to use the base properties and extend where I needed to.
Then I had to change was creating the OwnerDraw property to true. This property indicates whether the system should draw the menu or not. It is defaulted to false which indicates the system will draw the menu. Setting this to true tells the system that the menu creating will handle the drawing.
The random number generator in the System.Random class in .Net and the srand function in C++ both produce the same sequence of numbers for the same seed. To generate truly random numbers the System.Security.Cryptography.RandomNumberGenerator class in .Net should be used. These numbers are considered cryptographically random.
The random numbers combined with the password is called a salt. This combination provides a strong key even from weak passwords. The salt value is then stored with the encrypted data. This allows the key to be regenerated form the same password for decrypting the data. This allows you to encrypt and decrypt the data without ever storing the actual key used for the operations.
The basic approach is to generate the key from a password. This can be a user supplied password or a stored machine generated sequence of bytes. The length of the password affects the strength of the key. Typically you should require at least 9 characters, mixed case with at least one numeric or special character. This will ensure a strong key.
The random salt and the password are used to generate the key. The salt is then stored with the encrypted data. Most algorithms also require initialization data called the initialization vector (IV). This also needs to be stored with the encrypted data.
If the user is supplying a password its complexity can be validated with the following code.
 
Regex r = new Regex(@"^(?=.*[0-9])(?=.*[a-z])(?=.*\W)");
if (password.Length >= 9) && (r.IsMatch(password)))
return true;
else
return false;
The salt value can be generated uing the GetBytes method of the RandomNumberGenerator. The code sample generates a 16 byte random number.
 
RandomNumberGenerator rng = RandomNumberGenerator.Create();
byte[] salt = new byte[16];
rng.GetBytes(salt);
A similar method can be used to generate a random password if the user is not supplying a password. The machine generated password should be at least 16 bytes. I typically use a size that matches the maximum key size of the algorithm used.
The key for the encryption can be generated using the previously obtained salt and password. The System.Security.Cryptography.PasswordDerivedBytes class is used to generate the key. This will take the password and salt to generate a key of the specified size. Each algorithm has a range of legal key sizes. It is important to select a key size that is valid for the selected algorithm.
 
int keySize = 16;
PasswordDerivedBytes pdb = new PasswordDerivedBytes(password, salt);
byte[] key = pdb.GetBytes(keySize);
The steps that are needed are create the SymmetricAlgorithm, set its key to the key that has been created, create a stream to write the encrypted data, create the initialization vector, store the salt and initialization vector in the stream, create the ICryptoTransform, create a CryptoStream around the stream specifying the ICryptoTransform, write the encrypted data to the stream, flush the final block to the stream and close the CryptoStream after the data has been written.
The steps are the same for each SymmetricAlgorithm. The following is the code that performs encryption using the Rijndael algorithm.
 
SymmetricAlgorithm algorithm = new RijndaelManaged();
using(MemoryStream dataStream = new MemoryStream())
{
byte[] salt = GetSalt();
dataStream.Write(salt, 0, salt.Length);
algorithm.GenerateIV();
dataStream.Write(algorithm.IV, 0, algorithm.IV.Length);
algorithm.Key = GetKey(salt);
using(ICryptoTransform transform = algorithm.CreateEncryptor())
{
using(CryptoStream crypto =
new CryptoStream(dataStream, transform,
CryptoStreamMode.Write))
{
crypto.Write(plaintext, 0, plaintext.Length);
crypto.FlushFinalBlock();
dataStream.Flush();
byte[] encData = dataStream.ToArray();
crypto.Close();
return encData;
}
}
}
In this example the GetSalt method returns the salt as in the prior example. The GetKey method takes the salt and a previously supplied password and creates the password. The data image after the encryption has the following format.
Encrypted data layout format
Knowing this format and the password we can decrypt by reversing the operation we used to encrypt.
 
SymmetricAlgorithm algorithm = new RijndaelManaged();
using(MemoryStream dataStream = new MemoryStream(cipherText))
{
byte[] salt = new byte[16];
byte[] iv = new byte[algorithm.IV.Length];
dataStream.Read(salt, 0, salt.Length);
dataStream.Read(iv, 0, iv.Length);
algorithm.Key = GetKey(salt);
algorithm.IV = iv;
using(CryptoStream crypto =
new CryptoStream(dataStream, algorithm.CreateDecryptor(),
CryptoStreamMode.Read))
{
byte[] buffer = new byte[256];
int bytesRead = 0;
using(MemoryStream decryptedData = new MemoryStream())
{
do
{
bytesRead = crypto.Read(buffer, 0, 256);
decryptedData.Write(buffer, 0, bytesRead);
}
while(bytesRead > 0);
crypto.Close();
byte[] decData = decryptedData.ToArray();
decryptedData.Close();
return decData;
}
}
}
Since the steps to encrypt/decrypt are the same for each SymmetricAlgorithm it makes sense to wrap this functionality in a helper class. This also ensures that Dispose is called on all the disposable members. This is important as several of the SymmetricAlgorithm classes use the Crypt32 API to perform the encryption and handle unmanaged objects.
The DPAPI
Starting in Windows 2000, Microsoft included an encryption API as part of the OS.These are classes in the Crypt32.dll. Among these is the Data Protection API (DPAPI).
The DPAPI uses the triple DES algorithm for encryption. The master key is stored by the Local Security Authority (LSA). This key is used to generate the encryption keys used to encrypt data. The master key is rotated every 45 days with history tracking. There are two ways data can be protected using the DPAPI, using the user store or using the machine store.
The user store requires a user profile to be loaded as the password is used for the encryption. This means only the user that encrypted the data can decrypt it. Changes to the user's password by the user are tracked. Changes to the user's password made by someone other than the user will cause the data to be unrecoverable. If the user has a roaming profile the data can be decrypted on any machine by the user. By default, impersonation does not load the user store.
The machine store creates machine specific keys and can be decrypted by any user on the machine. The data can only be decrypted on the machine on which it was encrypted.
The Helper Library
The helper class in the accompanying source code encapsulates the encryption/decryption by using a factory pattern. The SymmetricAlgorithm contains an internal factory that takes the string name of the algorithm that you want to use. This helper class uses an enumeration which has better performance. It also wraps the encryption/decryption in a simple API.
The helper class has the following features:
  • It uses an indirect key. The user provides entropy (password) and it derives the key for the encryption. If entropy is not provided it will generate random entropy for the key generation. This does not include the DPAPI.

  • It supports accepting/returning the encrypted data as base64 or hex encoded strings.

  • It includes helper methods to convert to/from strings/bytes.

  • It supports the DPAPI wrapped in the same pattern as the other symmetric algorithms.

  • It exposes the handling of the encryption/decryption through an interface, ICryptoHelper which describes methods to encrypt and decrypt.

  • It supports reading and writing to the registry, both plain data and encrypted data. This is useful when you need to store secrets in the registry.

  • It includes an encrypted configuration section handler which allows you to encrypt custom configuration sections. These can be encrypted seamlessly during deployment as it wraps the original section handler. It also supports placing the entropy into the registry for security.

Usage Scenarios
The solution also includes a test harness Windows app that allows you to see how to use the library. These are some usage scenarios for the library.
Description:
The user wishes to encrypt data using the Rijndael algorithm with a random key. The user wishes to store the data as a hex encoded string. The user then wishes to decrypt the data.
Code Example:
 
using Persephone.Security.Utilities.Cryptography;
. . .
string key, encryptedData, decryptedData;
//encrypt the data and store the created random key string.
using(ICryptoHelper encrypt = CryptoFactory.Create(CryptographyAlgorithm.Rijndael))
{
string secretData = "My Secret Data";
encryptedData = encrypt.Encrypt(secretData, StringEncodingType.Hex);
key = encrypt.Entropy; //Note: This may be null if using the //DPAPI
}
. . .
//decrypt the data using the same key string
using(ICryptoHelper decrypt = CryptoFactory.Create(CryptographyAlgorithm.Rijndael, key))
{
decryptedData = decrypt.Decrypt(encryptedData, StringEncodingType.Hex);
}
//NOTE: The same ICryptoHelper can be used to encrypt and decrypt data.
Description:
The user wishes to store data in the registry at HKey_Local_Machine\Software\Applications\MyApp\TestData with a key of data. The user then wishes to read the data from the registry.
NOTE: The user must have read and write permission to the registry.
Code Example:
 
using Persephone.Security.Utilities.Registry;
. . .
string data = "Test Data";
using(RegistryWriter writer = new RegistryWriter(RegistryHive.LocalMachine))
{
writer.WriteDataToRegistry (@"Software\ Applications",@"MyApp\TestData","data",data);
//If the subkey MyApp\TestData does not exist, it will be created.
}
string returnData;
using(RegistryReader reader = new RegistryReader(RegistryHive.LocalMachine))
{
returnData = reader.ReadDataFromRegistry(@"Software\ Applications\MyApp\TestData","data");
}
Description:
Same as the previous scenario with the added requirement of encryption. The user wishes to encrypt the data using the machine DPAPI.
NOTE: The user must have read and write permission to the registry.
Code Example:
 
using Persephone.Security.Utilities.Cryptography;
using Persephone.Security.Utilities.Registry;
. . .
string data = "Test Data";
using(ICryptoHelper encrypt = CryptoFactory.Create(CryptographyAlgorithm.MachineDpApi))
{
using(RegistryWriter writer = new RegistryWriter(RegistryHive.LocalMachine))
{
writer.WriteDataToRegistry(@"Software\Applications", @"MyApp\TestData","data",data, encrypt);
//If the key MyApp\TestData does not exist, it will be created.
}
}
. . .
string returnData;
using(ICryptoHelper decrypt = CryptoFactory.Create(CryptographyAlgorithm.MachineDpApi))
{
using(RegistryReader reader = new RegistryReader(RegistryHive.LocalMachine))
{
returnData = reader.ReadDataFromRegistry(@"Software\ Applications\MyApp\TestData","data", decrypt);
}
}
Description:
The user is using a custom configuration section that contains a connection string. The user wishes to have the section encrypted for security using the TripleDES algorithm. The user wishes to store the key encrypted in the configuration file.
Code Example:
The custom section is defined in the configuration file as follows:
 
<configSections>
<section name="Data" type="System.Configuration.NameValueFileSectionHandler, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
</configSections>
<Data>
<add key="ConnectionString" value="Data Source=localhost;Initial Catalog=Northwind;trusted_connection=yes;" />
</Data>
The code the developer will use to extract the configuration data will be as follows:
 
System.Collection.Specialized.NameValueCollection config = (System.Collection.Specialized.NameValueCollection) System.Configuration.Configurationsettings.GetConfig("Data");
String conString = config["ConnectionString"];
After encrypting the section it will be defined as follows (NOTE: Verify the version of the library with the version specified in the configuration file.):
 
<configSections>
<section name="Data" type="Persephone.Security.Utiltities.Configuration.EncryptedSectionHandler, Persephone.Security.Utilities, Version=1.0.0.0, Culture=neutral, PublicKeyToken=e4da5ff13bdc48c3" />
</configSections>
<Data algorithm="TripleDes" key="AQAAANCMnd8BFdERjHoAwE/Cl+sBAAAAqWyXKImK00W+JULu8IO56QQAAAACAAAAAAADZgAAqAAAA BAAAAAusdYzqAntuiH3UO/DYxnXAAAAAASAAACgAAAAEAAAAJih9S+0Qle2e8bBvXIVZCJIAAAANC9TsrmwMTKKeuhVHhjAUL1fDqcK0A/qXjf0eg Dcs7+HvxC1PI+RTkgQxFtGmA9Wyt00lev2iWEID7+QvpDXp0Pan3V8v13+FAAAAFFG7pfRcE19mA24MT0R0rkJl8oT" encrypted="True" type="System.Configuration.NameValueFileSectionHandler, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
wkmLuIE/+1/K1PBn2E3k6vbkz2hBG015mV+2kGsRt/5G4nsILWj1FftUjO8SLcbIYiEECuHhLylhbD/EbhOop2EyWp/rEW7UybycZRgNbk9Ms9I gSnLOz9qbbhWHFdn5jZVDoqZDjMHusWRBktWuwrcJCCSSVTJZ+EFkSHxfdQ0hbKMgiO/mGfC1hoPqlBvQGA7WcnIMp4cw+DfYVjglhNNYwE673j c+P86XGdLYJOBRSgVvDBUKKEPGWiMoq/RubZ8xFdIn6RHjW6Vv9jBntbtP3k+g/A/JUNZzlbuaqkRIYpNAeo36w21P5YFjrKkZrJ+uabDhKt2wew GPBX2EOg9CDkGfJ4ECLQlvXr5VWIgw/sNxdlVl9v7vnLqgJb3ztHsKjeBBd/HwoZLXlwdNcQ==
</Data>
No changes in the executing code are required.
The accompanying test harness will allow you properly encrypt/decrypt the configuration sections. You must remember to save the changes of the encryption/decryption in the configuration file.
Have fun with the library!
Jon Wojtowicz is a C# MVP and a Systems Analyst at a large insurance company in Chattanooga, TN where he currently provides developer support and internal training. He has worked as a consultant working with Microsoft Technologies. This includes ASP, COM, VB6 and .Net, both C# and VB.Net since Beta 1. He has been an MCSD since 1999 and an MCT since 2000. Prior to getting a degree in Computer science he worked as a process engineer focusing on process automation, programmable controllers and equipment installations. In his spare time he likes woodworking and gardening.
Article Discussion: