Security Briefs

Encrypting Without Secrets

Keith Brown

Code download available at:SecurityBriefs0601.exe(150 KB)

Contents

Thinking About Encryption
Using Public Key Cryptography in Managed Code
How Smart is Your Key Container?
Key Containers and the Key Manager
Key Size
The Encryptor and Decryptor
The Sample Encrypting Agent (Take Orders)
The Sample Decrypting Agent (Process Orders)
Moving Ahead

Do you have a Web site or other system that deals in secrets of any sort? It seems like every time I give a security talk, people ask how to deal with the sticky problem of storing secrets. Connection strings with passwords are an obvious problem. You're better off simply using integrated security to get rid of those secrets, at least with SQL Server™, or an Oracle database. But what about credit card numbers and other financial or personal information? Can encryption help?

Thinking About Encryption

If you're considering encryption, keep in mind that it's a secret compression or transfer technique. Encryption doesn't get rid of secrets, but it's good at turning big secrets (secret documents and so on) into smaller secrets called keys. But there's still a key somewhere! The trick is figuring out how to separate the key from the data to reduce the chances that an attacker could get both the encrypted data and the decryption key.

Figure 1 Encrypting and Decrypting Agents

Figure 1** Encrypting and Decrypting Agents **

Many systems that deal in secrets (credit card numbers, personal information, and so on) can be factored to allow the most highly exposed machines, such as Web servers, to accept secret data and encrypt it without ever having a secret key. Once the ciphertext is stored in a database or queue, a business process running on a dedicated and highly secure machine can retrieve the ciphertext and decrypt it. Figure 1 shows an example of factoring the encrypting and decrypting agents onto separate machines. The encrypting agent holds a public key, and the decrypting agent holds the corresponding private key. Here I'll present a sample application that uses this technique to protect a user's financial information while it's waiting to be processed.

Using Public Key Cryptography in Managed Code

There are a couple of ways to use public key cryptography in the Microsoft® .NET Framework. If you're working with a public key infrastructure (PKI), you can make use of the rich support for certificates in the version 2.0 base class library (BCL). But not everyone wants to manage a PKI, and this isn't really required for the ideas I'm presenting here. For those simpler cases, the BCL includes a couple of low-level classes that allow you to use public key cryptography directly, without the overhead of certificates. The class I'll be using here is RSACryptoServiceProvider. This class allows you to generate public/private key pairs and store the key material in CryptoAPI (CAPI) key containers on a file system or smart card. And, of course, you can also use this class to encrypt and decrypt data.

One thing you should keep in mind with public keys is that they are very good at encrypting small bits of data, such as secret keys or hashes. But they are not designed for encrypting bulk data. So my solution will use a typical hybrid cryptosystem: I'll generate a random secret key and use Advanced Encryption Standard (AES) to encrypt the actual data I want to protect. Then I'll use RSACryptoServiceProvider to encrypt the AES key.

How Smart is Your Key Container?

RSACryptoServiceProvider is a thin veneer over CAPI, where key material is managed in something called a key container. This container abstraction allows you to use a variety of crypto providers, including some that use hardware devices, like smart cards, to store key material. Smart cards are a great way to store private keys because the key material never leaves the card. The card has its own embedded memory and a microprocessor, so it can generate the key material you need, store it, and use it to encrypt and decrypt data without ever disclosing the private key to the main computer's memory. If there's ever any downtime on your decrypting agent (that is, if it doesn't run 24?7), an administrator can simply pull out the smart card to reduce the window of exposure to attack. When the private key is not available anywhere on the system, your encrypted data is much safer than when it is!

My solution will use the built-in cryptographic service provider (CSP) so that you won't need any special hardware to use the code in this column. But it wouldn't be difficult to tweak the code to support a smart card, and smart cards are not at all expensive. You can get a smart card reader and a set of five cards for less than 100 bucks, so don't let the fear of hardware costs prevent you from considering them.

Key Containers and the Key Manager

Before you can encrypt data using a public key, you need to generate a key pair, and that means learning a bit about key containers. Each CAPI key container can have up to two public/private key pairs. One is used for "key exchange," which is a fancy term that means that the key pair is used to encrypt and decrypt secret keys like the AES key I'll use to protect sensitive data. The other set is used for creating digital signatures, which I won't be using in this column.

You can have as many key containers as you need, limited only by the storage space on the device you're using. While technically you only need one key pair to be able to encrypt and decrypt data, my encrypting agent assumes that it may be asked to use different keys to encrypt data. It therefore tags each bit of encrypted data with a key name so that the decrypting agent knows which key to use for decrypting. This simple key versioning scheme allows you to easily swap out keys from time to time, in case a key is compromised or as a best practice since the longer a single key is used, the weaker and more risky that key becomes.

Since key management is essential, the first program I've written is a Windows® Forms app called Key Manager (see Figure 2), which allows an administrator to enumerate, create, and delete key containers on a machine. (This program could be tweaked to manage key containers on a smart card.) Each time you create a new key container, Key Manager generates a new RSA public/private key pair in that container and displays the public key details in a format that's easy to cut and paste into the <appSettings> section of the config file of the encrypting agent. These settings include not only the public key itself, but also the name of the key container, which the other parts of my solution use for key versioning (as you'll see later).

Figure 2 Key Manager Application

Figure 2** Key Manager Application **

Using Key Manager is easy. Just run it on the machine where you will host your decrypting agent. Select a key size and a name for your key container, like "MyApp-1" (you can use the hyphenated number for key versioning). Once you've created the key container for the decrypting agent, the private key is installed and ready to process data encrypted with the MyApp-1 key. By default, administrators on the machine as well as the user who created the key container will have permission to use the key. This is enforced by the access control list (ACL) on the key container file. Press the "Security..." button and Key Manager will launch the file properties dialog to allow you to configure this ACL to grant read access to the user account that your decrypting agent will use. Be sure to copy the public key configuration data that Key Manager provides and paste it into the encrypting agent's config file, which contains the key container name and the public key details. This is all the encrypting agent needs in order to encrypt data. By the way, if you have ever wondered where file system-based key containers are stored, take a look inside the following hidden directory:

\Documents and Settings\All Users\Application Data\Microsoft\Crypto\RSA\MachineKeys

CAPI also supports per-user containers, but given that you can control the ACL on each machine-level container, I have kept things simple by using machine-level key containers.

Key Size

The size of the key you generate impacts the level of security and inversely, the performance you'll get from the cryptosystem. Also, the longer the key you choose, the longer it'll take to generate in Key Manager. In a Virtual PC environment running on a reasonably fast machine, a 4096-bit key takes several seconds to generate, while a 16384-bit key takes several minutes! At the moment, Key Manager creates these keys on the user interface thread, which was expedient for a sample app, but an obvious improvement would be to generate the key asynchronously so the UI doesn't freeze up. Regardless of how long it takes to create the key pair, what you should care about is the security and performance trade-off at run time, so I ran some tests to time encryption and decryption using the five key sizes that Key Manager supports (see Figure 3).

Figure 3 Key Size and Runtime Performance

Figure 3** Key Size and Runtime Performance **

Pay attention to the units in Figure 3. Encryption is measured in milliseconds, but decryption is measured in seconds. Private key operations are not cheap in RSA. If performance is critical, you can purchase specialized hardware to offload encryption and/or decryption from the main CPU, dramatically increasing performance without sacrificing security. This will require an alternate CSP, which will require a bit of tweaking of my code, but that's a small price to pay for the performance benefit you'll see. From a security perspective, current recommendations are to stick with RSA keys that are at least 2048 bits long as an absolute minimum; 4096 bits seems to be the sweet spot in the performance/security curve today. And in the future, you can go even higher. As you can see from Figure 3, my own tests include keys up to 16384 bits long.

When I set out to write this application, I tried to use RSACryptoServiceProvider exclusively because it is the official managed interface for using CAPI key containers. But, it wasn't quite up to snuff for what I needed, so I performed a bit of P/Invoke magic to get to CAPI directly. This allowed me to enumerate key containers so you could quickly see all the key containers for the built-in RSA provider on your machine. The two important CAPI functions I rely upon are CryptAcquireContext and CryptGetProvParam. I've wrapped all these details in a managed class called KeyContainerManager, which you may find useful on its own.

The Encryptor and Decryptor

Besides KeyContainerManager, there are two other core classes in my solution, Encryptor and Decryptor, which you'll use in the encrypting and decrypting agents, respectively. While Decryptor needs the private key installed on the machine in a key container, Encryptor only needs the public key details provided via its constructor. An easy way to configure this is using the encrypting agent's configuration file. In my sample application, the encrypting agent is an ASP.NET Web application, so I simply configure the public key information in the Web.config file.

RSACryptoServiceProvider grew a couple of new methods in version 2.0 that allow you to easily access key information:

string ToXmlString(bool includePrivateParameters) void FromXmlString(string xmlParameters)

In fact, Key Manager calls ToXmlString(false) to extract the public key parameters as a string of XML, which it then encodes and puts into an <appSettings> configuration entry that you can paste into the encrypting agent's config file. My Encryptor class must be constructed with two bits of information: a key name and the public key parameters as an XML string:

public Encryptor(string keyName, string publicKeyXmlParams) { this.keyName = keyName; rsa.FromXmlString(publicKeyXmlParams); }

The keyName argument is how I've implemented key versioning. Whatever string you provide here will be added as a prefix to the ciphertext produced when you encrypt data. The Decryptor will pick off this prefix and use it as the key container it binds to when decrypting the data. You'll want this to be easily configured in the encrypting agent, which is why Key Manager also includes this configuration item. In the sample application's Web.config file, you'll find an <appSettings> element that looks something like this:

<appSettings> <add key="keyName" value="TestKeyContainer"/> <add key="publicKeyData" value="&lt;RSAKey..."/> </appSettings>

When the sample app needs to encrypt some data, it constructs a new Encryptor with these two pieces of data from Web.config and then calls Encrypt. When you want to change keys, you just need to update the Web.config file to indicate the new key name and public key data, and the application will immediately start using the new key with no downtime. And since each piece of encrypted data includes the key name, the decryptor always knows which key it should use for any given ciphertext. You just need to ensure that the machine hosting the decrypting agent has all the necessary keys installed.

The Encryptor.Encrypt method UTF8 encodes the string you pass in, then generates a random 256-bit AES secret key and a random 128-bit initialization vector (IV). It uses that to encrypt the data you pass in via the AES class called RijndaelManaged. (Rijndael is the name of the algorithm that won the contest to become the AES standard, so you can think of it as a synonym for AES. Its name is based on the surnames of the cryptographers who invented it and is pronounced something like "Rain-Doll".)

Figure 4 Data Format

Figure 4** Data Format **

The Encryptor now encrypts the AES key with the RSA public key and forms the data structure shown in Figure 4, a length-prefixed, encrypted AES key followed by the IV and the encrypted data. This binary blob of data is base64-encoded into a string, to which is prepended the key name that identifies which RSA key should be used to decrypt the AES key.

If all of this sounds very complicated, you'll be happy to know that using the Encryptor class, you can simply pass in a string to be encrypted, and you'll get back a string that contains all the information needed for Decryptor.Decrypt to do its job (well, everything except the private key material, of course). This makes the interface to the Encryptor and Decryptor very easy to use:

string Encryptor.Encrypt(string plaintext); string Decryptor.Decrypt(string ciphertext);

Of course, this isn't the only way to package the encrypted blob. You may find it more convenient to separate out the key name from the blob, or deal in byte arrays instead of strings, but those are easy changes to make once you understand the overall architecture of this solution. Another thing you should consider up front in a real application is format versioning. Don't wait until version 2 to put a version number on those encrypted blobs!

Decryptor.Decrypt unwinds the data structure by first peeling off the key container name and constructing an RSACryptoServiceProvider instance bound to that key container. It then decodes the base64-encoded payload and peels off the encrypted AES key, decrypting it using RSACryptoServiceProvider.Decrypt. That step is likely the most CPU-intensive part of the entire process if you're encrypting relatively small strings. At this point an AES key, represented by an instance of the RijndaelManaged class, is initialized with the decrypted secret key and the IV in the payload, and the encrypted data is decrypted. Finally, the decrypted byte array is decoded into a string via UTF8 and returned as the result of Decryptor.Decrypt.

The cipher mode my solution uses is cipher block chaining (CBC), a very popular scheme that happens to be the default in the .NET Framework. This cipher mode does not provide integrity protection on the payload. That is, when you decrypt the data, you aren't guaranteed to be notified if the ciphertext was tampered with. While integrity protection is not a goal of this particular solution, based on my experience a lot of people believe they get integrity protection automatically when they encrypt data. Often, when you decrypt CBC-encrypted ciphertext that has been modified, you'll get an exception when the padding on the end isn't what was expected, but this isn't a real integrity guarantee. It won't stop a determined attacker.

If you're interested in learning more about cipher modes, there's a very good article on the topic at Wikipedia: Block cipher modes of operation. And, of course, my favorite book for understanding how cryptography fits together is Practical Cryptography by Niels Ferguson and Bruce Schneier (Wiley, 2003).

The Sample Encrypting Agent (Take Orders)

I've pulled these classes together into a sample application you can build with Visual Studio® 2005, and I encourage you to try it out. You've already seen the first part, Key Manager, which is used to administer the application's keys. I've also constructed a sample encrypting agent called Take Orders, a Web application that accepts a few pieces of private data in a simple user interface: an e-mail address, credit card number, and expiration date. Take Orders pops this data into an XML string and uses an instance of Encryptor to encrypt the entire string.

Figure 5 Orders Waiting to be Processed

Figure 5** Orders Waiting to be Processed **

Now I could have pushed each encrypted order into a message queue or database, but I wanted to make the application trivial for you to deploy, so I chose simply to write the data to a file and drop it into a directory of your choice. You'll need to configure a directory for Take Orders to use. Be sure to set the ACL on the directory so that the Web application has permission to write into it. The Web.config file for Take Orders includes an <appSettings> element called dataDir which you should point to this directory. Figure 5 shows what this directory looks like after a bit of use, as well as the innards of one of the files (you should recognize the key name at the front, separated from the base64-encoded blob by a pipe symbol). Note that each file is named with a GUID for uniqueness.

The Sample Decrypting Agent (Process Orders)

Figure 6 shows the Process Orders application, a Windows Forms app that displays each order based on the date the file was created. Selecting an order shows the contents of the encrypted file. The file can then be decrypted by pressing the Decrypt Order button, which uses an instance of the Decryptor class to unwind the order file and decrypt the message. Process Orders then displays the details of the order including the e-mail address, credit card number, and expiration date supplied by the user.

Figure 6 Process Orders Application

Figure 6** Process Orders Application **

At this point you can press the Process Order button to send an e-mail to indicate that the order has been processed. It also deletes the encrypted order file from disk, making this system behave very much like a queue. If you use your own e-mail address and SMTP server, you'll actually get an e-mail. This little extra uses the newly refactored SmtpClient class that you can find in the System.Net.Mail namespace, new to the .NET Framework 2.0.

Moving Ahead

I encourage you to think about novel ways of dealing with sensitive data. By factoring your system into agents that encrypt and decrypt data independently of one another, it's possible to significantly limit the exposure of sensitive data. Of course, the threat of information disclosure is never going to go away completely. If your decrypting agent is compromised, so is your sensitive data (or at least data that's encrypted using keys that the decrypting agent currently holds). Keep in mind that a smart card makes it easier to completely remove private key material when the decrypting agent is not active, limiting exposure even further. So take this application, play with it, and think of ways to put the ideas behind it to use.

Send your questions and comments for Keith to  briefs@microsoft.com.

Keith Brown is a cofounder of Pluralsight, a Microsoft .NET training provider. Keith is the author of Pluralsight's Applied .NET Security course, as well as several books, including The .NET Developer's Guide to Windows Security, which is available both in print and on the Web. Learn more at www.pluralsight.com/keith.