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.
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);
}
}
}
You'll need to correctly transmit and read IV's, but you can use a block cipher, for example AES, in CFB mode with a feedback size of 8 bits to eliminate the "blocky" limitation. I think you'll have to write your own crypto stream to get the bidirectional behavior you want.
On the encrypt side of the stream generate a random IV and transmit it first. On the decrypt side read the IV bytes first from the stream, then initialize the cipher transform with it and then pass the remaining bytes read from the stream through the crypto transform.
If you are willing to use the Bouncycastle C# library you can get the benefits of all the security engineering and analysis that went into TLS/SSL using pre-shared keys by using the SrpTlsClient
class. This class implements the SRP ciphersuites in TLS.
EDIT:
Nevermind about SRP TLS, the Bouncycastle library only has the client side of the protocol. Too bad.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With