Using Cryptographic Hash Functions

for RuBoard

We turn our attention now to the portion of the .NET Framework object model that represents cryptographic hash functions. Cryptographic hash functions are represented by the HashAlgorithm class and its descendants. In this section, we describe the hash algorithms that are included in the .NET Framework class library, how to create a HashAlgorithm object corresponding to the algorithm you desire , and how to compute hashes using these objects over static and streaming data.

Creating HashAlgorithm Objects

The .NET Framework includes "in-the-box" support for the hash algorithms commonly used today in cryptographic protocols. Table 30.3 lists the hash algorithms that are supported in the .NET Framework class libraries, along with their implementation classes.

NOTE

In keeping with the naming convention introduced for symmetric ciphers, implementations that are built on top of the Win32 CryptoAPIs are named *CryptoServiceProvider , and implementations that are written in managed code are named *Managed .


Table 30.3. Hash Algorithms Included in the .NET Framework
Hash Algorithm Hash Size in Bits Implementation Class(es)
MD5 128 MD5CryptoServiceProvider
SHA-1 160 SHA1CryptoServiceProvider SHA1Managed
SHA-256 256 SHA256Managed
SHA-384 384 SHA384Managed
SHA-512 512 SHA512Managed

By far, the most common hash function in use today is SHA-1, the Secure Hash Algorithm developed by the U.S. National Security Agency (NSA) and National Institutes of Standards and Technology (NIST) as part of their Digital Signature Standard (DSS). SHA-1 is a 160-bit hash algorithm, meaning that for any size input, it generates a 160-bit output. MD5 (short for "Message Digest Algorithm 5") is an older hash function designed by Ron Rivest that is still widely used today. SHA-256, SHA-384, and SHA-512 are new hash functions designed by NSA and NIST to complement Rijndael, the AES cipher. They are sized so that their output values are twice the length of the allowable key sizes in Rijndael.

NOTE

The reason SHA-256, SHA-384, and SHA-512 are sized as they are is so that a brute force attack against them will be comparable to a brute force attack against Rijndael with a 128-, 192- or 256-bit key. A brute force attack against Rijndael takes exponential time proportional to the key length, for example, O(2 128 ) operations to brute force a 128-bit key. A birthday attack against SHA-256 ”that is, the average time it would take to find a colliding pair of inputs x, y with SHA-256(x) = SHA-256(y) ”is also exponential proportional to half the key length, for example, O(2 256/2 ) = O(2 128 ).


Computing Hash Values Using the ComputeHash() Methods

The .NET Framework makes it very easy to compute hash values of streams and arrays of bytes. If you just want to compute the hash of some binary data, all you need to do is create an appropriate HashAlgorithm object corresponding to the algorithm you desire and call the ComputeHash() method on that object. The following computes the SHA-1 hash of an array of bytes stored in the myMessage variable:

 SHA1 sha1 = SHA1.Create(); // create a new SHA1 hash object byte[] hashValue = sha1.ComputeHash(myMessage); 

NOTE

Note that although hash algorithms are typically defined to work binary data of any length, the implementations provided in the .NET Framework only work for inputs that are a multiple of 8 bits in length (for example, consist of an whole number of bytes).


You can also compute the hash of a portion of an array by using the ComputeHash overload that accepts an array offset and count:

 byte[] hashValue = sha1.ComputeHash(myMessage, offset, count); 

There is also an overloaded ComputeHash method for hashing the contents of a stream of bytes. The stream must support reading, and hashing will begin from the current position of the stream and proceed until no more bytes are available. The following is a code snippet to hash the contents of the file "c:\hashinput.txt" :

 FileStream inputFile = new FileStream("c:\ \ hashinput.txt", FileMode.Open); SHA1 sha1 = SHA1.Create(); byte[] hashValue = sha1.ComputeHash(inputFile); inputFile.Close(); 

You can call ComputeHash() multiple times on the same hash object to compute multiple hashes; HashAlgorithm objects automatically reset their internal state after computing a hash value and are immediately ready to be used again. The HashSize property on a HashAlgorithm returns the size, in bits, of the hashes computed by the object. There is also a Hash property on the object that returns the most recently computed hash value.

Our next example program shows how a single HashAlgorithm object can be reused to compute multiple hash values. The program shown in Listing 30.7 takes as input a single command-line argument, a directory path , and prints out the SHA-256 hash value for every file within the directory. (This program ignores subdirectories, although it would be easy to modify it to recursively compute the hash of every file in the input directory or one of its subdirectories.) The PrintByteArray method is a subroutine that prints byte arrays as a hexadecimal string with spaces inserted every eight characters for readability.

Listing 30.7 Compute and Print the SHA-256 Hash of Every File in a Directory
 using System; using System.IO; using System.Security.Cryptography; public class HashDir {   // This routine pretty-prints a byte array   public static void PrintByteArray(byte[] array)  {     int i;     for (i = 0; i < array.Length; i++) {       Console.Write(String.Format("{ 0:X2} ",array[i]));       if ((i % 4) == 3) Console.Write(" ");     }     Console.WriteLine();   }   public static void Main(String[] args) {     if (args.Length < 1) {       Console.WriteLine("Usage: hashdir <directory>");       return;     }     // Create a DirectoryInfo object representing a directory     DirectoryInfo dir = new DirectoryInfo(args[0]);     // Get FileInfo objects for every file in the directory     FileInfo[] files = dir.GetFiles();     // Initialize a SHA-256 hash object and output variable     SHA256 sha256 = SHA256.Create();     byte[] hashValue;     // Loop over the files, computing & printing hash values     foreach (FileInfo fInfo in files) {       // Create a fileStream for the file       FileStream fileStream = fInfo.Open(FileMode.Open);       // Compute the hash of the fileStream       hashValue = sha256.ComputeHash(fileStream);       // Write the name of the file to the Console       Console.Write(fInfo.Name+": ");       // Write the hash value to the Console       PrintByteArray(hashValue);       // Close the file       fileStream.Close();     }     return;   } } 

Notice that all of the file hashes are computed using a single SHA256 hash object and repeated calls to its ComputeHash method.

Computing Hash Values of Streaming Data Using a CryptoStream

The ComputeHash methods on HashAlgorithm objects provide the easiest way to calculate hashes for fixed streams and arrays of bytes, but if you want to hash data as it is being generated, these methods are not useful. To hash data generated over time, you will need to use a HashAlgorithm object in conjunction with a CryptoStream . The HashAlgorithm class implements the ICryptoTransform interface so that every object that is a descendant of HashAlgorithm can be "wrapped" around a stream, just like an encryption or decryption transform.

HashAlgorithms work a little differently as ICryptoTransforms than the other transforms we have looked at already in that they do not actually modify the data being passed through them. When you use a HashAlgorithm object as an ICryptoTransform , you pass the to-be- hashed data through the transform to build up the hash state within the HashAlgorithm object, but the output of the transform itself is the input unchanged. The value of the Hash property of the HashAlgorithm contains the hash of the content that has passed through the CryptoStream and is constantly updated as data is written to or read from the stream.

NOTE

Notice that the InputBlockSize and OutputBlockSize of a HashAlgorithm are both one byte. This is because the HashAlgorithm is a "pass-through" transform when used in this mode.


Why do HashAlgorithms pass the source data through themselves unmodified when used as an ICryptoTransform , instead of returning the hash value as the output of the transform? The goal behind this particular design choice is to simplify common security protocol operations that mix hashing and other transforms together. One such common operation is "hash-and-encrypt," in which to-be-encrypted plain text is hashed, the hash value is then appended to the plain text, and then the resulting plain text and hash are encrypted values. By including the hash of the plain text in the encryption stream, the intended recipient of the ciphertext can easily verify whether the ciphertext was tampered with after leaving the sender. Any change in the ciphertext will change the resulting decryption output, and the included hash value will no longer match the hash of the decrypted ciphertext . The version of EncryptFile shown in Listing 30.8 is a modification of the code in Listing 30.3 that implements a form of hash-and-encrypt; it hashes the input plain text using the SHA256 algorithm and then encrypts both the plain text and the hash value using Rijndael. (As in Listing 30.3, we also use a random initialization vector that is prepended to the output ciphertext.)

Listing 30.8 Hash-and-Encrypt a Source File to a Target File, Appending the Hash Value to the End of the Encrypted File
 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);   SHA256 sha256 = SHA256.Create();   // Hash the input file as we read from it,   // encrypt as we write to the output file   CryptoStream hashedInStream = new CryptoStream(inStream, sha256, CryptoStreamMode.Read);   CryptoStream encryptedOutStream = new CryptoStream(outStream, aes.CreateEncryptor(), graphics/ccc.gif CryptoStreamMode.Write);   // Write the IV out   outStream.Write(aes.IV, 0, aes.IV.Length);   // Now we're ready to hash and encrypt.   int bytesRead;   byte[] buffer = new byte[1024]; // read 1K at a time   do {     // read from the wrapping CryptoStream!     bytesRead = hashedInStream.Read(buffer,0,1024);     encryptedOutStream.Write(buffer, 0, bytesRead);   }  while (bytesRead > 0);   // Done encrypting, we can close the input stream   inStream.Close();   // Write the hash value out to the encryption stream   encryptedOutStream.Write(sha256.Hash, 0, sha256.Hash.Length);   encryptedOutStream.FlushFinalBlock();   outStream.Close();   return; } 

In this example, note that we have not wrapped one CryptoStream around the other, but rather the hashing CryptoStream around the input stream and the encrypting CryptoStream around the output. For hash-and-encrypt, this particular configuration is easier to work with than the double-wrapping used in Listing 30.3 because we want to hash just the input plain text but encrypt both the plain text and the resulting hash value. So the first CryptoStream , hashedInStream , uses a SHA256 hash algorithm object as its ICryptoTransform and wraps it around the plain text stream in read mode, and the second CryptoStream wraps the encryption transform around the desired output stream in write mode. The hash of the input plain text is automatically calculated as we read bytes from hashedInStream ; all we need to do is write it to the encryptedOutStream after all the ciphertext hash been written.

Decrypting the ciphertext file produced by this version of EncryptFile is a little trickier than producing it, because we need to separate out the encrypted hash value and verify it. In the example method shown in Listing 30.9, we decrypt the ciphertext into a MemoryStream so that we can access the entire decrypted file as a buffer (the msArray variable). After we have the buffer, we can then compute the hash of the plain text portion of the buffer ( hashValue ) and compare it to the hash value stored in the buffer ( msHashValue ).

Listing 30.9 Decrypt and Verify the Hash of a Hashed-and-Encrypted 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;   MemoryStream ms = new MemoryStream();   CryptoStream decryptedOutStream = new CryptoStream(ms, 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();   // Now, compute the hash value of everything but the hash in ms   SHA256 sha256 = SHA256.Create();   int hashSizeInBytes = sha256.HashSize/8;   byte[] msArray = ms.ToArray();   byte[] msHashValue = new byte[hashSizeInBytes];   Array.Copy(msArray, msArray.Length - hashSizeInBytes, msHashValue, 0, hashSizeInBytes);   byte[] hashValue = sha256.ComputeHash(msArray, 0, msArray.Length - hashSizeInBytes);   // compare hashValue and msHashValue   for (int i=0; i< hashSizeInBytes; i++) {     if (msHashValue[i] != hashValue[i]) {       Console.WriteLine("Hash values differ! Encrytped file has been tampered with!");       return;     }   }   return; } 

If the ciphertext file has not been modified since it was created, this routine will decrypt the ciphertext into the plain text file silently. If, however, a modification has been made to the ciphertext file, then the computed hash value will not match the stored hash value and DecryptFile will print out a warning.

"Hash-and-encrypt" is one mechanism for providing authentication and integrity services for plain text. If the computed hash matches the stored hash, the recipient knows two facts about the plain text. First, the plain text originated with an entity that holds a copy of the encryption key; otherwise , the ciphertext would not be decodable by the receiver. Second, the ciphertext was not modified after being encrypted by the sender. In the next section, we describe another technique for providing authentication and integrity for data that does not require an explicit encryption step.

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