Background
Many times, I created TCP/IP based programs, but without using any special security for them. The problem appeared when I created a chat program and wanted to send login/password and also avoid someone "sniffing the network" to see this information or even the conversation.
I used SslStream. It works, but acquiring the certificates is the problem and, to be honest, my only problem was about the "sniffing", not about checking if the server is REALLY the server it should be, or if the client is the real client. I simply accepted all certificates, I needed the cryptography.
The Solution
Well, I soon looked for the solution without the certificates. I knew that SSL uses an asymmetric key to connect, and then creates a symmetric key to continue its communication. Looking at the System.Security.Cryptography
I saw the CryptoStream
. I thought it was the solution (at least for the symmetric part), but it wasn't. The CryptoStream
is unidirectional (so it is not OK for TCP/IP), its Flush doesn't flush the stream and, if I use FlushFinalBlock
, I must dispose the stream. So, I decided to look at the Symmetric and Asymmetric algorithms directly.
After some study, I created a solution. Maybe not the fastest one, but it works.
During initialization, the server creates an asymmetric key and sends the public part to the client. The client then creates an symmetric key (at the moment only he knows the key) and encrypts it using the server public key (so, only the server knows how to decrypt it). It sends the key to the server, and then, only this symmetric key is used.
Simple, but as the asymmetric and the symmetric key are created during connection, there is no chance of someone else also knowing the keys. And, as the symmetric key is sent using the cryptography that only the server knows how to decrypt, even someone sniffing the network with a good cryptography knowledge will not have anything to do.
So, let's see the implementation.
public SecureStream(Stream baseStream, RSACryptoServiceProvider rsa,
SymmetricAlgorithm symmetricAlgorithm, bool runAsServer)
{
if (baseStream == null)
throw new ArgumentNullException("baseStream");
if (rsa == null)
throw new ArgumentNullException("rsa");
if (symmetricAlgorithm == null)
throw new ArgumentNullException("symmetricAlgorithm");
BaseStream = baseStream;
SymmetricAlgorithm = symmetricAlgorithm;
string symmetricTypeName = symmetricAlgorithm.GetType().ToString();
byte[] symmetricTypeBytes = Encoding.UTF8.GetBytes(symmetricTypeName);
if (runAsServer)
{
byte[] sizeBytes = BitConverter.GetBytes(symmetricTypeBytes.Length);
baseStream.Write(sizeBytes, 0, sizeBytes.Length);
baseStream.Write(symmetricTypeBytes, 0, symmetricTypeBytes.Length);
byte[] bytes = rsa.ExportCspBlob(false);
sizeBytes = BitConverter.GetBytes(bytes.Length);
baseStream.Write(sizeBytes, 0, sizeBytes.Length);
baseStream.Write(bytes, 0, bytes.Length);
symmetricAlgorithm.Key = p_ReadWithLength(rsa);;
symmetricAlgorithm.IV = p_ReadWithLength(rsa);
}
else
{
var sizeBytes = new byte[4];
p_FullReadDirect(sizeBytes);
var stringLength = BitConverter.ToInt32(sizeBytes, 0);
if (stringLength != symmetricTypeBytes.Length)
throw new ArgumentException
("Server and client must use the same SymmetricAlgorithm class.");
var stringBytes = new byte[stringLength];
p_FullReadDirect(stringBytes);
var str = Encoding.UTF8.GetString(stringBytes);
if (str != symmetricTypeName)
throw new ArgumentException
("Server and client must use the same SymmetricAlgorithm class.");
sizeBytes = new byte[4];
p_FullReadDirect(sizeBytes);
int asymmetricKeyLength = BitConverter.ToInt32(sizeBytes, 0);
byte[] bytes = new byte[asymmetricKeyLength];
p_FullReadDirect(bytes);
rsa.ImportCspBlob(bytes);
p_WriteWithLength(rsa, symmetricAlgorithm.Key);
p_WriteWithLength(rsa, symmetricAlgorithm.IV);
}
rsa.Clear();
Decryptor = symmetricAlgorithm.CreateDecryptor();
Encryptor = symmetricAlgorithm.CreateEncryptor();
fReadBuffer = new byte[0];
fWriteBuffer = new MemoryStream(32 * 1024);
}
The constructor is large, but I will explain the key parts. It was created to be able to receive an already created and initialized RSA asymmetric algorithm and asymmetric algorithm of the users' choice. It has other constructors to initialize these objects with simple new RSACyptoServiceProvider
and SymmetricAlgorithm.Create
. After checking for null arguments and setting BaseStream
and SymmetricAlgorithm
properties, it must decide if it will run as a server or as a client. I will begin with the server, as it is the server that initializes the process.
The Server
The server sends the name of the symmetric algorithm being used, which will be used by the client to check for compatibility, exports the public RSA key it generated and also sends it to the client and, finally, reads the symmetric key and initialization vector that will be sent by the client.
The Client
The client does the reverse process of the server. So, it first receives the algorithm named used by the server. If the length of the algorithm name or the name itself don't match, it throws an exception. Later, it receives the RSA key used by the server and sends the Key and Initialization Vector of its symmetric algorithm.
Ok, I didn't show the encryption with the RSA key, but it is done by the p_ReadWithLength
and p_WriteWithLength
, that I will show later. Only to finish the constructor, it clears the RSA key, creates the symmetric encryptor and decryptor and initializes the buffers.
Let's see the Asymmetric cryptography:
private byte[] p_ReadWithLength(RSACryptoServiceProvider rsa)
{
byte[] size = new byte[4];
p_FullReadDirect(size);
int count = BitConverter.ToInt32(size, 0);
var bytes = new byte[count];
int read = 0;
while(read < count)
{
int readResult = BaseStream.Read(bytes, read, count - read);
if (readResult == 0)
throw new IOException("The stream was closed by the remote side.");
read += readResult;
}
return rsa.Decrypt(bytes, false);
}
private void p_WriteWithLength(RSACryptoServiceProvider rsa, byte[] bytes)
{
bytes = rsa.Encrypt(bytes, false);
byte[] sizeBytes = BitConverter.GetBytes(bytes.Length);
BaseStream.Write(sizeBytes, 0, sizeBytes.Length);
BaseStream.Write(bytes, 0, bytes.Length);
}
The Write
encrypts the message, and then sends the size of the encrypted message and the message itself.
The Read
reads the size, then reads the message and to finish decrypts and returns the decrypted message. But, wait, why I use "BaseStream.Write
" and p_FullReadDirect
? Why not BaseStream.Read
?
I am really thinking about making this an extension method. If you look at how Read
and Write
works, you will notice the difference. Write simply writes all the requested buffers or throws an exception. Read is more problematic, as you can ask for 1024 bytes, and it returns 3, because it read only 3 bytes. But I don't expect this to happen, I want the full block, even if I need to call read many times. So, p_FullReadDirect
looks like this:
private void p_FullReadDirect(byte[] bytes, int length)
{
int read = 0;
while(read < length)
{
int readResult = BaseStream.Read(bytes, read, length - read);
if (readResult == 0)
throw new IOException("The stream was closed by the remote side.");
read += readResult;
}
}
Ok. The initialization is done. At this moment, we can say that we have the fully implemented constructor, we already used the RSA algorithm (the asymmetric) and now we only need to care about the real streaming.
So, let's first understand the Encryptor and Decryptor. At least, the part I understood:
The Encryptor and the Decryptor are ICryptoTransform
. In it, we have the TransformBlock
and TransformFinalBlock
. I really considered using TransformBlock
, but in my tests, I encrypt a block and try to decrypt it, and nothing happens. If I join the blocks and at the end call TransformFinalBlock
, I get the wrong result, so I decided to use only TransformFinalBlock
, that alone works fine.
The problem is that at each "final encryption", I can end-up with an extra size in the message. So, instead of encrypting each write, I buffer all of them in a memory stream and, during Flush, I encrypt all of the writes and send them.
So:
public override void Write(byte[] buffer, int offset, int count)
{
fWriteBuffer.Write(buffer, offset, count);
}
public override void Flush()
{
if (fWriteBuffer.Length > 0)
{
var encryptedBuffer = Encryptor.TransformFinalBlock
(fWriteBuffer.GetBuffer(), 0, (int)fWriteBuffer.Length);
var size = BitConverter.GetBytes(encryptedBuffer.Length);
BaseStream.Write(size, 0, size.Length);
BaseStream.Write(encryptedBuffer, 0, encryptedBuffer.Length);
BaseStream.Flush();
fWriteBuffer.SetLength(0);
fWriteBuffer.Capacity = 32 * 1024;
}
}
I always reset the buffer capacity to 32K, as in one single moment we can send 1MB of information but, after, we continue with 32KB. I could make it configurable, but it will not change the real important thing here:
I first group all the buffers, which is not the problem. But, when I send it, I must also send the encrypted size, as to Decrypt with TransformFinalBlock
we need the full block to be loaded. The decryption itself is not very complicated, but the read method is.
Why?
Because the remote side sends 1MB of data. To decrypt, I must read the 1MB of data and decrypt. But, the calling code only wants to read 4 bytes. I can't simply discard the rest of the buffer, I must read the part of the requested buffer and update the internal position, so the next read can read another part of the buffer. Also, we can have a buffer of 16 bytes and a read of 1024 bytes but, in this case, we use the Read behavior of returning that one 16 was read.
So, let's see:
public override int Read(byte[] buffer, int offset, int count)
{
if (fReadPosition == fReadBuffer.Length)
{
p_ReadDirect(fSizeBytes);
int readLength = BitConverter.ToInt32(fSizeBytes, 0);
if (fReadBuffer.Length < readLength)
fReadBuffer = new byte[readLength];
p_FullReadDirect(fReadBuffer, readLength);
fReadBuffer = Decryptor.TransformFinalBlock(fReadBuffer, 0, readLength);
fReadPosition = 0;
}
int diff = fReadBuffer.Length - fReadPosition;
if (count > diff)
count = diff;
Buffer.BlockCopy(fReadBuffer, fReadPosition, buffer, offset, count);
fReadPosition += count;
return count;
}
When we do the first read, we are at position 0, and the readbuffer
has size 0, so we enter the if
. We will read the message size, create a new readbuffer
if ours does not have enough length and then will "fullread
" the messagesize
. With this, we have the full encrypted buffer, so decrypt it to the readbuffer
variable and tell that we are at the beginning of it.
Lefting the if
block, we will execute check if the read wants to read more bytes than we still have. If that's the case, we reduce the count variable. Then, we simply "BlockCopy
" the block we want, update the ReadPosition
and return the number of bytes read (the count).
Well, that's it. If we read "byte by byte", we will keep incrementing ReadPosition
while we still have a valid buffer in memory. As soon as it ends, we will receive the next one. And that's all. The stream is already working.
Usage
The usage is very simple. When you get a TCP/IP connection, you will probably already use the GetStream()
to read and write to the connection. You will only need to create a new SecureStream
over the TCP/IP stream and tell if it is the client or the server, and everything is done.
So, a little example, an encrypted "echo port".
using System.IO;
using System.Net.Sockets;
using System.Threading;
using Pfz.Remoting;
namespace Server
{
class Program
{
static void Main(string[] args)
{
var listener = new TcpListener(657);
listener.Start();
while(true)
{
TcpClient client = listener.AcceptTcpClient();
Thread thread = new Thread(p_ClientConnected);
thread.Start(client);
}
}
private static void p_ClientConnected(object data)
{
try
{
using(TcpClient client = (TcpClient)data)
{
var baseStream = client.GetStream();
var stream = new SecureStream(baseStream, true);
using(var reader = new StreamReader(stream))
{
var writer = new StreamWriter(stream);
while(true)
{
string line = reader.ReadLine();
if (line == null)
return;
writer.WriteLine(line);
writer.Flush();
}
}
}
}
catch
{
}
}
}
}
Try connecting by the telnet to see the difference.
Well, download the projects if you want the full source-code and want to run the samples.
To finish, I hope this helps those who need to create secure streams, but don't want to deal with certificates and SSL streams.
History
- 24th October, 2009: Initial post