Click here to Skip to main content
Click here to Skip to main content

Creating an Asymmetric/Symmetric Secure Stream without SSL

By , 24 Oct 2009
Rate this:
Please Sign up or sign in to vote.

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
  {
    // ok. We run as the client, so first we first check the
    // algorithm types and then receive the asymmetric
    // key from the server.
    
    // symmetricAlgorithm
    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.");

    // public key.
    sizeBytes = new byte[4];
    p_FullReadDirect(sizeBytes);
    int asymmetricKeyLength = BitConverter.ToInt32(sizeBytes, 0);
    byte[] bytes = new byte[asymmetricKeyLength];
    p_FullReadDirect(bytes);
    rsa.ImportCspBlob(bytes);
    
    // Now that we have the asymmetricAlgorithm set, and considering
    // that the symmetricAlgorithm initializes automatically, we must
    // only send the key.
    p_WriteWithLength(rsa, symmetricAlgorithm.Key);
    p_WriteWithLength(rsa, symmetricAlgorithm.IV);
  }
      
  // After the object initialization being done, be it a client or a
  // server, we can dispose the assymetricAlgorithm.
  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)
        {
          // if you don't want to encrypt the data, set stream to baseStream
          // directly.
          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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Paulo Zemek
Architect
Canada Canada
I started to program computers when I was 11 years old, as a hobbist, programming in AMOS Basic and Blitz Basic for Amiga.
At 12 I had my first try with assembler, but it was too difficult at the time. Then, in the same year, I learned C and, after learning C, I was finally able to learn assembler (for Motorola 680x0).
Not sure, but probably between 12 and 13, I started to learn C++. I always programmed "in an object oriented way", but using function pointers instead of virtual methods.
 
At 15 I started to learn Pascal at school and to use Delphi. At 16 I started my first internship (using Delphi). At 18 I started to work professionally using C++ and since then I've developed my programming skills as a professional developer in C++ and C#, generally creating libraries that help other developers do they work easier, faster and with less errors.
 
Want more info or simply want to contact me?
Take a look at: http://paulozemek.azurewebsites.net/
Or e-mail me at: paulozemek@outlook.com
 
Codeproject MVP 2012
Microsoft MVP 2013

Comments and Discussions

 
GeneralMy vote of 5 Pinmemberadriancs4-Jul-12 2:41 
GeneralGreate example PinmemberMember 364031215-May-11 21:03 
GeneralRe: Greate example PinmemberPaulo Zemek16-May-11 2:22 
GeneralGreat work PinmemberMarcelo Ricardo de Oliveira1-Jun-10 4:17 
GeneralA few Loopholes to take note of PinmemberDashmesh18-Mar-10 4:16 
QuestionWhy not use a streaming encryption mode PinmemberSkippums7-Jan-10 11:40 
AnswerRe: Why not use a streaming encryption mode PinmemberPaulo Zemek9-Jan-10 1:44 
To be honest, I don't know. I must say that I don't use Aes.
But, even if that works, there is the problem of using only a symmetric algorithm. Anyone with the source-code of the client can easily create a program to decrypt all the data transferred by others. Surelly it will avoid any sniffer to see what's happening, but does not have the same level of security.
GeneralRe: Why not use a streaming encryption mode PinmemberSkippums9-Jan-10 5:11 
GeneralRe: Why not use a streaming encryption mode PinmemberPaulo Zemek9-Jan-10 5:38 
GeneralMITM Attack PinmemberMrProphet19-Dec-09 7:16 
GeneralRe: MITM Attack PinmemberPaulo Zemek19-Dec-09 13:26 
GeneralRe: MITM Attack PinmemberMrProphet11-Jul-10 11:50 
GeneralRe: MITM Attack PinmemberPaulo Zemek12-Jul-10 12:10 
GeneralRe: MITM Attack PinmemberMrProphet13-Jul-10 5:07 
GeneralRe: MITM Attack PinmemberPaulo Zemek13-Jul-10 5:16 
QuestionCool example, but maybe there is simpler... PinmemberNicolas Dorier1-Nov-09 9:58 
AnswerRe: Cool example, but maybe there is simpler... PinmemberPaulo Zemek2-Nov-09 5:33 
GeneralRe: Cool example, but maybe there is simpler... PinmemberNicolas Dorier2-Nov-09 10:22 
GeneralRe: Cool example, but maybe there is simpler... PinmemberPaulo Zemek3-Nov-09 0:06 
GeneralInteresting Pinmembersupercat926-Oct-09 11:10 
GeneralRe: Interesting PinmemberPaulo Zemek26-Oct-09 13:18 
GeneralRe: Interesting Pinmembersupercat927-Oct-09 6:26 
GeneralRe: Interesting [modified] PinmemberPaulo Zemek27-Oct-09 6:50 
GeneralGreat PinmemberDaniel Vaughan25-Oct-09 13:31 
GeneralRe: Great PinmemberPaulo Zemek26-Oct-09 1:34 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140415.2 | Last Updated 24 Oct 2009
Article Copyright 2009 by Paulo Zemek
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid