Pro ASP.NET 2.0 In CSharp 2005 (2005) [eng]
.pdf
848 C H A P T E R 2 5 ■ C RY P TO G R A P H Y
Figure 25-3. Reading and decrypting data
In write mode, the transformation is performed before the data is written to the underlying stream (as shown in Figure 25-4).
Figure 25-4. Writing and encrypting data
You cannot combine both modes to make a readable and writable CryptoStream (which would have no meaning anyway). Similarly, the Seek() method and the Position property, which are used to move to different positions in a stream, are not supported for the CryptoStream() and will throw a NotSupportedException if called. However, you can often use these members with the underlying stream.
Encrypting Sensitive Data
Now that you’ve taken an in-depth look at .NET cryptography, it’s time to put it all together. In the following sections, you will create two utility classes that use symmetric and asymmetric algorithms. In the “Encrypting Sensitive Data in a Database” section, you will use one of these classes to encrypt sensitive information such as a credit card number stored in a database and to encrypt the query string. You need to perform the following steps to encrypt and decrypt information:
1.Choose and create an algorithm.
2.Generate and store the secret key.
3.Encrypt or decrypt information through a CryptoStream.
4.Close the source and target streams appropriately.
After you have created and tested your encryption utility classes, you will prepare a database to store secret information and then write the code for encrypting and decrypting this secret information in the database.
C H A P T E R 2 5 ■ C RY P TO G R A P H Y |
851 |
public static class SymmetricEncryptionUtility
{
private static bool _ProtectKey; private static string _AlgorithmName;
public static string AlgorithmName
{
get { return _AlgorithmName; } set { _AlgorithmName = value; }
}
public static bool ProtectKey
{
get { return _ProtectKey; } set { _ProtectKey = value; }
}
public static void GenerateKey(string targetFile) { }
public static void ReadKey(SymmetricAlgorithm algorithm, string file) { } public static byte[] EncryptData(string data, string keyFile) { }
public static string DecryptData(byte[] data, string keyFile) { }
}
Because the class is just a utility class with static members only, you can make it a static class so that nobody can create an instance of it. The class offers a possibility for specifying the name of the algorithm (DES, TripleDES, RijnDael, or RC2) through the AlgorithmName property. It also supports operations for generating a new key, reading this key from the file specified directly into the key property of an algorithm instance, and encrypting and decrypting data. For using this class, you must set the algorithm name appropriately and then generate a key if no one exists already. Then you just need to call the EncryptData and DecryptData methods, which internally will call the ReadKey method for initializing the algorithm. The ProtectKey property allows the user of the class to specify whether the key should be protected through the DPAPI.
You can generate encryption keys through the algorithm classes. The GenerateKey method looks like this:
public static void GenerateKey(string targetFile)
{
// Create the algorithm
SymmetricAlgorithm Algorithm = SymmetricAlgorithm.Create(AlgorithmName); Algorithm.GenerateKey();
// Now get the key
byte[] Key = Algorithm.Key;
if (ProtectKey)
{
// Use DPAPI to encrypt key Key = ProtectedData.Protect(
Key, null, DataProtectionScope.LocalMachine);
}
// Store the key in a file called key.config
using (FileStream fs = new FileStream(targetFile, FileMode.Create))
{
fs.Write(Key, 0, Key.Length);
}
}
852 C H A P T E R 2 5 ■ C RY P TO G R A P H Y
The GenerateKey() method of the SymmetricAlgorithm class generates a new key through cryptographically strong random number algorithms and initializes the Key property with this new key. If configured appropriately, it encrypts the key using the DPAPI. The ReadKey method reads the key from the file created by the GenerateKey method, as follows:
public static void ReadKey(SymmetricAlgorithm algorithm, string keyFile)
{
byte[] Key;
using (FileStream fs = new FileStream(keyFile, FileMode.Open))
{
Key = new byte[fs.Length]; fs.Read(Key, 0, (int)fs.Length);
}
if (ProtectKey)
algorithm.Key = ProtectedData.Unprotect(
Key, null, DataProtectionScope.LocalMachine);
else
algorithm.Key = Key;
}
If the key was protected previously, the ReadKey method uses the DPAPI for unprotecting the encrypted key when reading it from the file. The method furthermore requires passing in an existing instance of a symmetric algorithm. It directly initializes the key property of the algorithm so that this key will be used automatically for all subsequent operations. The function itself is used by both the EncryptData and DecryptData functions.
public static byte[] EncryptData(string data, string keyFile) { } public static string DecryptData(byte[] data, string keyFile) { }
As you can see, both methods require a keyFile parameter with the path to the file that stores the key. They subsequently call the ReadKey method for initializing their algorithm instance with the key. While the EncryptData method accepts a string and returns a byte array with the encrypted representation, the DecryptData accepts the encrypted byte array and returns the clear-text string.
Let’s get started with the EncryptData method:
public static byte[] EncryptData(string data, string keyFile)
{
// Convert string data to byte array
byte[] ClearData = Encoding.UTF8.GetBytes(data);
// Now create the algorithm
SymmetricAlgorithm Algorithm = SymmetricAlgorithm.Create(AlgorithmName); ReadKey(Algorithm, keyFile);
// Encrypt information
MemoryStream Target = new MemoryStream();
//Append IV Algorithm.GenerateIV();
Target.Write(Algorithm.IV, 0, Algorithm.IV.Length);
//Encrypt actual data
CryptoStream cs = new CryptoStream(Target,
C H A P T E R 2 5 ■ C RY P TO G R A P H Y |
853 |
Algorithm.CreateEncryptor(), CryptoStreamMode.Write); cs.Write(ClearData, 0, ClearData.Length); cs.FlushFinalBlock();
// Output the bytes of the encrypted array to the text box return Target.ToArray();
}
First, the method converts the string value into a byte array because all the encryption functions of the algorithms require byte arrays as input parameters. You can use the Encoding class of the System.Text namespace to do this easily. Next, the method creates the algorithm according to the AlgorithmName property of the class. This value can be one of the names RC2, Rijndael, DES, or TripleDES. The factory method of the SymmetricAlgorithm creates the appropriate instance, while additional cryptography classes can be registered through the <cryptographySettings> section in the machine.config file.
Afterward, the method creates a memory stream that will be the target of your encryption operation in this case. Before the class starts with the encryption operation through the CryptoStream class, it generates an initialization vector (IV) and writes the IV to the target stream on the first position. The IV adds random data to the encrypted stream of data.
Imagine the following situation: if your application exchanges the same information multiple times with actors, simple encryption will always result in the same encrypted representation of the information. This makes brute-force attacks easier. To add some sort of random information, symmetric algorithms support IV. These IVs are not only added to the encrypted stream of bytes themselves but they are also used as input for encrypting the first block of data. When using the CryptoStream for encrypting information, don’t forget to call the FlushFinalBlock to make sure the last block of encrypted data is written appropriately to the target.
Furthermore, you have to add initialization vectors to the encrypted set of bytes because you need the vector for decryption later, as follows:
public static string DecryptData(byte[] data, string keyFile)
{
// Now create the algorithm
SymmetricAlgorithm Algorithm = SymmetricAlgorithm.Create(AlgorithmName); ReadKey(Algorithm, keyFile);
// Decrypt information
MemoryStream Target = new MemoryStream();
// Read IV and initialize the algorithm with it int ReadPos = 0;
byte[] IV = new byte[Algorithm.IV.Length]; Array.Copy(data, IV, IV.Length); Algorithm.IV = IV;
ReadPos += Algorithm.IV.Length;
CryptoStream cs = new CryptoStream(Target, Algorithm.CreateDecryptor(), CryptoStreamMode.Write);
cs.Write(data, ReadPos, data.Length - ReadPos); cs.FlushFinalBlock();
// Get the bytes from the memory stream and convert them to text return Encoding.UTF8.GetString(Target.ToArray());
}
854 C H A P T E R 2 5 ■ C RY P TO G R A P H Y
The decryption function is structured the other way around. It creates the algorithm and creates a stream for the decrypted target information. Before you can start decrypting the data, you have to read the IV from the encrypted stream, because it is used by the algorithm for the last transformation. You then use the CryptoStream as you did previously, except you create a decryptor transformer this time. Finally, you get the decrypted byte representation of the string you have created through Encoding.UTF8.GetBytes(). To reverse this operation, you need to call the GetString() method of the UTF-8 encoding class for getting the clear-text representation of the string.
Using the SymmetricEncryptionUtility Class
Now you can create a page for testing the class you created previously. Just create a page that allows you to generate a key and enter clear-text data through a text box. You can output the encrypted data through Convert.ToBase64String() easily. For decryption, you just need to revert the operation through Convert.FromBase64String() to get the encrypted bytes back and pass them into the DecryptData method.
private string KeyFileName;
private string AlgorithmName = "DES";
protected void Page_Load(object sender, EventArgs e)
{
SymmetricEncryptionUtility.AlgorithmName = AlgorithmName;
KeyFileName = Server.MapPath("~/") + "\\symmetric_key.config";
}
protected void GenerateKeyCommand_Click(object sender, EventArgs e)
{
SymmetricEncryptionUtility.ProtectKey = EncryptKeyCheck.Checked; SymmetricEncryptionUtility.GenerateKey(KeyFileName);
Response.Write("Key generated successfully!");
}
protected void EncryptCommand_Click(object sender, EventArgs e)
{
// Check for encryption key if (!File.Exists(KeyFileName))
{
Response.Write("Missing encryption key. Please generate key!");
}
byte[] data = SymmetricEncryptionUtility.EncryptData( ClearDataText.Text, KeyFileName); EncryptedDataText.Text = Convert.ToBase64String(data);
}
protected void DecryptCommand_Click(object sender, EventArgs e)
{
// Check for encryption key if (!File.Exists(KeyFileName))
{
Response.Write("Missing encryption key. Please generate key!");
}
byte[] data = Convert.FromBase64String(EncryptedDataText.Text); ClearDataText.Text = SymmetricEncryptionUtility.DecryptData(
data, KeyFileName);
}
C H A P T E R 2 5 ■ C RY P TO G R A P H Y |
855 |
The previous page uses the DES algorithm because you set the AlgorithmName of your utility class appropriately. Within the Click event of the GenerateKeyCommand button, it calls the GenerateKey() method. Depending on the check box of the page, it encrypts the key itself through the DPAPI or not. After the data has been encrypted through your utility class within the Click event of the EncryptCommand button, it converts the encrypted bytes to a Base64 string and then writes it to the EcryptedDataText text box. Therefore, if you want to decrypt information again, you have to create a byte array based on this Base64 string representation and then call the method for decryption. You can see the result in Figure 25-6.
Figure 25-6. The resulting test page for symmetric algorithms
Using Asymmetric Algorithms
Using asymmetric algorithms is similar to using symmetric algorithms. You will see just a handful of differences. The major difference has to do with key management. Symmetric algorithms just have one key, and asymmetric algorithms have two keys: one for encrypting data (public key) and one for decrypting data (private key). While the public key can be available to everyone who wants to encrypt data, the private key should be available only to those decrypting information. In this section, you will create a utility class similar to the previous one.
Because the .NET Framework ships with only one asymmetric algorithm for real data encryption (RSA; remember, DSA is used for digital signatures only), you don’t need to include a way to select the algorithm (for a while).
public static class AsymmetricEncryptionUtility
{
public static string GenerateKey(string targetFile) { } private static void ReadKey(
RSACryptoServiceProvider algorithm, string keyFile) { } public static byte[] EncryptData(string data, string publicKey) { } public static string DecryptData(byte[] data, string keyFile) { }
}
856 C H A P T E R 2 5 ■ C RY P TO G R A P H Y
The GenerateKey method creates an instance of the RSA algorithm for generating the key. It stores only the private key in the file secured through the DPAPI and returns the public key representation as a string.
public static string GenerateKey(string targetFile)
{
RSACryptoServiceProvider Algorithm = new RSACryptoServiceProvider();
// Save the private key
string CompleteKey = Algorithm.ToXmlString(true); byte[] KeyBytes = Encoding.UTF8.GetBytes(CompleteKey);
KeyBytes = ProtectedData.Protect(KeyBytes,
null, DataProtectionScope.LocalMachine);
using (FileStream fs = new FileStream(targetFile, FileMode.Create))
{
fs.Write(KeyBytes, 0, KeyBytes.Length);
}
// Return the public key
return Algorithm.ToXmlString(false);
}
The caller of the function needs to store the public key somewhere; this is necessary for encrypting information. You can retrieve the key as an XML representation through a method called ToXmlString(). The parameter specifies whether private key information is included (true) or not (false). Therefore, the GenerateKey function first calls the function with the true parameter to store the complete key information in the file and then calls it with the false parameter to include the public key only. Subsequently, the ReadKey() method just reads the key from the file and then initializes the passed algorithm instance through FromXml(), the opposite of the ToXmlString() method:
private static void ReadKey(RSACryptoServiceProvider algorithm, string keyFile)
{
byte[] KeyBytes;
using(FileStream fs = new FileStream(keyFile, FileMode.Open))
{
KeyBytes = new byte[fs.Length]; fs.Read(KeyBytes, 0, (int)fs.Length);
}
KeyBytes = ProtectedData.Unprotect(KeyBytes,
null, DataProtectionScope.LocalMachine);
algorithm.FromXmlString(Encoding.UTF8.GetString(KeyBytes));
}
This time the ReadKey method is used by the decryption function only. The EncryptData() function requires the caller to pass in the XML string representation of the public key returned by the GenerateKey method, because the private key is not required for encryption. Encryption and decryption with RSA takes place as follows:
