Basic Crypto w/ the .NET Framework
by Ben Lowery02/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.
|
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 |

