Using Symmetric Algorithms

for RuBoard

Having introduced the cryptographic object model and the operation of CryptoStreams and ICryptoTransforms , we now move on to detailed discussions and examples of using each of the base types defined within the model: symmetric algorithm, cryptographic hash functions, keyed hash functions, random number generators, and asymmetric algorithms. We begin with the secret key ciphers, represented by the SymmetricAlgorithm class and its descendants. Secret key ciphers are most commonly used for bulk encryption ”scenarios that require encrypting or decrypting large amounts of data. We start with a discussion of the structure of an object that derives from SymmetricAlgorithm , describe how to create individual instances of SymmetricAlgorithm , and then present examples showing how to use a SymmetricAlgorithm for encryption and decryption.

The SymmetricAlgorithm Base Class

An instance object of a SymmetricAlgorithm descendant class represents a particular algorithm and choice of operating parameters. There are four major parameters defined for every SymemtricAlgorithm , denoted by the instance properties Key , Mode , IV , and Padding :

Key ” This property holds the secret key value that will be used to encrypt or decrypt data. The Key property is initialized to a random value when the object is created.

Mode ” This enumeration, of type CipherMode , defines the chaining mode of operation of the cipher. The chaining mode of an algorithm determines whether and how the encryption of one block of data influences the encryption of the next block of data. The default value for the Mode property is "cipher block chaining," CipherMode.CBC , which is the strongest generally available mode of operation. Unless you explicitly have a reason to choose another chaining mode, you probably want to use cipher block chaining.

NOTE

There are five defined values for CipherMode : electronic codebook (ECB, no chaining between blocks), cipher block chaining (CBC), cipher feedback mode (CFB), output feedback mode (OFB), and cipher text stealing (CTS). Every algorithm implementation that ships as part of the .NET Framework supports both ECB and CBC, and the *CryptoServiceProvider implementations additionally support CFB with a feedback size of 8 bits. OFB and CTS are not supported by any of the implementations included in the .NET Framework but can be supported by third-party implementations.


IV ” This property holds the initialization vector (IV), which is used when the algorithm is operated in a chaining mode, such as CBC. When an algorithm is operating in a chaining mode and encrypts the first block of data, the feedback values of the "previous block" are provided by the initialization vector. The IV is the same size as a single block of data and is initialized to a random value when the object is created. Initialization vectors need not be kept secret but should always be randomly chosen before encrypting a stream of data.

CAUTION

In particular, do not set your initialization vectors to a known value, such as all zeros, before use. This weakens the security of your application.


Padding ” This enumeration, of type PaddingMode , describes how the cipher should pad a short final block (that is, what happens if the last block of data to be encrypted is less that BlockSize bytes). Unless you have a specific reason for requiring a particular padding mode, stick with the default value, PaddingMode.PKCS7 .

NOTE

PKCS7 padding mode fills the remainder of the block with bytes whose value is equal to the number of bytes needed to pad; that is, if three bytes of pad are needed, the pad string is 03 03 03 . This has the advantage that when decrypting the last block it is immediately evident how many bytes of padding need to be removed. With zero padding (the other padding option) there is a possibility of error on decrypt if the last plain text bytes to be encrypted were zeros.


Specific algorithms can, of course, have additional operating parameters, but in general, these four are the only ones you will need to access.

Creating Instances of SymmetricAlgorithm Classes

To create an instance of a class that implements a particular symmetric algorithm, simply call the static Create() method on the abstract class representing the algorithm. This statement creates a new object that implements the Rijndael algorithm and assigns it to the variable rijndael :

 Rijndael rijndael = Rijndael.Create(); 

The object will be a member of the default implementation class for the Rijndael algorithm, which is the RijndaelManaged class by default. Recall that by convention, cryptographic objects are created with default parameter values set to the strongest available settings. For SymmetricAlgorithm descendant classes, instance objects will be initialized with a random secret key and random initialization vector, and will be configured to operate in cipher block chaining (CBC) mode with PKCS7 padding for short final blocks. For ciphers that support multiple key lengths, the largest legal key size will be used by default. Table 30.2 lists the default and legal key sizes for the symmetric algorithm implementations that ship as part of the .NET Framework.

Table 30.2. Symmetric Algorithm Key Sizes
Cipher Default Implementation Class Legal Key Sizes Default Key Size
DES DESCryptoServiceProvider 64 bits 64 bits
TripleDES TripleDESCryptoServiceProvider 128, 192 bits 192 bits
RC2 RC2CryptoServiceProvider 40 “128 bits 128 bits
Rijndael RijndaelManaged 128, 192, 256 bits 256 bits

NOTE

The supported key lengths for DESCryptoServiceProvider , TripleDESCryptoServiceProvider , and RC2CryptoServiceProvider depend on the "high encryption" version of CryptoAPI being installed on the platform. If you are running Windows XP, Windows 2000 with Service Pack 2 or later, or Internet Explorer 5.5 or later, you already have the high encryption version of CryptoAPI installed on your machine. For other versions of Windows, you will need to install a "high encryption" update (a "high encryption" flavor service pack for Windows NT 4, and a "high encryption" update for older versions of Internet Explorer). See http://www.gotdotnet.com/team/clr/cryptofaq.htm for details on where to download the high encryption update appropriate for your version of Windows.

The key length for DES and TripleDES includes the parity bits that are defined as part of the key but do not contribute to the overall strength of the algorithm. DES, for example, is defined to use 56-bit keys, but 56-bit keys are expanded to 64 bits by adding a parity bit per byte. CryptoAPI treats DES as a 64-bit algorithm and ignores every eighth bit of the input (recalculating the parity bits as necessary), and the DESCryptoServiceProvider class acts similarly. TripleDES has "two-key" and "three-key" flavors, which thus use 112 and 168 bits of key material. As with DES, CryptoAPI assumes TripleDES keys as though they include the parity bits, so it accepts 128- and 192-bit keys for use with TripleDES. The TripleDESCryptoServiceProvider follows this convention as well.

The RijndaelManaged class is implemented entirely in managed code (C#) and is available on every platform on which the .NET Framework has been installed. RijndaelManaged is also the default implementation class for SymmetricAlgorithm . Thus, in the following statement

 SymmetricAlgorithm symAlg = SymmetricAlgorihtm.Create(); 

the symAlg variable will be assigned to an instance of RijndaelManaged . (This mapping can be overridden by modifying the crypto configuration system.


After you have created a SymemtricAlgorithm object for the cipher, key, and IV you want to use, the next step is to create ICryptoTransform objects that represent encryption and decryption operations using that cipher/Key/IV combination. ICryptoTransforms are created using the CreateEncryptor() and CreateDecryptor() methods :

 ICryptoTransform rijndaelEncryptor = rijndael.CreateEncryptor(); ICryptoTransform rijndaelDecryptor = rijndael.CreateDecryptor(); 

The resulting ICryptoTransforms implement encryption and decryption using the current values of the Key , IV , Mode , and Padding properties of the rijndael object. The ICryptoTransforms are self-contained and once created are not affected by changes in these property values on the SymmetricAlgorithm object that was used to create them.

Encrypting and Decrypting with ICryptoTransforms Created from a SymmetricAlgorithm

After you have an ICryptoTransform , it can be combined with any stream using the CryptoStream class to transform data read from or written to the underlying stream. We now present some examples showing how to use the rijndaelEncryptor and rijndaelDecryptor ICryptoTransforms we just created with CryptoStream to perform encryption and decryption of memory buffers and on-disk files.

Encrypting an In-Memory Buffer

Our first code sample, shown in Listing 30.1, demonstrates simple encryption of an in-memory plain text buffer. Suppose you have an array of plain text bytes that you want to encrypt in memory using rijndaelEncryptor . An easy way to do this is to create a MemoryStream to hold the output ciphertext bytes, "wrap" the rijndaelEncryptor around the MemoryStream using CryptoStream , and then write the plain text bytes into the CryptoStream . The code in this listing performs the encryption; we'll assume that the plain text already exists in a byte array called plain textArray and that we ultimately want the encrypted ciphertext in a byte array called ciphertextArray .

NOTE

Source code in electronic form for all code listings in this chapter may be downloaded from the publisher's Web site for this book.


Listing 30.1 Encrypting In-Memory Plain Text Creating In-Memory Ciphertext
 // create the MemoryStream that will hold our output: MemoryStream ciphertextStream = new MemoryStream(); // create the CryptoStream. Bytes written to this stream will be encrypted // by the rijndaelEncryptor and then written to the ciphertextStream CryptoStream cryptoStream = new CryptoStream(ciphertextStream, rijndaelEncryptor, graphics/ccc.gif CryptoStreamMode.Write); // Now, write the all the plain text bytes to the cryptoStream to encrypt them cryptoStream.Write(plain textArray, 0, plain textArray.Length); // We have no more bytes to encrypt, so tell the cryptoStream that it // has seen the last block of input cryptoStream.FlushFinalBlock(); // read the output ciphertext from the ciphertextStream byte[] ciphertextArray = ciphertextStream.ToArray(); // close the CryptoStream cryptoStream.Close(); 

That's all there is to it! When the bytes in plain textArray are written to the cryptoStream , they are automatically encrypted by the rijndaelEncryptor transform and written out to the ciphertextStream . Note that the call to cryptoStream.FlushFinalBlock() is important; it tells the cryptoStream that we have finished using the stream and any remaining bytes buffered inside the CryptoStream need to be processed . In this case, that final processing will be to pad out the last block of input data to a multiple of the block size of Rijndael (128 bits or 16 bytes), encrypt it, and write it out to the ciphertextStream .

Encrypting an In-Memory Buffer to an On-Disk File

Let us now change the scenario slightly and show how to modify the code in Listing 30.1 to persist the ciphertext to an on-disk file. All we need to do is change the destination stream, ciphertextStream , from a MemoryStream to a FileStream associated with the desired on-disk file. CryptoStream does not care about the type of stream to which it is writing; so long as it is a descendant of the System.IO.Stream class and supports writing operations, CryptoStream will work with it. Listing 30.2 contains a modified version of the previous program that persists the ciphertext in the file C:\ciphertext.bin (root directory of the C: drive, which is usually the primary partition of your computer's hard disk drive).

Listing 30.2 Encrypting In-Memory Plain Text and Writing the Ciphertext to a File
 // we have to escape backslashes in C# strings String filename = "C:\ \ ciphertext.bin"; // create the FileStream that will hold our output: FileStream ciphertextStream = new FileStream(filename, FileMode.Create); // create the CryptoStream wrapping this FileStream // with the rijndaelEncryptor CryptoStream CryptoStream = new CryptoStream(ciphertextStream, rijndaelEncryptor, CryptoStreamMode. graphics/ccc.gif Write); // Write the plain text bytes to the cryptoStream cryptoStream.Write(plain textArray, 0, plain textArray.Length); // We are now done writing, so flush the last block out cryptoStream.FlushFinalBlock(); // close the CryptoStream cryptoStream.Close(); 

Notice that we do not need to explicitly close the underlying FileStream ciphertextStream ; when the Close() method is called on a CryptoStream instance, the underlying stream is automatically closed too. If you want to write some bytes out to the underlying stream after the CryptoStream is finished, you can do so after calling the CryptoStream's FlushFinalBlock() method.

Encrypting and Decrypting an On-Disk File to a Second File

The examples in Listings 30.1 and 30.2 demonstrate simple uses of CryptoStream to encrypt files, but they simplify the key management by assuming that you have an instance of SymmetricAlgorithm with the proper key and IV already set. In the next example, we create two methods, EncryptFile and DecryptFile , that convert between unencrypted and encrypted files. Both methods will always use Rijndael and take as input an encryption/decryption key suitable for use with Rijndael (that is, 16, 24, or 32 bytes in length). Initialization vectors will be randomly generated on encryption and persisted with the encrypted file as the first 16 bytes of the file; on decryption, we will read the IV from the ciphertext before decrypting the rest of the file. Both methods take two additional inputs ”the names of the source and destination files. For EncryptFile , the source is plain text and the destination is ciphertext; the reverse is true for DecryptFile . Listing 30.3 contains the source code for the EncryptFile method; the source for DecryptFile is shown in Listing 30.4.

Listing 30.3 Encrypting an On-Disk File to a Second File with Random IV Generation
 public static void EncryptFile(byte[] key, String sourceFile, String destFile) {   // initialize with random key and IV   Rijndael aes = Rijndael.Create();   // set the key to the passed-in arg   aes.Key = key;   // create FileStreams for source and dest   FileStream inStream = new FileStream(sourceFile, FileMode.Open);   FileStream outStream = new FileStream(destFile, FileMode.Create);   // We want to write the IV out to the destFile unencrypted,   // so in this case we can wrap a CryptoStream around the *sourceFile*   // and read encrypted bytes from it   CryptoStream encryptedInStream = new CryptoStream(inStream, aes.CreateEncryptor(), graphics/ccc.gif CryptoStreamMode.Read);   // Write the IV out to the ciphertext file as the first bytes in the file.   outStream.Write(aes.IV, 0, aes.IV.Length);   // Now we're ready to encrypt. Read the bytes from the CryptoStream   // in a loop until there aren't any more (end-of-file), writing to   // the output file as we go. We need a buffer to hold what we're going to   // read, and bytesRead tells us how many bytes in buffer are   //valid ciphertext.   int bytesRead;   byte[] buffer = new byte[1024]; // read 1K at a time   do {       bytesRead = encryptedInStream.Read(buffer,0,1024);       outStream.Write(buffer, 0, bytesRead);   }  while (bytesRead > 0);   // Done!  Close() the streams we used.   inStream.Close();   outStream.Close();   return; } 

Notice that when we create the aes object using Rijndael.Create() , the object is generated with a random initialization vector; each file we encrypt with this method will use a different random IV. (In fact, encrypting the same file twice will yield two different ciphertext files because the IVs used for the two files will be different.) The method uses a CryptoStream in "read" mode because it is slightly more convenient in this case where we are writing both the unencrypted IV and ciphertext to the same output file.

Decrypting a file is also straightforward; the only tricky part is that we have to read the IV from the file before creating the CryptoStream and ICryptoTransform .

Listing 30.4 Decrypting an On-Disk File to a Second File
 public static void DecryptFile(byte[] key, String sourceFile, String destFile) {   // initialize with random key and IV   Rijndael aes = Rijndael.Create();   // Create an array to hold the IV read from the file:   byte[] realIV = new byte[aes.IV.Length];   // create FileStreams for source and dest   FileStream inStream = new FileStream(sourceFile, FileMode.Open);   FileStream outStream = new FileStream(destFile, FileMode.Create);   // We want to read the IV out to the sourceFile unencrypted,   // so in this case we can wrap a CryptoStream around the *destFile*   // and write encrypted bytes into it. The output bytes will be decrypted   // First, read the IV in   inStream.Read(realIV, 0, realIV.Length);   // set the key and IV of the aes object   aes.Key = key;   aes.IV = realIV;   // Create the CryptoStream   CryptoStream decryptedOutStream = new CryptoStream(outStream, aes.CreateDecryptor(), graphics/ccc.gif CryptoStreamMode.Write);   // Now we're ready to decrypt.   int bytesRead;   byte[] buffer = new byte[1024]; // read 1K at a time   do {     bytesRead = inStream.Read(buffer,0,1024);     decryptedOutStream.Write(buffer, 0, bytesRead);   }  while (bytesRead > 0);   // We've read everything, so now call FlushFinalBlock() to write out any   // remaining bytes   decryptedOutStream.FlushFinalBlock();   inStream.Close();   decryptedOutStream.Close();   return; } 
Performing Multiple Cryptographic Transforms Using Cascaded CryptoStreams

For our final example in this section, we demonstrate how CryptoStreams themselves can be cascaded to perform multiple ICryptoTransforms in sequence. We modify the task presented in Listings 30.3 and 30.4 slightly. We want to encrypt and decrypt files on-disk, using random initialization vectors, but we additionally require that the encrypted ciphertext be stored Base64-encoded. Base64 is an encoding algorithm that represents binary data in a subset of the ASCII character set that is suitable for printing or sending through text email systems. It is commonly used today when sending attachments in MIME email messages and is also used in XML Digital Signatures (see Chapter 32, "Using Cryptography with the .NET Framework: Creating and Verifying XML Digital Signatures"). Base64 encoding works by converting three bytes of data into four encoded bytes, where each of the encoded bytes is one of the following 64 characters : a “z, A “Z, 0 “9, /, or +. (Additionally, the equal sign character, = , is used for padding any short blocks.)

The ToBase64Transform and FromBase64Transform classes in System.Security.Cryptography are ICryptoTransforms that implement Base64 encoding and decoding. To Base64 encode our ciphertext, all we need to do is pass the ciphertext through a second CryptoStream that uses a ToBase64Transform . Listing 30.5 shows the modified code for EncryptFile using a Base64 encoding. (Note that we do not Base64 encode the initialization vector in this example.)

Listing 30.5 Encrypting and Base64-Encoding an On-Disk File with Random IV Generation
 public static void EncryptFile(byte[] key, String sourceFile, String destFile) {   // initialize with random key and IV   Rijndael aes = Rijndael.Create();   aes.Key = key;   FileStream inStream = new FileStream(sourceFile, FileMode.Open);   FileStream outStream = new FileStream(destFile, FileMode.Create);   CryptoStream encryptedInStream = new CryptoStream(inStream, aes.CreateEncryptor(), graphics/ccc.gif CryptoStreamMode.Read);   // We want to Base64-encode the ciphertext that comes out   // of encryptedInStream, so we simply wrap a second CryptoStream   // around it that uses a ToBase64Transform. When we read from   // b64EncodedInStream, we will read transformed bytes that came   // from encryptedInStream, which in turn are encrypted bytes that   // came from inStream (the sourceFile)   CryptoStream b64EncodedInStream = new CryptoStream(encryptedInStream, new graphics/ccc.gif ToBase64Transform(), CryptoStreamMode.Read);   // Write the IV out   outStream.Write(aes.IV, 0, aes.IV.Length);   // Now we're ready to encrypt.   int bytesRead;   byte[] buffer = new byte[1024]; // read 1K at a time   do {     // read from the wrapping CryptoStream!     bytesRead = b64EncodedInStream.Read(buffer,0,1024);     outStream.Write(buffer, 0, bytesRead);   }  while (bytesRead > 0);   // Done! Close all streams   inStream.Close();   outStream.Close();   return; } 

Bytes read from the b64EncodedInStream are produced by Base64-encoding bytes read from the underlying stream encrytpedInStream . The bytes produced by encryptedInStream are, in turn, produced by encrypting the bytes read from its underlying stream ( inStream ). Thus, this routine cascades two distinct data transformations, represented by the two ICryptoTransforms .

Decrypting a Base64-encoded ciphertext file produced by EncryptFile is similarly straightforward; a version of DecryptFile that undoes the Base64 encoding before performing the decryption is shown in Listing 30.6.

Listing 30.6 Base64-Decoding and Decrypting an On-Disk File
 public static void DecryptFile(byte[] key, String sourceFile, String destFile) {   // initialize with random key and IV   Rijndael aes = Rijndael.Create();   // Create an array to hold the IV read from the file:   byte[] realIV = new byte[aes.IV.Length];   // create FileStreams for source and dest   FileStream inStream = new FileStream(sourceFile, FileMode.Open);   FileStream outStream = new FileStream(destFile, FileMode.Create);   // First, read the IV in   inStream.Read(realIV, 0, realIV.Length);   // set the key and IV of the aes object   aes.Key = key;   aes.IV = realIV;   // Create the CryptoStreams. We need two: one to undo the Base64 encoding   // and one to decrypt. We wrap the b64DecodedOutStream around the   // decryptedOutStream because we want to do the Base64 decoding   // before decrypting   CryptoStream decryptedOutStream = new CryptoStream(outStream, aes.CreateDecryptor(), graphics/ccc.gif CryptoStreamMode.Write);   CryptoStream b64DecodedOutStream = new CryptoStream(decryptedOutStream, new graphics/ccc.gif FromBase64Transform(), CryptoStreamMode.Write);   // Now we're ready to decrypt.   int bytesRead;   byte[] buffer = new byte[1024]; // read 1K at a time   do {     bytesRead = inStream.Read(buffer,0,1024);     // Write to the Base64 decoder     b64DecodedOutStream.Write(buffer, 0, bytesRead);   }  while (bytesRead > 0);   // We've read everything, so now call FlushFinalBlock() to write out any   // remaining bytes   b64DecodedOutStream.FlushFinalBlock();   inStream.Close();   b64DecodedOutStream.Close();   return; } 

The Base64 encoding is removed by passing the data through a FromBase64Transform object in the b64DecodedOutStream CryptoStream . The unencoded bytes are then automatically passed on to the second (inner) CryptoStream , decryptedOutStream , which then writes the decrypted plain text out to the desired file.

This concludes our introduction to using symmetric ciphers in the .NET Framework. Symmetric ciphers are one of the fundamental building blocks of cryptographic protocols, and we will use them extensively throughout the remainder of this chapter and the next in conjunction with hash functions and asymmetric algorithms.

for RuBoard


. NET Framework Security
.NET Framework Security
ISBN: 067232184X
EAN: 2147483647
Year: 2000
Pages: 235

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net