Click here to Skip to main content
15,885,216 members
Articles / Programming Languages / C#

Creating an Asymmetric/Symmetric Secure Stream without SSL

Rate me:
Please Sign up or sign in to vote.
4.93/5 (29 votes)
24 Oct 2009CPOL7 min read 69.7K   1.1K   73  
The article explains how to create a secure stream that uses asymmetric cryptography to connect and symmetric cryptography to continue without the need for SSL or Certificates
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

namespace Pfz.Remoting
{
	/// <summary>
	/// This is a class that transforms a simple (non-encrypted) stream into an
	/// encrypted one. <br/>
	/// It does: Create an assymetric private key (on server), sends the public
	/// part to the client and, then, the client creates a new symmetric key that 
	/// is known is sent to the server using the asmmetric key.<br/>
	/// After that, only the symmetric algorithm is used, as it is faster,
	/// but it is guaranteed that only the server and the client knows the key.<br/>
	/// This cryptography guarantees that no one "sniffing" the network would be
	/// able to interpret the messages, but does not guarantees that the requested
	/// host is really the host it should be. To that additional verification,
	/// you would probably need to deal with certificates and the SslStream.
	/// </summary>
	public class SecureStream:
		Stream
	{
		private static readonly byte[] fEmptyReadBuffer = new byte[0];
	
		private MemoryStream fWriteBuffer;

		/// <summary>
		/// Creates a new secure stream (stream that uses an assymetric key to
		/// initialize and then a symmetric key to continue it's work) over another
		/// stream, without any other parameters, so, running as client.
		/// </summary>
		public SecureStream(Stream baseStream):
			this(baseStream, new RSACryptoServiceProvider(), SymmetricAlgorithm.Create(), false)
		{
		}

		/// <summary>
		/// Creates a new secure stream (stream that uses an assymetric key to
		/// initialize and then a symmetric key to continue it's work) over another
		/// stream, specifying if running as client or server, but without changing
		/// the default symmetric or assymetric class/algorithm..
		/// </summary>
		public SecureStream(Stream baseStream, bool runAsServer):
			this(baseStream, new RSACryptoServiceProvider(), SymmetricAlgorithm.Create(), runAsServer)
		{
		}
		
		/// <summary>
		/// Creates a new secure stream (stream that uses an assymetric key to
		/// initialize and then a symmetric key to continue it's work) over another
		/// stream. <br/>
		/// Species the symmetricAlgorithm to use.
		/// </summary>
		public SecureStream(Stream baseStream, SymmetricAlgorithm symmetricAlgorithm):
			this(baseStream, new RSACryptoServiceProvider(), symmetricAlgorithm, false)
		{
		}
		
		/// <summary>
		/// Creates a new secure stream (stream that uses an assymetric key to
		/// initialize and then a symmetric key to continue it's work) over another
		/// stream. <br/>
		/// Species the symmetricAlgorithm to use and if it runs as a client or server.
		/// </summary>
		public SecureStream(Stream baseStream, SymmetricAlgorithm symmetricAlgorithm, bool runAsServer):
			this(baseStream, new RSACryptoServiceProvider(), symmetricAlgorithm, runAsServer)
		{
		}

		/// <summary>
		/// Creates a new secure stream (stream that uses an assymetric key to
		/// initialize and then a symmetric key to continue it's work) over another
		/// stream. <br/>
		/// Specifies the assymetric and the symmetric algorithm to use, and if it 
		/// must run as client or server.
		/// </summary>
		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 assymetric
				// key from the server.
				
				// symmetricAlgorithm
				var sizeBytes = new byte[4];
				p_ReadDirect(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_ReadDirect(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_ReadDirect(sizeBytes);
				int asymmetricKeyLength = BitConverter.ToInt32(sizeBytes, 0);
				byte[] bytes = new byte[asymmetricKeyLength];
				p_ReadDirect(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 = fEmptyReadBuffer;
			fWriteBuffer = new MemoryStream(32 * 1024);
 		}

		/// <summary>
		/// Releases the buffers, the basestream and the cryptographic classes.
		/// </summary>
		protected override void Dispose(bool disposing)
		{
			if (disposing)
			{
				var writeBuffer = fWriteBuffer;
				if (writeBuffer != null)
				{
					fWriteBuffer = null;
					writeBuffer.Dispose();
				}
				
				var encryptor = this.Encryptor;
				if (encryptor != null)
				{
					Encryptor = null;
					encryptor.Dispose();
				}
				
				var decryptor = this.Decryptor;
				if (decryptor != null)
				{
					Decryptor = null;
					decryptor.Dispose();
				}
				
				var symmetricAlgorithm = SymmetricAlgorithm;
				if (symmetricAlgorithm != null)
				{
					SymmetricAlgorithm = null;
					symmetricAlgorithm.Clear();
				}
				
				var baseStream = this.BaseStream;
				if (baseStream != null)
				{
					BaseStream = null;
					baseStream.Dispose();
				}
				
				fReadBuffer = null;
			}
		
			base.Dispose(disposing);
		}

		/// <summary>
		/// Gets the original stream that created this asymmetric crypto stream.
		/// </summary>
		public Stream BaseStream { get; private set; }
		
		/// <summary>
		/// Gets the symmetric algorithm being used.
		/// </summary>
		public SymmetricAlgorithm SymmetricAlgorithm { get; private set; }
		
		/// <summary>
		/// Gets the encryptor being used.
		/// </summary>
		public ICryptoTransform Decryptor { get; private set; }
		
		/// <summary>
		/// Gets the decryptor being used.
		/// </summary>
		public ICryptoTransform Encryptor { get; private set; }
		
		/// <summary>
		/// Always returns true.
		/// </summary>
		public override bool CanRead
		{
			get
			{
				return true;
			}
		}

		/// <summary>
		/// Always returns false.
		/// </summary>
		public override bool CanSeek
		{
			get
			{
				return false;
			}
		}

		/// <summary>
		/// Always returns true.
		/// </summary>
		public override bool CanWrite
		{
			get
			{
				return true;
			}
		}


		/// <summary>
		/// Throws a NotSupportedException.
		/// </summary>
		public override long Length
		{
			get
			{
				throw new NotSupportedException();
			}
		}

		/// <summary>
		/// Throws a NotSupportedException.
		/// </summary>
		public override long Position
		{
			get
			{
				throw new NotSupportedException();
			}
			set
			{
				throw new NotSupportedException();
			}
		}

		private readonly byte[] fSizeBytes = new byte[4];
		private int fReadPosition;
		private byte[] fReadBuffer;
		
		/// <summary>
		/// Reads and decryptographs the given number of bytes from the buffer.
		/// </summary>
		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;
		}

		/// <summary>
		/// Throws a NotSupportedException.
		/// </summary>
		public override long Seek(long offset, SeekOrigin origin)
		{
			throw new NotSupportedException();
		}

		/// <summary>
		/// Throws a NotSupportedException.
		/// </summary>
		public override void SetLength(long value)
		{
			throw new NotSupportedException();
		}

		/// <summary>
		/// Encrypts and writes the given bytes.
		/// </summary>
		public override void Write(byte[] buffer, int offset, int count)
		{
			fWriteBuffer.Write(buffer, offset, count);
		}

		/// <summary>
		/// Sends all the buffered data.
		/// </summary>
		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;
			}
		}
		
		private void p_ReadDirect(byte[] bytes)
		{
			p_FullReadDirect(bytes, bytes.Length);
		}
		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;
			}
		}
		private byte[] p_ReadWithLength(RSACryptoServiceProvider rsa)
		{
			byte[] size = new byte[4];
			p_ReadDirect(size);
		
			int count = BitConverter.ToInt32(size, 0);
			var bytes = new byte[count];
			p_ReadDirect(bytes);
			
			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);
		}
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

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


Written By
Software Developer (Senior) Microsoft
United States United States
I started to program computers when I was 11 years old, as a hobbyist, 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 their 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, 2015 & 2016
Microsoft MVP 2013-2014 (in October 2014 I started working at Microsoft, so I can't be a Microsoft MVP anymore).

Comments and Discussions