One way .NET benefits Windows developers is by bringing together previously separate APIs and SDKs under one framework. For example, consider the adaptation of the CryptoAPI to the .NET System.Security.Cryptography namespace. The cryptographic services have left their mysterious corner of the Platform SDK to become, in a sense, “just another .NET namespace.” Of course, there is more to it than that, but the point is that the cryptographic services are more approachable because of what they share with the rest of the framework as a whole. Now, you just have to learn what the System.Security.Cryptography namespace does and which classes are appropriate for specific situations.
Grab the code
You can download the .cs files for this article here.
The namespace contains classes that implement security solutions such as:
- · Encryption and decryption of data.
- · Management of persisted encryption keys.
- · Verification of the integrity of a piece of data to ensure that it has not been tampered with.
I will limit this article to encryption and decryption, but keep in mind that this is only one piece of the puzzle; a truly secure solution will make use of the other pieces as well. Our examples start with the encryption of a local text file and then move on to the more complicated encryption of messages between networked computers.
To encrypt a local text file, we use one of the symmetric algorithms; symmetric because the same key and initialization vector (IV) are used to both encrypt and decrypt a piece of data. (The IV’s relationship to the key is explained in the Cryptography Overview section of the .NET documentation.)
.NET implementations of symmetric algorithms derive from a common abstract base class, SymmetricAlgorithm, highlighting that the programmer can treat each of the specific algorithms—DES, TripleDES, and Rijndael—in the same fashion. The algorithms differ in how they encrypt the data, but the public interfaces are the same. This doesn’t mean that all algorithms are equal. For instance, as you may have guessed by the name, TripleDES is a more secure successor to DES.
Because the same key encrypts and decrypts data, symmetric algorithms are best suited for situations where the key does not need to be broadcast. Network encryption calls for a combination of asymmetric and symmetric algorithms, as you’ll later see. But first let’s put the symmetric algorithms to good use.
Encrypting a text file
Listing A contains a console program, TextFileCrypt, which encrypts a text file you specify on the command line. The top of Listing A shows how to invoke the program. Let’s look at some of the more important pieces of the code.
The symmetric algorithms work by encrypting data as it passes through a stream. We create a “normal” output stream (such as a file I/O stream), followed by an instance of the CryptoStream class, which will then piggyback on that normal stream.
You write byte arrays to the CryptoStream, and as the data streams through, it gets encrypted and put into the normal stream. To put the original text file into an array of bytes to be fed to the CryptoStream, you employ the FileStream class to read it. You also use another instance of FileStream as the output mechanism that the CryptoStream will hand the encrypted data to.
FileStream fsIn = File.Open(file,FileMode.Open, FileAccess.Read);
FileStream fsOut = File.Open(tempfile, FileMode.Open,FileAccess.Write);
It’s all about streams
.NET makes considerable use of streams to read and write data. In fact, the symmetric algorithm classes require you to use them. If you aren’t comfortable with .NET’s stream-based input and output, I encourage you to familiarize yourself with it, perhaps by reading this article.
We can instantiate and use any one of the symmetric algorithm providers while specifying the object variable as the abstract type SymmetricAlgorithm. I chose Rijndael, but you could just as easily instantiate DES or TripleDES:
SymmetricAlgorithm symm = new RijndaelManaged();
// could just as easily be “new TripleDESCryptoServiceProvider()”
.NET sets these provider instances with strong random keys. It can be dangerous to try to choose your own keys; acceptance of the “computer-generated” key is good practice.
Next, the algorithm instance provides an object to perform the actual data transformation. Each algorithm has CreateEncryptor and CreateDecryptor methods for this purpose, and they return objects implementing the ICryptoTransform interface:
ICryptoTransform transform = symm.CreateEncryptor();
Finally, a special CryptoStream is instantiated and told which underlying stream it should piggyback on, which object will perform the transformation of the data, and whether the purpose of the stream is to read or write data:
CryptoStream cstream = new CryptoStream(fsOut,transform,CryptoStreamMode.Write);
Now you simply write the byte-array version of the original file to the CryptoStream. You do this by reading the original file with a BinaryReader, whose ReadBytes method returns a byte array. In the following snippet from Listing A, the BinaryReader reads the input stream of the original file, and its ReadBytes method is called as the byte array parameter to the CryptoStream.Write method. Note the important call to the FlushFinalBlock method of CryptoStream:
BinaryReader br = new BinaryReader(fsIn);
The result is a temporary file containing the encrypted version of the original file. Listing A then reverses the process, decrypts the temporary file, and displays the decrypted text to the console so you can feel comfortable that the round-trip from encryption to decryption actually works. I won’t show each line of the decryption here, but the important difference is that the algorithm provides a decrypting instance of ICryptoTransform using CreateDecryptor, and then a new CryptoStream is used to read the encrypted file.
If you encrypt and decrypt files over multiple Windows sessions, you will want to persist and recall the symmetric key and IV. They are available in the provider class objects as byte arrays (e.g., TripleDESCryptoServiceProvider.Key), so technically, you could save them directly to a file. That’s dangerous. A better solution is to use the key management facilities in the System.Security.Cryptography namespace. Specifically, you would use the asymmetric providers such as RSA to store your symmetric keys. The key management facilities are beyond the scope of this article, but you can read more about the CspParameters class and the RSA provider’s PersistKeyinCsp property in the MSDN .NET documentation.
The final example makes use of both symmetric and asymmetric algorithms. Asymmetric algorithms, such as RSA and DSA, deal with two keys, the “public” and “private” keys. Together, they can help securely send data over networks, as the following scenario shows.
If I have a document that I want only you to see, I shouldn’t simply e-mail it to you. I could encrypt it using a symmetric algorithm; then if anybody grabbed it along its way, they wouldn’t be able to read it because they wouldn’t have the single key that was used to encrypt it. But neither would you. I have to somehow get you the key so that you can decrypt the document, but without risking someone else intercepting both the key and the document.
Asymmetric algorithms are the solution. The two keys that these algorithms produce have the following relationship: Anything encrypted with the public key can be decrypted only with the companion private key. So I should first ask you to send me your public key. Anyone else can grab it on its way to me, but it doesn’t matter, since that just enables them to encrypt things for you. I use your public key to encrypt the document then send it to you. You decrypt it with your private key, which is the only thing that can decrypt it, and which you have not sent over the wire.
The asymmetric algorithms are computationally expensive and slower than the symmetric ones, so we don’t want to asymmetrically encrypt everything in our online sessions. Instead, we can go back to using symmetric algorithms. As the next example shows, we merely use asymmetric encryption to encrypt the symmetric key. Then, we use symmetric encryption from that point forward.
Encrypting network data
Although it’s a simplification, the description above is pretty much what the Secure Socket Layer (SSL) does to create secure sessions between browser and server. The idea is also put into practice in Listing B and Listing C.
Listing B is a small TCP server program you can run on your own computer in one process. You can then run the client contained in Listing C in another process (i.e., use two command windows). Near the top of each listing is a comment showing how to invoke the program at the command line.
- · Receives a public key from the client.
- · Uses that public key to encrypt a symmetric key that can be used by both.
- · Sends the encrypted symmetric key to the client.
- · Sends the client a secret message encrypted with the symmetric key.
- · Creates and sends a public key to the server.
- · Receives an encrypted symmetric key from the server.
- · Decrypts that symmetric key using its private asymmetric key.
- · Receives and decrypts a secret message encrypted with the symmetric key.
Upon startup, the client creates its own instance of the RSACryptoServiceProvider class. When instantiated, the object contains strong default keys. The client needs to get the public key out of this RSA object and send it to the server. The public key is extracted using the ExportParameters method, resulting in an RSAParameters object that holds the public key. How do we send this object to the server? We can use .NET’s binary serialization, from the System.Runtime.Serialization.Formatters.Binary namespace:
NetworkStream ns = client.GetStream();
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(ns,key); // where key is the RSAParameters object
The BinaryFormatter writes directly to streams, and in this case, it writes the serialized version of the RSAParameters to the network stream. The server receives those bytes and deserializes them into an RSAParameters object:
result = (RSAParameters)bf.Deserialize(ms);
// ms is a memory stream containing the bytes sent by client
// bf is a BinaryFormatter
Now the server creates a symmetric key and IV that both sides can use and encrypts them using the client’s public key:
symKeyEncrypted = rsa.Encrypt(symm.Key, false);
symIVEncrypted = rsa.Encrypt(symm.IV, false);
// symKeyEncrypted and symIVEncrypted are byte arrays
Unlike the symmetric providers, the asymmetric providers encrypt to a byte array, not to a stream. The byte array can then be sent to the client using the NetworkStream.
Once the client receives the encrypted versions of the symmetric key and IV, it decrypts them using its own private asymmetric key. Now both sides have an agreed-upon symmetric key and IV. From this point forward, they send each other data that is encrypted using only the symmetric key; the asymmetric algorithm has served its purpose and need not be used again.
We feel comfortable using the symmetric algorithms to encrypt local data. We can choose from multiple algorithms while keeping the code generic by typing them as the abstract SymmetricAlgorithm class. The algorithms make use of transformer objects to actually encrypt the data as it passes through the special CryptoStream. When we need to send the data over a wire, we first encrypt the symmetric key itself using the recipient’s public asymmetric key.
It’s important to restate in closing that encryption is just one of the services offered in the System.Security.Cryptography namespace. For instance, although the techniques in this article would guarantee that only a certain private key could decode the message encrypted with its companion public key, they do not guarantee anything about who sent the original public key; it could have been an impostor. Classes dealing with digital certificates would also have to be employed to address that risk.