Aug. 26, 2007, 11:45 p.m.
posted by vendetta
Protecting Network Datacipher block chaining initialization vector (IV)
The .NET library includes several symmetric encryption algorithm classes, described in Figure.
|
Class |
Description |
|---|---|
|
DESCryptoServiceProvider |
Implements the DES encryption algorithm |
|
RC2CryptoServiceProvider |
Implements the RC2 encryption algorithm |
|
RijndaelManaged |
Implements the Rijndael Managed encryption algorithm |
|
TripleDESCryptoServiceProvider |
Implements the triple DES encryption algorithm |
Asymmetric Encryptionpublic key public key encryption
| Note |
One common technique is to use symmetric encryption to encrypt network data and use asymmetric encryption to encrypt the symmetric private key as it is sent to the remote customer.The .NET library offers two asymmetric encryption classes to encrypt data with private and public keys: |
Using Data Encryption
Using symmetric encryption within network programs can be somewhat tricky. Encrypting data and sending it out on the network is a multistep task, with plenty of opportunity for things to go wrong.
Encrypting Data
The .NET symmetric encryption classes pass data in streams. The tricky part is dealing with data in the stream. All of the standard problems of handling data in a stream apply.
For this example, the TripleDESCryptoServiceProvider class will provide the encryption functionality using the triple DES encryption algorithm. The CryptoStream class passes the blocks of encrypted data through to a Stream object. The stream can be anything including FileStreams, MemoryStreams, and NetworkStreams.
The CryptoStream constructor requires three parameters:
CryptoStream(Stream stream, ICryptoTransform transform, CryptoStreamMode mode)
The first parameter, stream, represents the external Stream object that the encrypted data will be passed to (or read from).
The second parameter, transform, represents the encryption algorithm. To create this value, you must use the CreateEncryptor() method of the TripleDESCryptoServiceProvider class, where the key parameter is the private key value and the iv parameter is the initialization vector:
CreateEncryptor(Key key, IV iv)
Both of these values are 16-byte byte arrays, which must be the same for both sides of the encryption transaction.
The final parameter of the constructor, mode, defines whether the stream will read (CryptoStreamMode.Read) or write (CryptoStreamMode.Write). Unfortunately, the same CryptoStream object cannot both read and write.
A code snippet for encrypting data with symmetric encryption looks something like this:
FileStream fs = new FileStream("test.enc",
FileAccess.Create);
TripleDESCryptoServiceProvider tdes = new TripleDESCryptoServiceProvider();
CryptoStream csw = new CryptoStream(fs,
Tdes.CreateEncryptor(key, iv), CryptoStreamMode.Write);
byte[] data = Encoding.ASCII.GetBytes("Phrase to encrypt");
csw.Write(data, 0, data.Length);
csw.Close();
fs.Close();
Once the CryptoStream object is created, any data written to the stream is encrypted and passed to the underlying stream defined in the constructor (the FileStream object in this example).
Decrypting Data
The decryption process works similar to the encryption process: data is passed through the decryptor to a specified stream object. The decryptor stream is created using the CreateDecryptor() method of the TripleDESCryptoServiceProvider class:
CryptoStream csr = new CryptoStream(memstrm, tdes.CreateDecryptor(key, iv), CryptoStreamMode.Read);
| Warning |
It is imperative that you use the same key and initialization vector values to decrypt the data that encrypted it. Often these values can be transferred in a secure snail-mail message or via a separate encrypted message. |
A simple example of symmetric decryption looks like this:
FileStream fs = new FileStream("test.enc", FileAccess.Open);
TripleDESCryptoServiceProvider tdes = new TripleDESCryptoServiceProvider();
CryptoStream csr = new CryptoStream(fs,
tdes.CreateDecryptor(key, iv), CryptoStreamMode.Read);
byte[] data = new byte[1024];
int recv = csr.Read(data, 0, data.Length);
string phrase = Encoding.ASCII.GetString(data, 0, recv);
csr.Close();
fs.Close();
The CryptoStream again points to the FileStream object, but this time defines a decryptor and uses CryptoStreamMode.Read to enable reading data from the stream. As data is read from the stream, it is automatically decrypted back to the original text.
Sample Encryption/Decryption Program
To see the encryption technique in action, here’s a simple program that encrypts a text phrase and then decrypts it back to the original text. Listing 17.3, the CryptoTest.cs program, is a simple example of how the encryption functions work in a C# program.
using System;
using System.IO;
using System.Security;
using System.Security.Cryptography;
using System.Text;
class CryptoTest
{
...public static void Main()
{
Console.Write("Enter phrase to encrypt: ");
string phrase = Console.ReadLine();
MemoryStream memstrm = new MemoryStream();
byte[] Key = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16};
byte[] IV = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16};
TripleDESCryptoServiceProvider tdes = new TripleDESCryptoServiceProvider();
CryptoStream csw = new CryptoStream(memstrm,
tdes.CreateEncryptor(Key, IV), CryptoStreamMode.Write);
csw.Write(Encoding.ASCII.GetBytes(phrase), 0, phrase.Length);
csw.FlushFinalBlock();
byte[] cryptdata = memstrm.GetBuffer();
Console.WriteLine("Encrypted: {0}",
Encoding.ASCII.GetString(cryptdata, 0, (int)memstrm.Length));
memstrm.Position = 0;
byte[] data = new byte[1024];
CryptoStream csr = new CryptoStream(memstrm,
tdes.CreateDecryptor(Key, IV),
CryptoStreamMode.Read);
int recv = csr.Read(data, 0, data.Length);
string newphrase = Encoding.ASCII.GetString(data, 0, recv);
Console.WriteLine("Decrypted: {0}", newphrase);
csr.Close();
csw.Close();
memstrm.Close();
}
}
The CryptoTest.cs program uses a MemoryStream to hold the encrypted data. The key and IV values are hard-coded into the program, using phony 16-byte values for each. For a real encryption application you should determine real values for these parameters and keep them secret.
After the string phrase is converted to a byte array, encrypted, and stored in the MemoryStream, the FlushFinalBlock() method is called. This method ensures that all of the encrypted blocks have been sent out to the stream, and none are sitting in the encryption class buffer space.
After the phrase is encrypted, the MemoryStream contents are converted into a byte array and displayed on the console as a string. (Because the encrypted value will contain non-ASCII text characters, you may see some interesting results.) The idea is to see that the data really was encrypted.
After the encrypted data is displayed, the MemoryStream object place pointer is reset back to the beginning of the stream, and the decryptor method converts the encrypted text back into the original string phrase. Here’s a sample output from the program:
C:\>CryptoTest
Enter phrase to encrypt: A test phrase.
Encrypted: ~/@'F?p{=.$g=R2U
Decrypted: A test phrase.
C:\>
Network Data Encryption
Although it is convenient to use the CryptoStream class with FileStream and MemoryStream objects, it is often difficult to use it with NetworkStream objects. The downside to using the CryptoStream class with NetworkStream objects is the same old issue of determining data boundaries within the data stream. When data is encrypted and passed directly to a NetworkStream object, there is no easy way of determining when the receiver has received the end of the encrypted data. If the decryptor attempts to decrypt the data before the end of the stream has arrived, an error will occur.
Handling Encrypted Data
The solution to the data boundary issue is to deploy one of the common stream data handling techniques discussed in Chapter 7, “Using the C# Sockets Helper Classes.” As described in Chapter 7, to handle stream data you can either use fixed-length messages, send the message size in the stream before the message, or use message boundary markers.
The easiest technique to use with encrypted messages is to determine the size of each encrypted message and send it in the stream before the actual message. The receiving program can extract the message size from the NetworkStream and will know exactly how many bytes of data to read from the network to properly form the encrypted message. The receiving program can then forward the entire encrypted message to the appropriate decryptor to extract the original data.
To determine the size of each encrypted message, the message must be stored in a buffer after being encrypted and before being sent out on the NetworkStream. Once the encrypted message is in the buffer, it’s easy to determine the size of the message and send both the message size and the message out on the NetworkStream. A simple solution is to use the MemoryStream class, as demonstrated in the CryptoTest.cs program. By forwarding the CryptoStream output to a MemoryStream, you can use the GetBuffer() method to create a byte array with the encrypted message. The array can then be sent out on the network just like any other byte array of data.
On the receiver side, the opposite steps are performed. Data read from the NetworkStream is fed for decryption into the CryptoStream object, which points to a MemoryStream object. Once the entire decrypted message is in the MemoryStream object, it can be extracted using the GetBuffer() method.
Figure illustrates both the sender and receiver processes in network data encryption.
The CryptoDataSender Program
The CryptoDataSender.cs program, Listing 17.4, demonstrates encryption of complex data for sending on the network. CryptoDataSender takes the original BetterDataSender.cs program (Listing 16.6) and adds triple DES encryption to secure the data being sent.
| Note |
Just like the original program, the CryptoDataSender program must be compiled with the SerialEmployee.dll file created in Chapter 16 using the /r: compiler option: csc /r:SerialEmployee.dll CryptoDataSender.cs. |
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Soap;
using System.Security;
using System.Security.Cryptography;
using System.Text;
class CryptoDataSender
{
private void SendData (NetworkStream strm, SerialEmployee emp)
{
IFormatter formatter = new SoapFormatter();
MemoryStream memstrm = new MemoryStream();
byte[] Key = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16};
byte[] IV = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16};
TripleDESCryptoServiceProvider tdes = new TripleDESCryptoServiceProvider();
CryptoStream csw = new CryptoStream(memstrm, tdes.CreateEncryptor(Key, IV), CryptoStreamMode.Write);
formatter.Serialize(csw, emp);
csw.FlushFinalBlock();
byte[] data = memstrm.GetBuffer();
int memsize = (int)memstrm.Length;
byte[] size = BitConverter.GetBytes(memsize);
strm.Write(size, 0, 4);
strm.Write(data, 0, (int)memsize);
strm.Flush();
csw.Close();
memstrm.Close();
}
public CryptoDataSender()
{
SerialEmployee emp1 = new SerialEmployee();
SerialEmployee emp2 = new SerialEmployee();
emp1.EmployeeID = 1;
emp1.LastName = "Blum";
emp1.FirstName = "Katie Jane";
emp1.YearsService = 12;
emp1.Salary = 35000.50;
emp2.EmployeeID = 2;
emp2.LastName = "Blum";
emp2.FirstName = "Jessica";
emp2.YearsService = 9;
emp2.Salary = 23700.30;
TcpClient client = new TcpClient("127.0.0.1", 9050);
NetworkStream strm = client.GetStream();
SendData(strm, emp1);
SendData(strm, emp2);
strm.Close();
client.Close();
}
public static void Main()
{
CryptoDataSender cds = new CryptoDataSender();
}
}
If you remember, the BetterDataSender program used SOAP serialization to serialize an employee data object for sending to a remote device across the network. Because the SOAPFormatter was used, all of the data was sent in text mode across the network. Should that data include sensitive information such as a Social Security number, anyone viewing the packet would see the information. You don’t want this to happen, of course, in your applications.
The CryptoDataSender program creates the same SOAP serialization of the data, but then it runs it through the CryptoStream object to encrypt the information before sending it out:
formatter.Serialize(csw, emp); csw.FlushFinalBlock(); byte[] data = memstrm.GetBuffer(); int memsize = (int)memstrm.Length; byte[] size = BitConverter.GetBytes(memsize); strm.Write(size, 0, 4); strm.Write(data, 0, (int)memsize);
The SOAPFormatter Serialize() method serializes the employee data object to a stream but uses the CryptoStream stream object instead of the NetworkStream. The FlushFinalBlock() method ensures that all of the encrypted blocks are flushed out of the buffer. Because the CryptoStream object pointed to a MemoryStream object, that object contains the entire encrypted, SOAP-serialized, SerialEmployee data object. The MemoryStream object is then converted to a byte array, the size of the data is determined, and both are sent out on the NetworkStream object to the remote receiver.
The CryptoDataRcvr Program
Now let’s look at the receiving program. The CryptoDataRcvr.cs program, Listing 17.5, demonstrates how to read and decrypt data from a NetworkStream. As expected, the CryptoDataRcvr.cs program mimics the BetterDataRcvr program of Listing 16.7.
| Note |
Again, the CryptoDataRcvr program must be compiled with the SerialEmployee.dll file from Chapter 16 using the /r: option: csc /r:SerialEmployee.dll CryptoDataRcvr.cs. |
using System; using System.IO; using System.Net; using System.Net.Sockets; using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters.Soap; using System.Security; using System.Security.Cryptography; using System.Text; class CryptoDataRcvr { private SerialEmployee RecvData (NetworkStream strm) { MemoryStream memstrm = new MemoryStream(); byte[] Key = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16}; byte[] IV = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16}; TripleDESCryptoServiceProvider tdes = new TripleDESCryptoServiceProvider(); CryptoStream csw = new CryptoStream(memstrm, tdes.CreateDecryptor(Key, IV), CryptoStreamMode.Write); byte[] data = new byte[2048]; int recv = strm.Read(data, 0, 4); int size = BitConverter.ToInt32(data, 0); int offset = 0; while(size > 0) { recv = strm.Read(data, 0, size); csw.Write(data, offset, recv); offset += recv; size -= recv; } csw.FlushFinalBlock(); IFormatter formatter = new SoapFormatter(); memstrm.Position = 0; SerialEmployee emp = (SerialEmployee)formatter.Deserialize(memstrm); memstrm.Close(); return emp; } public CryptoDataRcvr() { TcpListener server = new TcpListener(9050); server.Start(); Console.WriteLine("Waiting for a client..."); TcpClient client = server.AcceptTcpClient(); NetworkStream strm = client.GetStream(); SerialEmployee emp1 = RecvData(strm); Console.WriteLine("emp1.EmployeeID = {0}", emp1.EmployeeID); Console.WriteLine("emp1.LastName = {0}", emp1.LastName); Console.WriteLine("emp1.FirstName = {0}", emp1.FirstName); Console.WriteLine("emp1.YearsService = {0}", emp1.YearsService); Console.WriteLine("emp1.Salary = {0}\n", emp1.Salary); SerialEmployee emp2 = RecvData(strm); Console.WriteLine("emp2.EmployeeID = {0}", emp2.EmployeeID); Console.WriteLine("emp2.LastName = {0}", emp2.LastName); Console.WriteLine("emp2.FirstName = {0}", emp2.FirstName); Console.WriteLine("emp2.YearsService = {0}", emp2.YearsService); Console.WriteLine("emp2.Salary = {0}", emp2.Salary); strm.Close(); server.Stop(); } public static void Main() { CryptoDataRcvr cdr = new CryptoDataRcvr(); } }
In Chapter 16’s BetterDataRcvr program, the data was read from the NetworkStream and fed directly into the SOAP Deserialize() method. Here in the CryptoDataRcvr program, however, there is obviously another step involved. Because the new data is encrypted, it must be decrypted before it can be deserialized.
First, the size of the encrypted data array is read from the NetworkStream. Knowing the size of the data array to expect, a while loop ensures that all of the encrypted data is read from the network:
while(size > 0)
{
data = new byte[2048];
recv = strm.Read(data, 0, size);
csw.Write(data, offset, recv);
offset += recv;
size -= recv;
}
csw.FlushFinalBlock();
As each block of data is read from the network, it is fed to the CryptoStream stream, which decrypts the data and stores it in the created MemoryStream object. When all of the data has been received from the network, the FlushFinalBlock() method ensures that all of the decrypted data blocks have been sent to the MemoryStream.
After all of the decrypted data is placed in the MemoryStream object, the pointer is reset, and the MemoryStream object is used in the Deserializer() method to produce the original employee data object.
Testing the Programs
After compiling both the CryptoDataSender.cs and CryptoDataRcvr.cs programs (using the SerialEmployee.dll file), you can test them out on your network. The final result should be the complete data for both employee records:
C:\>CryptoDataRcvr Waiting for a client... emp1.EmployeeID = 1 emp1.LastName = Blum emp1.FirstName = Katie Jane emp1.YearsService = 12 emp1.Salary = 35000.5 emp2.EmployeeID = 2 emp2.LastName = Blum emp2.FirstName = Jessica emp2.YearsService = 9 emp2.Salary = 23700.3 C:\>
As shown in the output, both employee data objects sent by the sender were successfully received by the receiver. However, this does not prove that the encryption really did take place. The only way to see this is to watch the packets as they are sent across the network. If you are testing these applications on separate devices on a network, you can use the WinDump or Analyzer programs to watch the packets on the network. Figure shows one packet in the Analyzer program. You can see that the data contained in the packets has indeed been encrypted, preventing the casual network snooper from seeing your data.

