Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro ASP.NET 2.0 In CSharp 2005 (2005) [eng]

.pdf
Скачиваний:
107
Добавлен:
16.08.2013
Размер:
29.8 Mб
Скачать

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

849

Managing Secrets

Before you learn the details of using the encryption classes, you have to think about one additional thing: where do you store the key? The key used for encryption and decryption is a secret, so it must be stored securely. Often developers think the best way to store such a key is in source code. However, storing secrets in source code is one of the biggest mistakes you can make in your application. Imagine that you have the following code in the code of a class library that will be compiled into a binary DLL:

public static class MyEncryptionUtility

{

// Shhh!!! Don't tell anybody!

private const string MyKey = "m$%&kljasldk$%/65asjdl";

public static byte[] Encrypt(string data)

{

// Use "MyKey" to encrypt data return null;

}

}

Keys such as this can easily be revealed through disassembling tools. You just need to open ILDASM and analyze your class. Of course, you definitely will be able to find this secret, as shown in Figure 25-5.

Figure 25-5. ILDASM with the previous class and the secret

850 C H A P T E R 2 5 C RY P TO G R A P H Y

If you think this is a problem in the managed world only, try something similar with an unmanaged C++ application. Create a class, and include the secret as a constant value in your application. Because constant values are stored in a special section of native executables, perform the following steps:

1.Install the Microsoft platform SDK.

2.Open a command shell, and execute the following command: dumpbin /all BadProtectCPlus.exe /out:test.txt

3.Open the generated file test.txt with Notepad, and scroll to the .rdata section. Somewhere in this section you will find your hard-coded secret.

So, you definitely have to protect the key somehow. You might want to encrypt the key on its own, but then you need another encryption key.

Windows supports a built-in mechanism for storing and protecting secrets. This mechanism uses a machine key generated with the system installation for encrypting data. Only the local operating system (the system’s local security authority) has access to this machine key. Of course, the machine key is unique for every installation. Windows supports the Data Protection API for protecting data with this key. You don’t have direct access to the key when using this API; you just tell the system to encrypt or decrypt something with the machine’s key. So, this solves the problem of key management: your application could encrypt the key used by your application through DPAPI. For this purpose, the .NET Framework supports the class System.Security.Cryptography.ProtectedData, which you can use as follows:

byte[] ProtData = ProtectedData.Protect(

ClearBytes, null, DataProtectionScope.LocalMachine);

Possible scopes are LocalMachine and CurrentUser. While the first option uses the machine key, the second one uses a key generated for the currently logged-on user’s profile. (In the case of roaming profiles, this key is machine independent.) If a user is the administrator of the machine and has the necessary know-how, he can decrypt the data by writing a program that calls the previous function. However, this definitely “raises the bar” and makes it harder to access the key. And if the user is not the administrator and has no permission to use the DPAPI, she cannot decrypt data encrypted with the machine key.

Caution Don’t use the DPAPI to encrypt information in your database. Although it is easy to use the DPAPI with

.NET 2.0, this method has one problem: encrypted data is bound to the machine if you use the DataProtectionScope.LocalMachine setting. Therefore, if the machine crashes and you have to restore your data on another machine, you will lose all the encrypted information. If you use the DPAPI for encrypting the key as described previously, you should have a backup of the key in another secure place. If you want to use the DPAPI in web farm scenarios, you have to run your application under a domain user account and use the key created for the user’s profile (DataProtectionScope.CurrentUser). We recommend creating a separate domain for your web farm so that you don’t have to use a domain user of your company’s internal domain network.

Using Symmetric Algorithms

As mentioned, symmetric encryption algorithms use one key for encrypting and decrypting data. The class you will create basically has the following structure and can be used for encrypting and decrypting string data:

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:

C H A P T E R 2 5 C RY P TO G R A P H Y

857

public static byte[] EncryptData(string data, string publicKey)

{

//Create the algorithm based on the public key RSACryptoServiceProvider Algorithm = new RSACryptoServiceProvider();

Algorithm.FromXmlString(publicKey);

//Now encrypt the data

return Algorithm.Encrypt( Encoding.UTF8.GetBytes(data), true);

}

public static string DecryptData(byte[] data, string keyFile)

{

RSACryptoServiceProvider Algorithm = new RSACryptoServiceProvider();

ReadKey(Algorithm, keyFile);

byte[] ClearData = Algorithm.Decrypt(data, true); return Convert.ToString(

Encoding.UTF8.GetString(ClearData));

}

Now you can build a test page, as shown in Figure 25-7. (You can find the source code of this page in the book’s downloads.)

Figure 25-7. A sample test page for asymmetric algorithms