Written by Steve Johnson
Published on December 15, 2004
Reader Rating:
In this article I will explain the classes and code necessary to put encryption to work in your applications. I will also explain the basic principles of operation of these classes and point out several pitfalls to avoid.
In today's hostile, interconnected computing environment, we often find the need to protect data while it is stored on a physical medium or transmitted over a network. To achieve this goal, we turn to cryptography. While the field of cryptography entails much more, in this article we will focus on encryption; specifically the ciphers available in the .NET Framework Class Library (FCL) and how to correctly utilize them.
There exists a group of developers who approach the subject of cryptography with a cavalier attitude, who think they can wave the wand of cryptography at their applications and make them secure. This group of developers mistakenly believes that applying cryptography is fool-proof and rests secure in the fact that their applications utilize "128-bit encryption" or comply with some other such buzzword. I hope that this article will awaken this group to the many pitfalls and subtleties that can silently render your crypto system easily attackable.
While the application of encryption technologies to applications is a subject that must be approached with a modicum of care and understanding, the FCL has made excellent cryptographic technologies more accessible than ever, though there are still a number of principles that must be adhered to and pitfalls to be avoided.
Although there are several reasons why you shouldn't do this, I'll just state the primary reason: You'll get it wrong. While the application of standard, public, seasoned algorithms is relatively simple and accessible to the common developer, the design of the cryptographic algorithms themselves is extraordinarily difficult and is best left to the elite of the field. While the output of your cipher may look completely secure to you, a skilled cryptanalyst can likely easily find many weaknesses, such as simple algebraic relationships between input, output and key. The tiniest weakness can reveal information about the plaintext or, worse yet, the key. Unless you happen to be an elite cryptographer, stick with standard, public, seasoned algorithms and proven implementations.
A cipher is a cryptographic algorithm that transforms a plain text input into an encrypted output using a secret key. The output of a cipher is called "cipher text". There are two basic types of ciphers, block ciphers and stream ciphers. A block cipher transforms fixed-size plain text blocks into fixed-size cipher text blocks. A stream cipher doesn't actually encrypt the plain text. Instead, a stream cipher transforms plain text into cipher text by generating what is know as a key stream and then XORing the key stream bytes with the plain text bytes to yield the cipher text. All of the ciphers provided in the FCL are block ciphers. However, by using what is known as a cipher mode, a block cipher can be caused to behave as a stream cipher. We will discuss cipher modes later in this article, though we will not specifically discuss streaming modes.
Have you ever seen advertisements of "128-bit Encryption" or similar? How about the IE About dialog, which states (on my computer) "Cipher Strength: 128-bit"? What does this mean and why does it matter? Usually, when you hear about Cipher Strength or Encryption Strength, what is referred to is the length of the key used by the underlying cipher. This length is important, but it does not tell the whole story.
Though there are many possible attacks against a cipher, the simplest attack is what is known as a "brute force" attack. The brute force attack simply tries all possible key values to decrypt one or more blocks of data until a sensible value is achieved. Since the plain text is highly non-random, it will be obvious to the attacker when the correct key has been chosen. This process is simplified when the attacker actually knows what the plain text is. This is more likely than you might think. Most communications protocols, for example, have standard session headers, message headers, etc, so it's not terribly difficult for the attacker to figure out what these blocks of data contain, but I digress.
In order to demonstrate the value of a long key, let's reduce the problem ad ridiculum. Let's say that you are using an 8-bit key. In this case, the attacker only has to perform a maximum of 256 guesses before he knows the key. In reality, though, he'll only have to perform less than half that many steps before his chances of success begin to exceed 50%. In the case of a 40-bit key, the attacker will have to perform a maximum of just over a trillion steps, but in reality, about 500 billion would give him a good chance of success. This is completely feasible given time and the power of modern processing. Even 64-bit keys are now being considered weak, and 128-bit is pretty much the standard today. Let's keep one thing in mind, though; 56-bit keys were considered safe 20 years ago or even less. Why are they not safe today? Well, it's because of the rapid advance of processing technology, a process that is likely to continue for the foreseeable future. When we build systems today that implement encryption, we need to consider how long our system will be in place and take into consideration what the technology will look like well into the future.
Though the length of the key is important, there are also other considerations. It is equally important to consider how the key is selected. Many systems derive keys from sources such as passwords. The problem with this is that, though the resultant key is long, the source is usually much shorter and highly non-random. Users simply cannot and/or will not remember good passwords. Suppose we derive a 128-bit key from a 6 character password. In this case, how random is the key? That's right; it is only as random as the source from which it is derived. Since we must assume that the attacker knows our key-derivation algorithm, he really only needs to attack our 6-character password, a problem that is much easier to solve than attacking the full 128 bits of the key. Don't use a password-derived key to encrypt data. If you must use a password, your only acceptable option is to require a long password that contains at least as many bits of entropy as the key length. Entropy essentially means unpredictability or randomness, and there are public formulas available for calculating the entropy of a string password. You'll want to implement such an algorithm to verify the strength of the user's password in any system that uses passwords to derive keys in order to ensure a strong key.
Another key source is a Pseudo Random Number Generator (PRNG). Randomness is difficult to achieve using a computer, because the operation of a computer is very predictable and highly non-random. Don't try to roll your own PRNG. Instead, rely on the built in technology provided by the RNGCryptoServiceProvider class to generate random bytes. The underlying implementation of this PRNG is what is used by the GenerateKey method of the FCL ciphers, as we will discuss later.
The first step toward your goal of encrypting data in C# is to choose a cipher. There are 4 ciphers provided by the FCL: DES, RC2, Rijndael and TripleDES. I'll offer a brief explanation of these and offer some advice as to why you would use one over another.
DES – DES stands for Data Encryption Standard. This is an old algorithm that has been shown to be weak by today's standards. The only weakness I'll discuss here is that it uses a short key and block size of 64 bits. We discussed the problem with short keys above, but short blocks are also weak, for reasons that are beyond the scope of this article. If you would like to learn more about why short keys and blocks are bad, search the web for "brute force key attack", "birthday paradox", "birthday attack" and "meet-in-the-middle attack", or read Practical Cryptography, by Schneier & Ferguson. Schneier & Ferguson recommend a key and block length of 256 bits. Another problem with DES is that it was purposely designed to be efficient in hardware and inefficient in software. This makes DES a relatively slow algorithm in any software implementation.
Triple DES – Triple DES is simply a strengthened version of DES. To strengthen the cipher, Triple DES runs the DES algorithm over the plain text three times, hence the "triple" in the name. It also offers stronger key lengths of 128 and 192 bits. However, it still uses a relatively weak block size of 64 bits. Another disadvantage of Triple DES is that, since it must perform the DES algorithm three times, it is a relatively slow cipher.
RC2 – RC2 is generally accepted as a good cipher and it has been around since the mid 1990s. It is also more than twice as fast as DES when implemented in software. However, it still uses a 64 bit block size and a key length (in the FCL implementation) of 40 to 128 bits in 8-bit increments.
Rijndael (a.k.a. AES) – Rijndael is the U.S. government's Federal Information Processing Standard (FIPS) Advanced Encryption Standard (AES) cipher algorithm. The names Rijndael and AES are used interchangeably in cryptography literature. Rijndael offers key lengths and block sizes of 128, 192 and 256 bits. One drawback of Rijndael is it's relative newness. Although it is highly recommended and considered strong by most cryptographers, a new cipher is one that has not withstood the same scrutiny and test of time as an older, more seasoned cipher. Newness not withstanding, the available 256-bit key and block lengths and it's acceptance as a government standard lead me to favor this cipher and I would recommend this cipher to you if you aren't constrained by compatibility or political concerns.
Key management and authentication are not discussed in detail in this article, but our discussion would be incomplete without mentioning them.
Key Management – To borrow a phase from Keith Brown, in essence, cryptography simply compresses large secrets into small secrets. That is, a large, secret message is rendered unreadable and no longer secret without the use of a much smaller secret, the key. Although your message, if properly encrypted, is no longer secret, you must still maintain the secrecy of the key. If an attacker gains access to the key, your messages are obviously no longer secret. Therefore, it is absolutely essential that you implement a good key management strategy to maintain the security of your keys. Another precaution you must take is to change keys frequently. Keys with a long lifetime not only allow an attacker plenty of time to break the key, but they also increase the amount of damage an attacker can do should he succeed in recovering your key.
Authentication – Even if you properly encrypt messages in your system, you are still left with two problems on the receiving end. First, do you really know who sent the data? Is the data really from the party with whom you think you're communicating, or has an attacker sent the message? Secondly, is the data received really the same as the data that was sent by the other party, or has an attacker modified the contents of the message? These problems are addressed by Message Authentication Codes (MACs) and digital signatures, and you absolutely must implement an authentication strategy in your application so that you can detect bogus messages.
Before you read on, I would highly recommend further study of cryptographic principles if you are new to the subject. An excellent place to start would be with the book I mentioned above, Practical Cryptography, by Schneier and Ferguson.
For the sample code and instructions in the remainder of this article, we will be using the Rijndael cipher as implemented in the RijndaelManaged FCL class. OK, let's get started with the code. Our first steps will be to create an instance of the cipher, set the key and block sizes and generate a random key. The following code snippet demonstrates this procedure:
RijndaelManaged InitCipher() { // Create an instance of the cipher RijndaelManaged cipher = new RijndaelManaged(); // Set the key and block size. // Although the key size defaults to 256, it's better to be explicit. cipher.KeySize = 256; // BlockSize defaults to 128 bits, so let's set this // to 256 for better security cipher.BlockSize = 256; // GenerateKey method utilizes the RNGCryptoServiceProvider // class to generate random bytes of necessary length. cipher.GenerateKey(); return cipher; }
The code above initializes a new instance of RijndaelManaged, sets the appropriate key and block sizes and creates a new random key. The value of the key can be retrieved by accessing the Key property of the cipher.
Assuming that the messages encrypted by our cipher are to be transmitted across the network, the generated key must somehow be securely transmitted to the other party. Just how to accomplish this safely is a problem beyond the scope of this article, but this is generally accomplished by using a longer term key, which is used to encrypt "session" keys for transmission between parties. This session key is then used for a relatively short duration communications session. If you are simply encrypting data to be stored on a medium such as a hard drive, you simply need to devise a means of securely storing the key so it can later be used to decrypt the data. I say simply, but this is yet another tricky problem beyond the scope of this article. In fact there is really no way to completely securely store a key online. There are ways to make it difficult for an attacker to access the key, but the fact is that the key is still present on a computer somewhere. If an attacker is able to gain privileged access to the computer, he can read the key. A more secure way to store keys is offline, such as on a disk locked in a safe.
We have a problem now. What if we are the "other party" described above? That is, what if the key has already been decided on by another who is initiating communication with us and we need to make use of the same key? In this case, you can initialize the key directly by assigning the key byte array to the Key property of the cipher. Let's refactor our initialization so that we can add an overload that allows us to play the role of either initiator or responsor. Here's the new code:
RijndaelManaged CreateCipher() { RijndaelManaged cipher = new RijndaelManaged(); cipher.KeySize = 256; cipher.BlockSize = 256; return cipher; } RijndaelManaged InitCipher() { RijndaelManaged cipher = CreateCipher(); cipher.GenerateKey(); return cipher; } RijndaelManaged InitCipher(byte[] key) { RijndaelManaged cipher = CreateCipher(); cipher.Key = key; return cipher; }
This modified code will now allow us either to initiate a session or participate in an already established session. However, we obviously can't encrypt or decrypt messages yet. Let's create a class, CipherWrapper, to which we will later add encryption and decryption methods.
class CipherWrapper { RijndaelManaged _cipher = null; public CipherWrapper() { _cipher = InitCipher(); } public CipherWrapper(byte[] key) { _cipher = InitCipher(key); } public byte[] Key { get { return _cipher.Key; } set { _cipher.Key = value; } } }
Now we can simply copy/paste our initialization functions into the CipherWrapper class. Note that the key initialization is taken care of by the constructor and a property has been added that allows us to change the key, if need be.
One peculiarity of block ciphers that you should be aware of is that they must operate on entire blocks of data. Data less than one block in length can't be encrypted. So what happens if (as is generally the case) the length of our plain text is not an even multiple of the block size? Enter padding. Padding is extra data added to the end of the message to force the length to be an even multiple of the block size. There are several methods for accomplishing this. These methods are called padding modes, of which there are three supported by version 1.1 of the FCL (the current version as of this writing): None, PKCS7 and Zeros. The padding mode is set by assigning a value from the PaddingMode enumeration to the Padding property of an instance of the cipher.
The PaddingMode.None mode is really not a padding mode at all. If you specify PaddingMode.None, you're telling the cipher that you don't want any padding performed at all. In this case, you must ensure that you only attempt to encrypt data whose length is an even multiple of the block size. If the data doesn't meet this requirement, you will get an exception stating that this is the case. The PaddingMode.Zeros mode simply pads the data with enough zeros to cause the length of the message to be an even multiple of the block size. The problem with PaddingMode.Zeros is that it is not reversible on decryption. Suppose the cipher encounters data that decrypts with five zero bytes at the end of the message. Are there five bytes of padding or are there 4 bytes of padding and an actual zero byte in the original plain text? How would the cipher be able to know? It really can't tell. If you use the PaddingMode.Zeros mode, you must transmit the actual length of the data as part of the message so that when the data is decrypted, the decrypting party will know how many bytes of plain text there are so they can strip the padding from the decrypted message.
The PaddingMode.PKCS7 mode is more interesting. This padding mode fills out the last block of the message with a sequence of bytes, the value of each of which is equal to the total number of padding bytes. For instance, given a block size of 128 bits and a final block plain text value of [AA BB CC DD EE FF], the padding string would be "0A 0A 0A 0A 0A 0A 0A 0A 0A 0A". The 0A value indicates that there are 10 total padding bytes. The padded block would then look like so: [AA BB CC DD EE FF 0A 0A 0A 0A 0A 0A 0A 0A 0A 0A]. In order to reverse this, the cipher simply examines the value of the last byte in the block, verifies that the appropriate number of bytes containing this value exist at the end of the message, and then removes the padding from the decrypted data. You may be wondering what happens if our data length is a perfect multiple of the block size. In this scenario, PaddingMode.None and PaddingMode.Zeros add no padding. However, in the case of PaddingMode.PKCS7, padding must be added because the cipher must be able to reverse even a no-padding situation. In this case, an additional block must be added to the plain text and the value of each byte set to the block size in bytes. In the case of a 128-bit block, a block containing [10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10] would be added to the end of the message.
A couple of new padding modes are added to the .NET 2.0 FCL, ANSIX923 and ISO10126. These modes operate in a manner very similar to PKCS7. Each of the two new modes sets the value of the final byte equal to the total number of padding bytes. ANSIX923 then sets the remaining bytes to zero, while ISO10126 places random data in the remaining padding bytes. If you are using the 1.1 version of the FCL, I would recommend using the PKCS7 mode because it requires the least amount of effort and care on your part. Although, fortunately, this is the default, I would still recommend explicitly setting the Padding property of your cipher to PaddingMode.PKCS7 in your code. Let's modify our CreateCipher method to do this:
RijndaelManaged CreateCipher() { RijndaelManaged cipher = new RijndaelManaged(); cipher.KeySize = 256; cipher.BlockSize = 256; // Here's the new line: cipher.Padding = PaddingMode.PKCS7; return cipher; }
If you happen to be using the 2.0 version of the FCL, I would recommend using the ISO10126 mode as it adds random padding bytes, which reduces the predictability of the plain text. In 2.0, PKCS7 is still the default, so in this case, explicitly setting the Padding property to PaddingMode.ISO10126 will be mandatory. In recommending these modes, I realize that you won't always have a choice. You're not operating in a vacuum and you need to play nicely with the other parties with whom you're communicating. However, given the choice, go with PKCS7 in 1.1 and ISO10126 in 2.0.
You'll get to be good friends with this interface as you put encryption to work in your applications. ICryptoTransform is the standard interface through which you ask the cipher to encrypt and decrypt data. Note that this interface is also used by other cryptographic classes, such as hashes. Let's have a quick look at the members of this interface.
TransformBlock – Call this method to transform (encrypt or decrypt) 1 or more blocks before the end of the message.
TransformFinalBlock – Call this method to transform 1 or more blocks at the end of the message. Given our discussion of padding, you should understand why it is necessary to transform blocks at the end of the message differently than those before the end.
CanReuseTransform – Returns a boolean indicating whether the current transform can be reused. In the case of the FCL ciphers, this will always return true.
CanTransformMultipleBlocks – Returns a boolean indicating whether multiple blocks can be transformed in a single call to either TransformBlock or TransformFinalBlock. In the case of the FCL ciphers, this property also always returns true, indicating that you need not add logic to your code to process individual blocks of data. The one caveat, of course, is that you must call the appropriate transform depending on whether the data is at the end of the message (TransformFinalBlock) or not (TransformBlock).
InputBlockSize/OutputBlockSize – These properties return an integer indicating the input and output block sizes, respectively. In the case of ciphers, these will always be the same, and their return value will be dependent upon the value you specified in the BlockSize property of the cipher.
We now have enough background to begin encrypting and decrypting data. To perform the actual transformation of Encryption or Decryption, you need to create an instance of what is called a Transform class. You do this in .NET by calling the cipher's CreateEncryptor or CreateDecryptor methods, depending, of course, on whether you're encrypting data or decrypting data. The CreateEncryptor/Decryptor methods return an ICryptoTransform interface to the newly created transform class. Once you have the ICryptoTransform interface, you simply need to call the TransformBlock and TransformFinalBlock methods as appropriate. Let's add two new methods to our CipherWrapper class: EncryptMessage and DecryptMessage.
public byte[] EncryptMessage(byte[] plainText) { ICryptoTransform transform = _cipher.CreateEncryptor(); byte[] cipherText = transform.TransformFinalBlock(plainText, 0, plainText.Length); return cipherText; } public byte[] DecryptMessage(byte[] cipherText) { ICryptoTransform transform = _cipher.CreateDecryptor(); byte[] plainText = transform.TransformFinalBlock(cipherText, 0, cipherText.Length); return plainText; }
Note that the only difference between the two functions is the call to either CreateEncryptor or CreateDecryptor. These two methods return an ICryptoTransform interface to a new transform class which actually performs the correct cryptographic transformations depending on the encryption mode, Encrypt or Decrypt. It is necessary to return a new class instance, because the transformation must maintain state in the case of transformation performed in multiple steps.
The example above assumes that we're using a message-based protocol where we know the entire contents of either the plain or cipher text at encryption/decryption time. Since we also know that the FCL ciphers always return true from CanTransformMultipleBlocks, we need not call TransformBlock method at all. We can simply transform the entire message in one go by calling TransformFinalBlock. If you are implementing a system where you may not know the entire message, you will have to modify this simple example to call TransformBlock/TransformFinalBlock as appropriate. It's not terribly difficult to use the TransformBlock method. Basically, the only difference from a coding standpoint is that it does not return any data. Instead, you must pass in a buffer, into which the cipher will place the transformed bytes, as well as an offset into the buffer where you would like the data to be stored.
For stream-based protocols, it would be nice to have built-in functionality to allow us to read and write individual bytes at a time or at least to write chunks not broken into perfect blocks. Fortunately, the FCL provides this functionality in the CryptoStream class. I don't want to clutter the article with code that is specific to streaming rather than cryptography, so I'll just provide a couple of examples that demonstrate the basics of using the CryptoStream class.
byte[] EncryptMessageUsingStream(byte[] plainText) { ICryptoTransform transform = _cipher.CreateEncryptor(); MemoryStream ms = new MemoryStream(); CryptoStream cs = new CryptoStream(ms, transform, CryptoStreamMode.Write); cs.Write(plainText, 0, plainText.Length); // Remember padding! This instructs the transform to pad and finish. cs.FlushFinalBlock(); byte[] cipherText = ms.ToArray(); return cipherText; }
Notes: In order to implement this in a stream-based protocol, you would break this method up into possibly several methods that initialize the stream once, then call CryptoStream.Write in iterations, then call FlushFinalBlock and read the data.
byte[] DecryptMessageUsingStream(byte[] cipherText) { ICryptoTransform transform = _cipher.CreateDecryptor(); MemoryStream ms = new MemoryStream(cipherText); CryptoStream cs = new CryptoStream(ms, transform, CryptoStreamMode.Read); byte[] plainTextBuffer = new byte[cipherText.Length]; // Remember padding! The length read may be different than the // length of the plain text. int plainTextLength = cs.Read(plainTextBuffer, 0, cipherText.Length); byte[] plainText = new byte[plainTextLength]; Array.Copy(plainTextBuffer, 0, plainText, 0, plainTextLength); return plainText; }
Notes: The notes on EncryptMessageUsingStream also apply here. The reason that the length of the cipher text and the actual number of bytes that are read from the stream are different is, of course, padding. You don't necessarily have to allocate a temporary buffer, as I have done. You could simply return the entire plainTextBuffer with an integer indicating the length of the actual plain text, which is returned from the CryptoStream.Read call. Alternatively, if you know the length of the plain text, you could pass this as a parameter and only allocate and read the appropriate length.
The process by which individual blocks of a message are put together to form the complete encrypted message is called a cipher mode. You can specify a cipher mode by assigning a value from the CipherMode enumeration to the Mode property of an instance of the cipher. Choosing a secure cipher mode is one of the most important things to get right when you implement encryption in your applications.
Usually, a message is more than one block in length. So how are the blocks of the message then put together to form the complete encrypted message? The obvious answer to this question would be simply to encrypt each block individually and then place the encrypted blocks end-to-end to form the encrypted message. In fact, there is a cipher mode available in the FCL that does exactly this, Electronic Code Book (ECB). Sounds simple, doesn't it? It turns out that this mode is very insecure for most applications and should never be used unless you really know what you're doing and have a good reason. Let's look at an example that uses ECB so that we can see it's weakness. We'll use the following code snippet to demonstrate:
string testMessage = "This is a block.This is a block.This is a block."; byte[] plainText = Encoding.ASCII.GetBytes(testMessage); RijndaelManaged cipher = new RijndaelManaged(); cipher.Mode = CipherMode.ECB; ICryptoTransform transform = cipher.CreateEncryptor(); byte[] cipherText = transform.TransformFinalBlock(plainText, 0, plainText.Length);
Here's the hex dump for the data contained in cipherText with the padding block removed from the end:
C6 D7 EA 66 D0 A9 D5 6B 42 5F 45 95 BF 9B 5E FA C6 D7 EA 66 D0 A9 D5 6B 42 5F 45 95 BF 9B 5E FA C6 D7 EA 66 D0 A9 D5 6B 42 5F 45 95 BF 9B 5E FA
(if you try this, you'll end up with different data because your key will be different)
Do you see the problem? That's right; the problem is that each block contains precisely the same cipher text. Actually, we should expect this, given that we stated that ECB mode simply encrypts each block individually and places them end-to-end. I've just tried to highlight the problem by laying the data out in blocks like this. The cipher text in the hex dump reveals a great deal to a potential attacker. The structure of our message is obvious to anyone who wishes to look. Remember from our discussion of key strength that our data is not as random as we might think. There is probably a high recurrence of certain headers and many documents contain a lot of NULL data. We don't want this information revealed in the cipher text.
In order to correct this problem, we need to use a better mode. Cipher Block Chaining (CBC) mode is a much more secure mode. CBC creates each cipher text block by first XORing the plain text block with the previous cipher text block and then encrypting this combined block. Obviously, we have the problem of what to use as the previous cipher text block when we encrypt the first plain text block. In this case, an Initialization Vector (IV), which we'll discuss shortly, is used as the first cipher text block. This process prevents the structure of the message from being revealed to the degree it was with ECB. In order to decrypt the message, this process is simply unwound from the end of the message forward. Let's make a slight change to the code and then examine the hex output.
The line that reads...
cipher.Mode = CipherMode.ECB;
becomes...
cipher.Mode = CipherMode.CBC;
...and here's the hex dump, also with the padding block removed:
E7 E7 57 00 A3 FB 39 0D 0F 9C FC 2B E5 8F B6 80 8C 41 25 C7 D1 81 F4 9B 4F F6 9F 23 C5 80 F3 65 A9 63 8A 86 24 D0 53 39 BE 64 B0 63 A2 00 1B 21
As you can see, the message structure is no longer revealed. Let's change our CipherWrapper class to reflect this new knowledge. As I said above, CBC mode is the default, but it's better to always be explicit in the code with such important details. Let's modify our CreateCipher method to explicitly state our intention:
RijndaelManaged CreateCipher() { RijndaelManaged cipher = new RijndaelManaged(); cipher.KeySize = 256; cipher.BlockSize = 256; cipher.Padding = PaddingMode.PKCS7; cipher.Mode = CipherMode.CBC; // The new modification return cipher; }
Although there are other modes provided in the CipherMode enumeration, I won't cover them here for two reasons. First, I just wanted to illustrate the importance and principles of cipher modes. Secondly, there isn't consistent support for other modes in the FCL.
I mentioned IVs above, but I wanted to return to it and cover it in some detail, now that you have a better understanding of modes. IVs are very important to getting encryption right. An IV is a block of random data that is used as the initial block for modes such as CBC. The IV can be safely transmitted in the clear with each message. Let's look at an example to illustrate the importance of IVs. For this example, I will use our CipherWrapper to encrypt two messages, as follows:
CipherWrapper cipher = new CipherWrapper(); string testMessage = "This is the test message"; byte[] plainText = Encoding.Unicode.GetBytes(testMessage); byte[] cipherText = cipher.EncryptMessage(plainText); byte[] cipherText2 = cipher.EncryptMessage(plainText);
Here's a hex dump of these two messages:
3B 10 7C D3 B8 AC BE 6D 7C 2F A6 24 8C 2C B3 05 08 6F 46 06 98 B7 12 E7 64 D0 44 3A CA 55 2D 27 A1 1E 45 1B 4B A3 7B AF 5E 23 44 07 9B BC AF B2 EE CE 3C 38 58 32 90 8D 6E FF FE 09 69 28 A7 56 3B 10 7C D3 B8 AC BE 6D 7C 2F A6 24 8C 2C B3 05 08 6F 46 06 98 B7 12 E7 64 D0 44 3A CA 55 2D 27 A1 1E 45 1B 4B A3 7B AF 5E 23 44 07 9B BC AF B2 EE CE 3C 38 58 32 90 8D 6E FF FE 09 69 28 A7 56
Do you see the problem? The problem is the same as before, only now the duplication is at a message level. This leaks information to an attacker in the same way as the ECB mode problem did, only now at a message rather than a block level. Our implementation of EncryptMessage is broken. Let's fix it by making a simple modification. Here's the corrected code:
public byte[] EncryptMessage(byte[] plaintext, out byte[] iv /*added*/) { _cipher.GenerateIV(); //added iv = _cipher.IV; //added ICryptoTransform transform = _cipher.CreateEncryptor(); byte[] cipherText = transform.TransformFinalBlock(plainText, 0, plainText.Length); return cipherText; }
Here's the hex dump of the data produced by the modified version of EncryptData:
C7 62 8D E5 87 F2 1A 00 6E 1C 09 EE 73 CF E1 A2 61 E7 FF 05 C2 FF 2E 9C 71 EC FF D5 07 91 47 41 39 D3 30 29 49 05 DA 56 8E 87 6A AB AB 01 7C DC 36 B0 0F 3D EC 01 36 AF 34 8F C0 78 89 AC E6 F3 C6 8F 9F 83 43 A4 4C 16 73 A8 0D 69 4F D4 B2 FB 4F F6 C4 91 CA 8A BA 57 EA 2A 28 D9 88 8A 24 E2 D0 00 CD E2 35 9D 7D 29 38 E3 84 71 79 A5 8B 5D C2 73 09 4B 50 4B 18 B9 53 37 66 B9 68 4E B4 F5
As you can see, the data is no longer the same in the two messages.
The corrected code now calls the Generate IV method of the cipher each time a message is encrypted and returns this value in a new out parameter. The GenerateIV method makes use of the RNGCryptoServiceProvider class to generate random bytes 1 block in length and assigns the value to the IV field of the cipher. You could do this manually by using the RNGCryptoServiceProvider class to generate random data and then assign the value to the IV property of the cipher yourself. It is very important that you use a unique IV for each message and that you never reuse it. Reusing an IV will cause your cipher text to leak information. Now we need to modify the DecryptMessage method to allow the IV to be passed in and used.
public byte[] DecryptMessage(byte[] cipherText, byte[] iv) { ICryptoTransform transform = _cipher.CreateDecryptor(); byte[] plainText = transform.TransformFinalBlock(cipherText, 0, cipherText.Length); return plainText; }
While we have left much about the field of cryptography unsaid, I hope that you've gained a good understanding of the basics of using the FCL classes for encryption. In summary, remember the following points:
Finally, here is the complete text of the CipherWrapper class we developed in this article. Keep in mind that this code is provided for demonstration purposes only. I designed the code for simplicity and illustration foremost and I typically write more complex crypto code to accomplish such goals as to make the classes thread safe and useable from multiple sessions. These details, in my view, do not contribute to the subject at hand, so they were omitted. That stated, this class puts to work all the principles covered by this article. Take some time to study the documentation for the SymmetricAlgorithm class, from which all the FCL ciphers derive, and the ICryptoTransform interface, particularly the overloads for the CreateEncryptor/Decryptor methods.
class CipherWrapper { RijndaelManaged _cipher = null; public CipherWrapper() { _cipher = InitCipher(); } public CipherWrapper(byte[] key) { _cipher = InitCipher(key); } public byte[] Key { get { return _cipher.Key; } set { _cipher.Key = value; } } public byte[] EncryptMessage(byte[] plainText, out byte[] iv) { _cipher.GenerateIV(); iv = _cipher.IV; ICryptoTransform transform = _cipher.CreateEncryptor(); byte[] cipherText = transform.TransformFinalBlock(plainText, 0, plainText.Length); return cipherText; } public byte[] DecryptMessage(byte[] cipherText, byte[] iv) { _cipher.IV = iv; ICryptoTransform transform = _cipher.CreateDecryptor(); byte[] plainText = transform.TransformFinalBlock(cipherText, 0, cipherText.Length); return plainText; } private RijndaelManaged InitCipher() { RijndaelManaged cipher = CreateCipher(); cipher.GenerateKey(); return cipher; } private RijndaelManaged InitCipher(byte[] key) { RijndaelManaged cipher = CreateCipher(); cipher.Key = key; return cipher; } private RijndaelManaged CreateCipher() { RijndaelManaged cipher = new RijndaelManaged(); cipher.KeySize = 256; cipher.BlockSize = 256; cipher.Mode = CipherMode.CBC; cipher.Padding = PaddingMode.PKCS7; return cipher; } }
Many thanks to Keith Brown for his review of this article. Keith provided many helpful suggestions and corrections.
Steve is a senior developer and consultant for 3t Systems, Inc. in Denver, CO, where he spends his days doing architecture, training, and "in-the-trenches" development. Steve began programming Windows on version 3.1 in 1995 using only C, the SDK, and Petzold's classic "Programming Windows 3.1". He was a hobbyist programmer until February 1999, when he turned professional. Steve particularly enjoys learning internals and behind-the-scenes details and developing low-level infrastructure code.
Steve currently resides in Highlands Ranch, CO with his wife Kathleen and two cats.