AddThis Social Bookmark Button

Print

Basic Crypto w/ the .NET Framework

by Ben Lowery
02/10/2003

Overview

The .NET Framework offers basic support for cryptographic operations inside of the System.Security.Cryptography namespace in the mscorlib assembly. Out of the box, you are provided with implementations of many common symmetric key and public key-based algorithms. In addition, the cryptography framework was designed to be extensible, so that your implementation of any algorithm can be plugged in quite easily.

This article will show you how to use some of the classes provided to encrypt and decrypt data using symmetric keys, sign messages using public key crypto, and generate hashes of passwords for secure storage. Just remember, cryptographic algorithms are not a silver bullet that will solve all of your security problems! For a better understanding of why and when you should use these techniques, please see other sources, such as Bruce Schneier's Applied Cryptography or Howard & LeBlanc's Writing Secure Code.

All of the examples shown here can be downloaded from my web site.

A Basic Example

Let's start with some code showing how to take the contents of one file and encrypt them into another file. We'll then decrypt the contents back into a third file.


const string s_plaintext = 
             "plaintext.txt";
const string s_ciphertext = 
             "simple-ciphertext.bin";
const string s_decrypted = 
             "simple-plaintext-decrypted.txt";

void Run() 
{
  using(SymmetricAlgorithm algo = 
	      SymmetricAlgorithm.Create("Rijndael")) 
	{
    Encrypt(algo);
    Decrypt(algo);
  }

}

void Encrypt(SymmetricAlgorithm algo) 
{
  using(Stream cipherText = GetWriteableFileStream(s_ciphertext))
  using(ICryptoTransform enc = algo.CreateEncryptor())
  using(CryptoStream crypt = GetWriteCryptoStream(cipherText, enc))
  using(Stream input = GetReadOnlyFileStream(s_plaintext)) {
    Pump(input,crypt);
    crypt.Close(); //have to call, not called by Dispose
  }
}

void Decrypt() 
{
  using(Stream cipherText = GetReadOnlyFileStream(s_ciphertext))
  using(CryptoStream decrypt = GetReadCryptoStream(cipherText, decryptor))
  using(Stream output = GetWriteableFileStream(s_decrypted)) {
    Pump(decrypt,output);
    decrypt.Close(); // have to call, not called by Dispose
  }
}

static 
void Pump(Stream input, Stream output) 
{
  byte[] buffer = new byte[1024];
  int count = 0;

  while((count = input.Read(buffer, 0, 1024)) != 0) {
    output.Write(buffer, 0, count);
  }
}

static 
FileStream GetReadOnlyFileStream(string path) 
{
  return new FileStream(path, 
                        FileMode.Open, 
                        FileAccess.Read);
}

static 
FileStream GetWriteableFileStream(string path) 
{
  return new FileStream(path, 
                        FileMode.OpenOrCreate, 
                        FileAccess.Write);
}

static 
CryptoStream GetWriteCryptoStream(Stream stream, 
                                  ICryptoTransform transform) 
{
  return new CryptoStream(stream, 
                          transform, 
                          CryptoStreamMode.Write);
}

static 
CryptoStream GetReadCryptoStream(Stream stream, 
                                 ICryptoTransform transform) 
{
  return new CryptoStream(stream, 
                          transform, 
                          CryptoStreamMode.Read);
}

As you can see, the cryptography namespace was designed using a few different pieces. There's a lot here, so let's take it a piece at a time. First, let's take a look at the Run method:

  using(SymmetricAlgorithm algo = SymmetricAlgorithm.Create("Rijndael"))

This line creates a SymmetricAlgorithm and sets that algorithm up to be disposed. Algorithms are the heart of the cryptography namespace, so we'll look at them first. Just as a preview of things to come, let's quickly dig into the Encrypt method:


  using(ICryptoTransform enc = algo.CreateEncryptor())
  using(CryptoStream crypt = GetWriteCryptoStream(cipherText, enc))

These two lines set up an ICryptoTransform and a CryptoStream. ICryptoTransform is the thing that actually does the work of transforming blocks from plain text to cipher text and back again. A CryptoStream lets us view the world of symmetric key and one-way hash-based cryptography through the rose-colored glasses of System.Stream. Together, they allow us to put different pieces of the puzzle together in interesting ways. We'll talk about them later in the article.

Algorithm Inheritance Model

Algorithms are the heart of any cryptography library. An algorithm describes how you are going to transform bytes from one format to another. Within the .NET cryptography namespace, different classes of algorithms are designated using an inheritance hierarchy. The hierarchy has three roots: SymmetricAlgorithm, HashAlgorithm, and AsymmetricAlgorithm. Symmetric algorithms include common block ciphers such as RC2, 3DES, and Rijndael. These are used to quickly encrypt and decrypt information using a shared secret key. Hash algorithms include MD5 and SHA1, as well as some keyed hashes. Hashing is a quick non-reversible transform, and is generally used for information verification purposes. Passwords and message digests are often stored in a hashed format. Asymmetric algorithms include the public-key algorithms RSA and DSA. Asymmetric algorithms are generally used to sign messages and for key exchange. They have the benefit of not requiring both parties to share a secret, though they are significantly slower than Symmetric algorithms. Each class of algorithm has its purpose, and understanding when to use each will strengthen your application of cryptography. Again, for more information, see Applied Cryptography. Figure 1 shows the inheritance hierarchy.

System.Security.Cryptography partial hierarchy
Figure 1. System.Security.Cryptography Partial Hierarchy

Each algorithm is separated into a base class that defines the algorithm and an implementation class that performs the work. The base class offers an overloaded static method called Create that will create an instance of an algorithm implementation object. The factory method allows a developer to decouple their assemblies from an actual implementation of any algorithm. Indeed, by using only the factory method on the algorithmic category class (such as SymmetricAlgorithm), a developer could isolate their code from the actual algorithm employed. The code below shows how to create a Rijndael algorithm object using three different methods.


  SymmetricAlgorithm s1 = new RijndaelManaged();
  SymmetricAlgorithm s2 = Rijndael.Create();
  SymmetricAlgorithm s3 = SymmetricAlgrotihm.Create("Rijndael");

Notice that you can create an implementation object directly using new, just like most other objects. I'm not sure why the crypto team chose to implement it this way, other than this implementation allows developers their choice of ways to create the algorithm object. In my opinion, it just complicates things by offering more than one way to create an instance of an implementation object, and there's no good reason to expose the RijndaelManaged object to the public. It may be faster, though I have not profiled it.

So, how does the framework know which class to instantiate? The crypto framework takes advantage of the CLR's configuration system to determine what class implements each algorithm. Developers can tap into the configuration settings used to bind algorithm names to implementation types, allowing you to substitute implementations or add new ones. At this point, there is precisely one implementation for each algorithm. There may be a small market here for alternative implementations that take advantage of accelerator cards, or for implementations of algorithms not present in the CLR, such as IDEA or RC5.

Using an Algorithm

Now that we have an algorithm, we need to use it to do something. Another concept used by the crypto framework comes from a desire to make working with symmetric and hashing algorithms easier. Generally, when a developer wants to encrypt or decrypt something, they're going to use a symmetric algorithm or a hashing algorithm, and they're going to be processing data coming from a System.IO.Stream-based class. Asymmetric algorithms, which are significantly slower than symmetric algorithms and hashing, are not included in the model, as you would rarely use one to encrypt large amounts of data.

To make working with stream-based data easier, the crypto framework team designed the CryptoStream class and the ICryptoTransform interface. CryptoStream derives from System.IO.Stream and enables developers to chain multiple operations together into one stream. The operation to perform is specified by the ICryptoTransform supplied to the CryptoStream constructor. From the code above:


void Encrypt(SymmetricAlgorithm algo) 
{
  using(Stream cipherText = 
        GetWriteableFileStream(s_ciphertext)) 
  using(ICryptoTransform enc = 
	      algo.CreateEncryptor())
  using(CryptoStream crypt = 
        GetWriteCryptoStream(cipherText, enc))
  using(Stream input = 
        GetReadOnlyFileStream(s_plaintext)) {
    Pump(input,crypt);        
    //have to call, not called by Dispose
    crypt.Close(); 
  }
}

static 
CryptoStream GetWriteCryptoStream(Stream stream, 
                                  ICryptoTransform transform) 
{
  return new CryptoStream(stream, 
                          transform, 
                          CryptoStreamMode.Write);
}

static 
CryptoStream GetReadCryptoStream(Stream stream, 
                                 ICryptoTransform transform) 
{
  return new CryptoStream(stream, 
                          transform, 
                          CryptoStreamMode.Read);
}

Okay, so what's going on here? First, let me explain that big stack of using(..) statements. This wasn't perfectly clear the first time I saw it, but the construct is rather handy. Remember that the scope of a block statement like using is either the next line, or the statements between { and }. Well, you can nest the "next-line" rule, with the end result of stacking using statements like this to avoid having to use all the braces. The Dispose() methods will be called from the inside out, so this is really a handy way to use a bunch of disposable objects together. Pretty much all of the objects in the crypto namespace implement IDisposable, so you'll see lots of code with using blocks. Now that we understand that, let's go through the code bit by bit.

  using(Stream cipherText = GetWriteableFileStream(s_ciphertext))

The first statement sets up the file stream to which we're going to write our encrypted bytes.

  using(ICryptoTransform enc = algo.CreateEncryptor())

The next statement uses the factory method on the algorithm class to create an encrypting ICryptoTransform. This transform knows how to take a chunk of bytes and encrypt them using the algorithm.

  using(CryptoStream crypt = GetWriteCryptoStream(cipherText, enc))

Here we create the CryptoStream. Notice that we're passing in another Stream, the one we wish to write to, and the transform. I wrap up the process in a little helper method to save on some typing.

  using(Stream input = GetReadOnlyFileStream(s_plaintext)) {

Now we're grabbing a Stream that represents the input file, which we're going to send through the CryptoStream and down into the cipherText stream.

  Pump(input,crypt);
  crypt.Close(); //have to call, not called by Dispose

Okay, last step in the process. Here, we're pumping all of the data from the input stream into the output stream. What happening is we're reading from the plaintext file and then writing the result into the CryptoStream. The CryptoStream takes the bytes, transforms them, and then writes them into the underlying Stream (in this case, a FileStream). Pretty simple, really. That little call to crypt.Close() is of particular interest, though. Remember that we're inside a using block for the CryptoStream and that Dispose() is going to be called on that CryptoStream when it leaves the current scope.

Generally, objects derived from System.IO.Stream do not implement IDisposable on their own, but instead override Stream.Close() and do any necessary cleanup work there. Stream implements IDisposable as a virtual call to Close(), so in general, subclasses don't have to do anything other than override Close(). Dispose() is generally equivalent to Close() for Stream-derived classes.

Unless, of course, you're CryptoStream! CryptoStream implements IDisposable on its own, which does not call Close()! CryptoStream.Dispose() instead clears out its internal buffers and does nothing more. It does not flush the remaining bytes in the buffer to the underlying Stream or do quite what you would expect. This means that you have to call either FlushFinalBlock() or Close() on the CyptoStream to get a valid encrypted or decrypted stream. The difference is that CryptoStream.FlushFinalBlock() will not call Close() on the underlying stream, while CryptoStream.Close() will. Which one you use depends on how you'd like to write your code.

Also, be aware that the code below will not call CryptoStream.Close (or Stream.Dispose, for that matter):

  using(Stream s = new CryptoStream(...)) { ... }

When the using asks for the IDisposable interface, the CryptoStream hands back its implementation, not the Stream's implementation. Remember that just like in COM, when you query an object for an interface, the object must always return the same instance. It's just not always obvious on reading the code that you're going to get CryptoStream's implementation of IDisposable, and that there is, in fact, no way to call Stream's implementation directly.

Is this a bug? I'm not sure. CryptoStream's Close() calls Close() on the underlying Stream as well, which changes the semantics of Stream.Close() a bit. It's a tough call. I think that CryptoStream.Dispose() should call FlushFinalBlock() at the very least, though. Having to remember to call either Close() or FlushFinalBlock() is a bit a pain and has led to a number of hours spent banging my against a wall in frustration.

In the example above, if you want to change the algorithm being used, you can simply change the string passed to SymmetricAlgorithm.Create(). Everything else will just work itself out. By default, the SymmetricAlgorithm that's created is configured using secure defaults (PKCS7 padding, and it operates in CBC mode) with a randomly-generated key and initialization vector. If you need to specify a key or IV, or want to change the padding or chaining mode, you can access all of those things via properties on the algorithm. Just check the LegalKeySizes property to determine if the key size you wish to use is valid. Also, be aware that PaddingMode.Zeros is broken in the v1 implementation of the Framework. If you use this padding mode for backwards-compatibility purposes, you'll have to find some way to determine the number of bytes you're going to pull from the stream before you start. Prefixing the stream with the number of bytes contained within is always a good way.

Lastly, figuring out the Decrypt function is left as an exercise to the reader.

Pages: 1, 2

Next Pagearrow