How to create a bidirectional encrypted stream on top of an underlying .NET Stream?

Viewed 708

I have a Stream which transmits and receives data over an insecure channel. I have a pre-shared secret that both endpoints of the channel already have (say, a passphrase).

I'd like to construct a new stream using the secret and the original insecure stream. The two issues I came across are:

  • CryptoStream is one-way: read-only or write-only. I can create two streams (a reading and a writing stream) on top of my original Stream, but this is unacceptable. Do I have to write a wrapper stream to get what I want? (namely, a single read/write stream)

  • CryptoStream is said to work in blocks, and may not write anything to the underlying stream until the block is complete. Ideally I'd like to write any amount of data and have it be sent to the underlying stream (encrypted) immediately.

Is there an easy way of achieving this? I know of SslStream, but it's tailored towards private/public keys and certificates, rather than pre-shared secrets.

2 Answers

I doubt you will come back and accept an answers two years later, however I just did what you are asking about, and I think it is a fairly common problem so I am posting this for the benefit of others that may come across this question.

I incorporated GregS' information into my implementation. For your specific purpose, you would make the Initialize method your constructor, strip out the net & diffie-hellman code, and assign your pre-shared key to the Aes object (instead of the generated key).

Please note that I am using AES256 despite that it has been compromised to being of similar strength to AES128 (if your keys are related because of bad implementation, which mine are not to my knowledge). If you do not trust NIST to know if the NSA is messing with their specs, then do NOT use AES.

Also, this is a starting point! I am working through the commonly encountered problem of sending encrypted data over a NetworkStream in .NET!

In a hundred twelve lines or less, without further ado:

using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;

namespace FullDuplexCrypto
{
    class CryptoNetworkStream : Stream
    {
        public CryptoNetworkStream(IPAddress address, int port)
        {
            Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
            socket.Connect(address, port);
            //socket.NoDelay = true;
            Initialize(new NetworkStream(socket, true));
        }

        public CryptoNetworkStream(Socket socket)
        {
            Initialize(new NetworkStream(socket, true));
        }

        private void Initialize(Stream stream)
        {
            underlyer = stream;

            using(ECDiffieHellmanCng dh = new ECDiffieHellmanCng())
            {
                dh.KeyDerivationFunction = ECDiffieHellmanKeyDerivationFunction.Hash;
                dh.HashAlgorithm = CngAlgorithm.Sha256;
                byte[] buffer = dh.PublicKey.ToByteArray();
                underlyer.Write(buffer, 0, buffer.Length);
                underlyer.Read(buffer, 0, buffer.Length);

                using(Aes aes = Aes.Create())
                {
                    aes.KeySize = 256;
                    aes.Key = dh.DeriveKeyMaterial(CngKey.Import(buffer, CngKeyBlobFormat.EccPublicBlob));
                    aes.FeedbackSize = 8;
                    aes.Mode = CipherMode.CFB;

                    underlyer.Write(aes.IV, 0, aes.IV.Length);
                    encrypter = new CryptoStream(underlyer, aes.CreateEncryptor(), CryptoStreamMode.Write);

                    underlyer.Read(aes.IV, 0, aes.IV.Length);
                    decrypter = new CryptoStream(underlyer, aes.CreateDecryptor(), CryptoStreamMode.Read);
                }
            }
        }

        private Stream underlyer;
        private Stream encrypter;
        private Stream decrypter;

        public override bool CanRead { get { return decrypter.CanRead; } }
        public override bool CanWrite { get { return encrypter.CanWrite; } }
        public override bool CanSeek { get { return underlyer.CanSeek; } }
        public override long Length { get { return underlyer.Length; } }
        public override long Position { get { return underlyer.Position; } set { underlyer.Position = value; } }

        public override void Flush()
        {
            encrypter.Flush();
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            return decrypter.Read(buffer, offset, count);
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            encrypter.Write(buffer, offset, count);
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            return underlyer.Seek(offset, origin);
        }

        public override void SetLength(long value)
        {
            underlyer.SetLength(value);
        }

        private bool isDisposed = false;

        protected override void Dispose(bool isDisposing)
        {
            if(!isDisposed)
            {
                if(isDisposing)
                {
                    // Release managed resources.
                    encrypter.Dispose();
                    decrypter.Dispose();
                    underlyer.Dispose();

                }
                // Release unmanaged resources.

                isDisposed = true;
            }
            base.Dispose(isDisposing);
        }
    }
}
Related