Richard K. Blum

Subscribe to Richard K. Blum: eMailAlertsEmail Alerts
Get Richard K. Blum: homepageHomepage mobileMobile rssRSS facebookFacebook twitterTwitter linkedinLinkedIn


Article

Encrypting Data in Network Connections

Encrypting Data in Network Connections

In today's computing environment, creating applications that transfer data between devices on networks has become a necessity for programmers. Fortunately, Microsoft has included several classes in the .NET Framework that make network programming easy. The TcpClient, TcpListener, and NetworkStream classes are popular classes that provide all the functionality necessary to pass data across any network.

However, sending data across networks can be a risky business. There are lots of prying eyes watching packets as they traverse the network. This can be a serious problem if your application handles sensitive information such as employee addresses and phone numbers. Most commercial applications incorporate some type of encryption technique to hide the data as it is sent over the network. While encrypting data is not 100% foolproof, it adds a basic level of protection to the data.

The .NET Framework provides several cryptographic classes that can easily be incorporated into your network programs to help disguise your data as it travels across the network. This article demonstrates how to incorporate the cryptographic classes in network applications to create a safer environment for your application data.

Symmetric vs Asymmetric Encryption
There are lots of different encryption techniques available in the .NET Framework, and trying to differentiate between them can be difficult. Basically, they fall into two main schemes: symmetric encryption classes and asymmetric encryption classes.

Asymmetric encryption classes use two separate keys for encryption and decryption. The device receiving the data uses a private key to decrypt data as it is received. Any remote device wanting to send encrypted data to the receiver must use a separate public key to encrypt the data before it is sent. This is demonstrated in Figure 1.

 

The nice feature of this technique is that people who only have the public key can't decrypt messages encrypted with the public key. Only the owner of the private key can decrypt the messages. This helps prevent others from decrypting your messages. Unfortunately, this also means that applications that communicate with many devices must maintain many different public keys.

Asymmetric encryption encrypts data in blocks, using a fixed block size. Each block of data is encrypted separately from the other blocks. Unfortunately this feature makes asymmetric encryption difficult to use on data contained in a stream (such as a FileStream or NetworkStream).

Symmetric encryption algorithms use a single key to both encrypt and decrypt data. The same key must be shared between all parties involved in the communication. Because of this, symmetric encryption is often referred to as private-key encryption. The same private key must be held by everyone, and is used to both encrypt and decrypt messages between all parties. Because of this, you must trust all of the people that have the private key, as they can decrypt any encrypted message intended for you.

Symmetric encryption algorithms also encrypt data in blocks, but have the ability to pad data to ensure that the same block size is used for each block, regardless of the amount of data. With symmetric encryption, each block of encrypted data can be linked together with previously encrypted blocks (called cipher-block chaining). By chaining blocks of encrypted data together, symmetric encryption can simulate a data stream environment. This feature makes it ideal for use with stream objects.

The .NET Framework provides four different symmetric encryption classes that can be used to encrypt data. These are shown in Table 1.

 

As mentioned, each of the symmetric encryption algorithms uses a private key to encrypt and decrypt the data. Along with the private key, an initialization vector (IV) is used as a known seed value shared between all parties to add a second level of security to the encryption. The private key and IV values together are used to define the encryption/decryption code. They are each defined as 16-byte byte arrays, allowing for many possible encryption codes.

Creating a Symmetric Encryption Stream
The CryptoStream class is used to pass encrypted blocks of data to an underlying stream. The underlying stream can be any Stream type - FileStream, MemoryStream, or NetworkStream. The CryptoStream constructor requires three parameters:

CryptoStream(Stream stream,
ICryptoTransform transform,
CryptoStreamMode mode)

The first parameter, stream, represents the underlying stream the encrypted data will be passed to (or read from). The third parameter, mode, defines whether the CryptoStream will read data from, or write data to, the underlying stream (unfortunately, you cannot use the same CryptoStream object to both read and write data on the same stream).

The second parameter, transform, is the tricky one. It controls the encryption algorithm used, and whether the stream will be used for encrypting or decrypting the data. For this parameter you must use either the CreateEncryptor() or CreateDecryptor() method from one of the symmetric encryption classes in Table 1 to define the specific algorithm used.

When the encryptor and decryptor are defined they must include the private key and IV values used to encrypt or decrypt the data. And of course, both the encryptor and decryptor must use the same key and IV values. Listing 1 shows a sample code snippet that demonstrates how to create an encryptor in an application.

This code creates the private key and IV for the triple DES encryption as a 16-byte byte array (for a real system, you would want to mix up the byte values some). Next, a FileStream is created that will hold the encrypted data, along with an empty TripleDESCryptoServiceProvider object to define the symmetric encryption type. Finally, the CryptoStream object is created, pointing to the output FileStream and the encryption type CreateEncryptor() method.

Data is written to the encryptor using the Write() method of the CryptoStream object. As the data is written to the CryptoStream, it is encrypted and sent to the underlying stream defined in the constructor (in this case an output file). After the Write() method, a FlushFinalBlock() method is called. This method is used to ensure that none of the encrypted data blocks are left in the stream buffer before the buffer is closed.

The decryptor code is shown in Listing 2. In this example, the CryptoStream is connected to a FileStream that contains the encrypted data. The CreateDecryptor() method is used, along with the CryptoStreamMode.Read mode to read data from the stream and decrypt it. As data is decrypted from the stream, the plain text is stored in a byte array, which is then converted to a string object.

Figure 2 shows the data flow between the encryptor and decryptor processes. As plain text data is fed into the encryptor CryptoStream, it is encrypted and placed in the underlying FileStream. The FileStream is used as a holding area for the encrypted data to be stored and read from the decryptor. You can view the created file to see what the encrypted data looks like, It should appear as gibberish (including binary data), and should in no way resemble the original text data. The decryptor CryptoStream reads the data from the FileStream and decrypts the blocks of data back into the original text messages.

 

The TCP/IP Problem
Reading the encrypted data from the FileStream object was easy - the decryptor program knew when all of the encrypted data was read when the end of the data file was reached. This is not the case when dealing with data on a NetworkStream. The problem with sending encrypted data streams with a NetworkStream object is that it is difficult to determine when the end of the encrypted data is received from the remote host.

TCP is a stream-oriented protocol. As separate messages are fed into the same stream, they all merge to become a single stream of data, with no delineation between individual messages. As the stream is read, multiple messages can be extracted from the stream in a single Read() method. This is a problem for the decryptor program. For the CryptoStream to properly decrypt a message it must know exactly where each encrypted message starts and stops.

There are three methods that are commonly used to differentiate messages in TCP streams. The first method is similar to the FileStream solution - terminating the stream at the end of a message. This means only one encrypted message can be sent per TCP connection. In applications where this method is impractical (such as a chat program), you must resort to one of the other two methods - sending a message boundary marker between messages, or sending each message size to the remote device.

For encrypted messages, boundary markers are often impractical, as it is difficult to determine a character to use as the marker. The easiest solution is to determine the size of each encrypted message, and send it to the remote device before the actual message. When the remote device receives the message size, it knows exactly how many bytes to read from the NetworkStream and feed into the decryptor. The problem is often how to accomplish this task.

Using a MemoryStream
If the data is fed directly to a NetworkStream, it is impossible to determine its size. The solution is the MemoryStream object. By feeding the output of the CryptoStream to a MemoryStream instead of directly to the NetworkStream, you can determine the encrypted data length of the message, and then easily move the data into a byte array for sending out the NetworkStream.

The MemoryStream object is linked to the CryptoStream, placing the output of the encrypted data directly into the MemoryStream. The MemoryStream can then be converted to a byte array using the GetBytes() method, and the length of the encrypted data is determined using the Length property of the MemoryStream (which must be typecast to an int).

A Complete Encrypted Network Application
Listing 3 shows the CryptoNet class, which contains two methods, CryptoSend() and CryptoRecv(). The CryptoSend() method accepts a string object, encrypts it into a MemoryStream, then sends the size of the encrypted data and the data itself out on a specified NetworkStream. The CryptoRecv() method accepts a NetworkStream object, reads a data size received from the stream, and then reads that amount of data from the stream. The data is decrypted, placed in a MemoryStream, and then converted into a string object, which is returned to the calling program.

Using the CryptoNet class in network programs is a snap. Listing 4 shows the CryptoSrvr program, which acts as an echo server for encrypted messages. Listing 5 shows the CryptoClient program, which connects to the server, sends an encrypted message, and waits for the encrypted message to be returned. Both programs use the CryptoNet classes to encrypt and decrypt the data.

To compile the CryptoClent and CryptoSrvr classes, you must include the CryptoNet class on the compiler line:

C:\> csc CryptoSrvr.cs CryptoNet.cs

This creates the CryptoSrvr.exe executable program that can be run on the workstation. Do the same to compile the CryptoClient program. You can then start the CryptoSrvr program, and from a separate Command Prompt window start the CryptoClient program. It should connect, and allow you to pass messages between the applications. If you have a network available, you can change the IP address of the server in the CryptoClient program, and run the programs on separate devices. You can verify that the data is indeed encrypted by watching the packets on the network with a sniffer program.

Conclusion
The .NET cryptographic classes can easily be combined with the standard network classes to provide full-featured encrypted communications across networks. This provides a basic level of security for your network applications.

More Stories By Richard K. Blum

Richard Blum currently works for a large US government organization as a network and systems administrator. He is the author of C# Network Programming (2002, Sybex) and Professional Assembly Language (2005, Wrox).

Comments (0)

Share your thoughts on this story.

Add your comment
You must be signed in to add a comment. Sign-in | Register

In accordance with our Comment Policy, we encourage comments that are on topic, relevant and to-the-point. We will remove comments that include profanity, personal attacks, racial slurs, threats of violence, or other inappropriate material that violates our Terms and Conditions, and will block users who make repeated violations. We ask all readers to expect diversity of opinion and to treat one another with dignity and respect.