Every systems engineer needs tools to serve his daily needs at work. These tools do not necessarily come from vendors, they can be done by hand, specially when there is a need for custom ones.
One of the tools I needed lately is a TCP client/server application, with the server part running as a Windows service and securely accepting connections from the client part, while serving multiple purposes.
Well, the subject itself is not new. How many TCP client/server examples are there around on the web? Countless! It was really easy given that I already have the
TcpClient in hand. Once I grabbed the idea, I was able to come out with some sort of an Echo server in less than an hour (experienced programmers could have done that in minutes ), but that would never be used. Why? Not because it lacks functionality, but just because it lacks SECURITY. Yes, Telnet like programs are no option, specially when you will run them on a production server.
So, I went for the obviously obvious option: cryptography with the .NET
CryptoStream. Or, did I?
CryptoStream is easy to be used once you figure out how to do the
ICryptoTransform operation that will be used with the
CryptoStream, that is the cryptography Symmetric Key in either of its modes; Read or Write. But, there is a chronic problem with the
CryptoStream, which I discovered with pain as a cryptography newbie: the
CryptoStream will ultimately be unable to distinguish when a TCP message or block of data has ended (or that's at least what everybody is saying around the web and I'll get to that later ). Yes, as an object in an OOP-based framework, it inherits properties and methods of other "normal" streams, like
.EndOfStream. Dare to use any of these, and you will get a nasty and painful exception telling you that the
CryptoStream does not support these methods/properties!
This is because the
CryptoStream is performing its work at a higher level than that of the Socket. It builds on top of the
NetworkStream that is created on
TcpClient connection. You can Google this exact phrase "
cryptostream does not support seek" and browse the few first results. Most are suggesting to do byte count before and after encryption/decryption, by recording the length of data being sent into the few first bytes before data itself.
That actually works, but why does the
.Read operation fail to complete normally unless the sending party closes the connection on its side? When you translate the length-bytes and then try to read the now pre-determined byte-count
CryptoStream.Read seems to be working like forever, before it eventually throws a timeout exception. One example used that technique of length-bytes and I wasted my time reading its code while it was "inventing" an object of its own for the TCP message just to find that it eventually closes the connection on the sending side so as to force it be read on the receiving side. Hilarious. Yes, closing the stream on the sending side automatically signals the receiving side to read whatever bytes are left in queue, thus fixes the
CryptoStream.Read problem. This only creates one-way cryptographic communication; definitely not the way networking was designed to work. One of the people complaining about the same problem noticed that the data actually can be sent without closing the connection, only if the
.Write function is committed twice. The reply to that observation - as pessimistic as it was - is what evoked me do what I've been refraining to do for long: count the bytes as they are sent-and-read in real time. Guess what?
Lucky Number 32 (not sleven )
Just as I was testing my proto-type code, I used a "password" of 777 bytes (oops, security breach ), some flag bytes, and the rest was randomly generated bytes to try push my data onto the wire. I was
.Write'ing these bytes each at a time, and as I was testing each time, I found the reading of the random bytes sent stopping at index number 718, which would make it byte number 719! When I got the same result each time, I counted the total bytes, it was 1504. I rushed into many conclusions, sorryfully, but it came out to be much simpler. As I'm using the Rijndael algorithm, and specifying the
.BlockSize of the Rijndael provider that the
ICrytoTransform uses as the maximum allowable 256 bits = 32 bytes, the
CryptoStream was reading the data in
.BlockSize, all the time.
Dim x As New RijndaelManaged x.BlockSize = 256 x.KeySize = 256 x.Mode = CipherMode.CBC x.Padding = PaddingMode.ISO10126 Dim key() As Byte = New Rfc2898DeriveBytes("keyPass", ASCII.GetBytes_
("saltsaltsalt".ToCharArray)).GetBytes(32) Dim iv() As Byte = New Rfc2898DeriveBytes("ivCryptic", ASCII.GetBytes_
The trick here was that you must *at least* append an extra "dummy block" in
.BlockSize for the
ICryptoTransform to sense that there is another block. If I went for 128 bits BlockSize, the extra data required to push
.Read operation would have been 16 bytes, instead.
Dim x As New RijndaelManaged x.BlockSize = 128 x.KeySize = 128 x.Mode = CipherMode.CBC x.Padding = PaddingMode.ISO10126 Dim key() As Byte = New Rfc2898DeriveBytes("keyPass", ASCII.GetBytes
("saltsaltsalt".ToCharArray)).GetBytes(16) Dim iv() As Byte = New Rfc2898DeriveBytes("ivCryptic", ASCII.GetBytes
Unfortunately, there is not much reference for the
CryptoStream, and all I had was web resources, most of which have NO EXACT EXPLANATION to the way
CryptoStream.Read function is behaving.
So, finally I was able to do 2-Way
CryptoStream communication, knowing exactly when my sent data will actually be read on the receiving side.. Finally
The code I made for this article is split into 2 applications: dataSender.exe and dataReceiver.exe. Using those demo apps is very easy, you start both apps, define in both apps' textboxes how many bytes of data will be written/read, then finally start sending the bytes. To guarantee the delivery of a non-32-multiple chunk of data, just add extra 64 bytes to the count, and you are done No-nag! To help you use the same code over the network, I've included a random byte generator app, that will produce lines of text into a file to the same directory it runs in, each line will be in the form (Chr(i) + Chr(i) + ...) for the count of 32 concatenated Chars. You take those and paste them in the appropriate commented line within the code, and do necessary changes.
The code is also heavily commented. Just beware that comments always come after code, not the regular opposite. I extremely hate comments that show before code; it is anti-logic. Your mind parses a code line then demands explanation, not the opposite!! You will also find the code eye-friendly: no blank lines that would force you to re-parse whole blocks just to re-remember what you were reading!!!! The only pitfall: this technique requires highlighting. So, don't use Notepad to read it.
You can download the latest and updated article code at http://admincraft.net.
Points of Interest
To guarantee delivery of your bytes over .NET
CryptoStream with Rijndael algorithm, append extra dummy block of bytes in
.BlockSize at the end. If the length of your data will produce float if divided by
.BlockSize, then count the length of the last
BlockSize-fragment of your data and append extra bytes until the count = (BlockSize x 2)
crypto.Write(ASCII.GetBytes("done!".ToCharArray), 0, 5) Dim y As New Random Array.Resize(rwBuffer, 59) y.NextBytes(rwBuffer) crypto.Write(rwBuffer, 0, rwBuffer.Length)
I also want to add that some people insist that the 2 functions
.FlushFinalBlock have a "healing" effect on the
.Read function on the receiving side. This is absolutely non-sense, and if you use my demo apps you will surely know that
CryptoStream.Write does not and will never need any flushing at all. All buffer data handed over to the
.Write function gets written IMMEDIATELY. Plus, if - and only if -
.FlushFinalBlock was going to have any effect on the
.Read function on the receiving side - which I extremely doubt, it is already useless. Why? Simply because it can only be called ONCE - ONLY ONE TIME - during
CryptoStream lifetime, after which you MUST close the stream. Otherwise? You will get an ugly exception that will force you to close it. So, still one-way communication.
Don't forget to vote if you like it.