The Problem
In a previous post, Encrypting and Decrypting Text, I discussed how the shared key derived from a Diffie-Hellman Key Exchange can be used with a simple cipher (XOR) to encrypt and decrypt text. I wrote a demonstration program that sends encrypted messages between a client and server, with the server knowing how to respond to funny lines from the classic comedy, Airplane! I concluded with a mysterious statement, “There’s a weakness in our simple XOR cipher that’s exposed by sending a message not recognized as a line from Airplane!”
Let’s pick up there by sending a simple phrase to the server. How does it respond?
Enter a message to send to server: Testing. ... Client: Decrypted message is "Testing. Airplane!"
It simply echoes the message with the addition of two spaces and the phrase “Airplane!”
Let’s examine the encrypted messages sent from the client to the server and the server back to the client in more detail, looking for weaknesses in each. These weaknesses leak information that’s exploitable by a malicious party monitoring the communication. Similar to how an observant poker player can an exploit the “tells” of a weaker poker player- like, for example, if a weaker player sits up straighter in his chair when a helpful card is revealed, the stronger player can factor that observation into his decisions in the subsequent rounds of betting; similar to exploiting player “tells” in poker, a malicious party monitoring an encrypted communication channel can exploit weaknesses in the cipher used to encrypt messages. Without information from inside sources, the malicious party doesn’t know the cipher (the algorithm) used to protect the communication channel. However, if the cipher is very weak, he can determine it. And if he can determine the cipher and confirm it’s a weak algorithm, he can use that knowledge to reduce the problem-space of cracking the shared key used for encryption and decryption.
First, let’s refactor our program to enable dependency injection of a cipher. Let’s require the user to specify a cipher by name and to specify a key length.
Notice how I injected a Func delegate when creating the Client and Server objects. I did this because I can’t construct a Cipher object until I know the shared key derived from the Diffie-Hellman Key Exchange. But I can’t construct a Client or Server object to initiate the key exchange without a Cipher object. So I have what a layman calls a “chicken and egg scenario” and a programmer calls a “cyclical reference.” I prefer to resolve issues like this with a delegate to be called in the constructor of the dependent class. I prefer this technique because it ensures when an object’s constructor returns the object is fully constructed. An alternative technique of immediately setting particular properties or calling particular methods of a newly constructed object (once the object it depends on is constructed) is more prone to bugs because every programmer for every object instance has to remember to do this or the dependent object is left in an inconsistent, partially constructed state.
Now let’s encapsulate our XOR cipher in its own class.
The XOR cipher depends on a base class. We’ll make the base class disposable in anticipation of more complex ciphers requiring unmanaged resources.
Now back to examining the encrypted messages exchanged by client and server, looking for weaknesses in each. I’ll use the term “plaintext” to refer to an unencrypted message and “ciphertext” to refer to an encrypted message. Here are the weaknesses I find in the XOR cipher.
Weakness 1 : If two plaintexts begin with same characters, then the two resulting ciphertexts begin with same characters.
Enter a message to send to server: Testing. Client: Sending encrypted message "eUlY6TDDQQw=" Server: Received encrypted message "eUlY6TDDQQw=" Server: Decrypted message is "Testing." Server: Sending encrypted message "eUlY6TDDQQyjgBQALGWFzufJeg==" Client: Received encrypted message "eUlY6TDDQQyjgBQALGWFzufJeg==" Client: Decrypted message is "Testing. Airplane!"
The client and server plaintexts above both begin with the phrase “Testing.” Their corresponding ciphertexts both begin with the same phrase “eUlY6TDDQQ”. The entire “eUlY6TDDQQw=” phrase is not present in the ciphertext due to output padding required when encoding a byte array as a Base64 string.
Weakness 2 : For a given shared key, repeated encryption of the same plaintext produces the same ciphertext.
Enter the same message again.
Enter a message to send to server: Testing. Client: Sending encrypted message "eUlY6TDDQQw=" Server: Received encrypted message "eUlY6TDDQQw=" Server: Decrypted message is "Testing." Server: Sending encrypted message "eUlY6TDDQQyjgBQALGWFzufJeg==" Client: Received encrypted message "eUlY6TDDQQyjgBQALGWFzufJeg==" Client: Decrypted message is "Testing. Airplane!"
The client and server generate the same ciphertexts as they did in the first example. If you hit the Enter key without typing a message, the program will exit. If you run the program again and enter the same message, the client and server generate different ciphertexts than in earlier examples. This is because the program initiates a new Diffie-Hellman Key Exchange, with new random integers and therefore a new shared key, each time it’s run. So we could eliminate this weakness by initiating a Diffie-Hellman Key Exchange prior to sending every message. However, that would be inefficient because it introduces another round trip (network call from client to server back to client) and set of computations for every message. We don’t want to write inefficient, slow code- we’re professionals.
The Solution
How do we improve our cipher so it doesn’t leak information as illustrated above? We don’t. We heed this sage advice when writing cryptography code:
Don’t write your own solution. Cryptography is an extremely complex topic requiring mastery of mathematics and computer science. Use known good solutions from reputable parties.
We’ll use the Advanced Encryption Standard (AES) implementation included in Microsoft’s .NET Core runtime in the System.Security.Cryptography namespace. The System.Security.Cryptography namespace is a managed wrapper over Microsoft’s unmanaged Cryptography API : Next Generation (CNG) C++ library.
Let’s use AesCng to write a more secure cipher. We’ll be sure to dispose unmanaged resources used by the AesCng class by implementing the IDisposable interface required by our CipherBase class.
Let’s run our program, specifying the AES cipher with a 32 byte (256 bit) key.
PS C:\Users\Erik\...\netcoreapp2.2\publish> .\ErikTheCoder.Sandbox.EncryptDecrypt.exe aes 32 Client: G = 51007795224883012828854105573838180760149952181431603260693280577894643142453 N = 15221051005577275196665964051185154863251693071725010104567051995859756872419 M = 4419381971150842767402448168728340641308043654449190910089872233749629882283 Server: Shared Key = 6446010254530530752892219250957281791222465179283006629840666226260027675307. Server: G = 51007795224883012828854105573838180760149952181431603260693280577894643142453 N = 15221051005577275196665964051185154863251693071725010104567051995859756872419 M = 6448550482867508447125573323229162931705964202849078923954944776708273669756 Client: Shared Key = 6446010254530530752892219250957281791222465179283006629840666226260027675307. Enter a message to send to server: Testing. Client: Sending encrypted message "K5H72v/yntHSpMfJI+OU43n0Xqro/etKHFHZK9V5DME=" Server: Received encrypted message "K5H72v/yntHSpMfJI+OU43n0Xqro/etKHFHZK9V5DME=" Server: Decrypted message is "Testing." Server: Sending encrypted message "ZTB5CEfV9f6C/62paBeIoJedjmnwz8Yubz7/zpCI0e6/9DLMKq5o90rv8wjljAGK" Client: Received encrypted message "ZTB5CEfV9f6C/62paBeIoJedjmnwz8Yubz7/zpCI0e6/9DLMKq5o90rv8wjljAGK" Client: Decrypted message is "Testing. Airplane!"
Like the example above, the server does not recognize the client’s message as a line from Airplane! So it responds by echoing the message with the addition of two spaces and the phrase “Airplane!” The client and server plaintexts above both begin with the phrase “Testing.” Unlike the example above, their corresponding ciphertexts do not begin with the same phrase.
Enter the same message again.
Enter a message to send to server: Testing. Client: Sending encrypted message "cvCgHw3Qc1aHP7i8bYOsSEVGjT3phS+EN43HATsr3j4=" Server: Received encrypted message "cvCgHw3Qc1aHP7i8bYOsSEVGjT3phS+EN43HATsr3j4=" Server: Decrypted message is "Testing." Server: Sending encrypted message "G4zi89OYNiGhhVxaA3YiF/sKNVzd5wOB3iH3CLVMvXAFdogFKJFwt6jOzZetaY+e" Client: Received encrypted message "G4zi89OYNiGhhVxaA3YiF/sKNVzd5wOB3iH3CLVMvXAFdogFKJFwt6jOzZetaY+e" Client: Decrypted message is "Testing. Airplane!"
Unlike the example above, the client and server do not generate the same ciphertexts for the same plaintexts using the same shared key. This is a much stronger cipher than our naive implementation, the XOR cipher. In fact, despite AES being old as measured by typical technology timelines (it was announced by the National Institute of Standards and Technology (NIST) on 2001 Nov 26), it remains the one and only publicly available cipher approved by the National Security Agency (NSA) to protect Top Secret information.
To learn how the AES cipher was designed, read the book written by the creators of AES. I haven’t. Honestly, the math looks too intimidating.
You may review the full source code in the Encrypt Decrypt folder of my Sandbox project in GitHub.