|
AES-DH Implementation
|
This repository contains a working implementation of both Diffie-Hellman key exchange, and AES encryption, with an emphasis on heavy documentation in order to better understand both algorithms, and how they may be used together in a fully functional, network based application. It serves as an educational aid for those wanting to better understand these two algorithms, how they work underneath the hood, and how they might be implemented in code.
There’s three routes to approach this repository:
main/main_pc uses AES and DH to allow two instances of the program to securely connect and exchange messages over the network by utilizing sockets!aes/aes_pc is a command-line utility that allows you to encrypt/decrypt strings and files using the implementation of AES!aes.cpp contains the source code for the aes application, and shows interfacing with the AES implementation and reading in files and user input.aes.h is the fully functional implementation of the AES algorithm, including ECB, CTR, and GCM modes, and supporting key sizes of 128, 192, and 256 bits. Its size may be daunting, so make sure to check out the Codebase Walkthrough to go over the various parts of each file, and functions of interest!exchange.h is our Diffie-Hellman implementation.hmac.h uses the OpenSSL implementation of HMAC-SHA256!network.h holds all the functionality that the program uses to talk over the network, including sending and receiving arbitrary data.main.cpp contains the code for the main application, prime.h contains the functions related to prime-number generation and other mathematical functions, and util.h contains helper functions for the main application, including the functionality for sending encrypted messages between the peers.>[!note]
While the primary motivation of this repository is a well-documented AES and DH implementation, the entire code-base has received equally thorough documentation!
docs/html. Simply open the index.html with your favorite web browser, and navigate between the various namespaces and functions! This repository is written in C++. If you’re not familiar with the language, or Programming as a whole, the syntax may seem nebulous, but it’s been written to try and make the logic easy to follow. That said, here are some general points:
x + yx - yx * yx / y Note that because all values in this repository are integers—as opposed to floating point with decimals—division rounds down to the nearest whole number.x % y, This returns the remainder of x / yx & yx | yx ^ y!x Fixed width types: The uint_t class of numbers are fixed-width, defined by the number of bits:
uint8_t: Is 8 bits, or a byte.uint32_t: Is 32 bits, 4 bytes, or a word. Array Indexing: AES uses a block of 16 bytes, typically organized as a 4x4 grid. While most implementations forgo this to simply have 16 bytes in a row, this implementation uses the 4x4 scheme to make it easier to follow. Indexing is done with the [] operator, so array[x][y] would return the value located at column $x$, row $y$.
C++ has two main types of loops, for loops, and while loops:
for (X; Y; Z) will initialize the statement $X$, and continue to loop until $Y$ is no longer satisfied, calling statement $Z$ on each iteration. This is most commonly used in reference to indexing the block:
Where size_t row = 0 and size_t col = 0 initialize the row and col index as 0, row < 4 and col < 4 ensure that the loop runs until these values iterate through the entire array, and ++row and ++col increment these values on each iteration.
You’ll also see the C++ versions:
This iterates through each element of arrays, setting the current element to the value x.
while (X) will repeatedly run until statement $X$ becomes false. Loops (both for and while) can jump back to the beginning using continue, or exit immediately with break
The standard library includes all functions and classes contained in the std namespace. This repository uses it extensively, such as the fixed-size collection of objects in the std::array, the variable sized collection of objects in the std::vector, and utilities like std::rotl.
>[!tip] >The C++ Reference: https://en.cppreference.com/w/cpp is a great source for documentation related to everything in the standard library!
The AES implementation is available within the aes.h; as with every file in the repository, the functionality is partitioned into a namespace, namely the aes namespace. This is why other programs will call AES functionality like aes::gcm::Enc, where Enc is a function within the gcm namespace, which is within the aes namespace.
AES operates by taking a message of arbitrary size, and breaking them down into 16 byte Blocks. These Blocks are collected into a single state. Then, a series of algorithms and transformations are applied to each Block, four in total: AddRoundKey, SubBytes, ShiftRows, and MixColumns. These four steps are then repeatedly applied depending on the size of the key, with $10$ rounds for a 128 bit key, $12$ for a 192 bit key, and $14$ for a 256 bit key. This key is transformed into a Key Schedule where the key is turned into a unique set of words for each round, and is then applied to the Block during the AddRoundKey step.
Once we have run through all the steps, we have the ciphertext. For ECB and CTR modes, we generate an HMAC against the ciphertext and key, producing a string that will change if the key or ciphertext is modified in transit, ensuring integrity. Because the key is used for HMAC generation, an attacker cannot modify the ciphertext and create a corresponding HMAC! GCM creates its own integrity check that is stored as a Block at the end of the ciphertext.
The aes namespace includes the following members:
gf namespace contains functions related to Galois Field computation, used frequently in both AES and AES-GCM.key namespace contains functions related to the KeyExpansion algorithm in AES, which is used by the AddRoundKey step in AES.state_array class is the basic unit of AES, containing the Block. Every step of AES applies to the Block, and is thus implemented within this class as member functions. Importantly there is:state_array::AddRoundKey: Add the round key.state_array::SubBytes: Perform the substitution step.state_array::InvSubBytes: Revert SubBytesstate_array::ShiftRows: Transpose the rows by a cyclical shift.state_array::InvShiftRows: Revert ShiftRowsstate_array::MixColumns: Transform each column by a matrix.state_array::InvMixColumns: Revert MixColumnsstate_array is transformed in place, which means it is initially filled with plaintext, and each of these above steps are applied, changing the internal values, before the final ciphertext is unraveled out as a string.state is little more than a collection of individual state_arrays. Because Blocks are fixed at 16 bytes, the state contains an entire message broken into these 16 byte segments. It is responsible for generating the Key Schedule (See state::Schedule), but besides that does nothing more than apply all of the steps mentioned in the state_array to each Block.Cipher and InvCipher functions are a verbatim translation of the Encryption and Decryption outlined in the Reference paper. Taking a string, a key, and a round number, it encrypts the message with AES, returning the resulting cipher text. Using these functions by themselves is using AES in ECB mode.Ctr function is a implementation of the AES-CTR mode, where rather than passing the plaintext through AES directly, we instead generate a nonce value, pass that through AES to get a Pad, and then perform a One-Time Pad form of encryption where the plaintext is XOR’d against this Pad, to which a unique pad is generated for each Block in the plaintext by the incrementing nonce. Because encryption is done via XOR, Ctr both encrypts and decrypts a message.gcm namespace includes all the functions related to the AES-GCM mode. These functions were implemented in reference to: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdfincrement function increments the Nonce value; unlike AES-CTR, the nonce has a specific algorithm for incrementing it to the next value.mult function multiplies two Blocks together.GHASH function is the main aspect of AES-GCM, and generates an authenticated hash of the state, returning it in a Block that can be appended onto the state.GCTR function is almost identical to aes::Ctr, but rather than taking a numerical nonce, it uses a Block nonce/IV called ICB. It also uses aes::gcm::increment To step the ICB to new values, and rather than returning a string message, returns the block state instead.Enc function takes a message, a key, a round count, and nonce, and encrypts the message with AES-GCM.Dec function takes a ciphertext, a key, a round count, and a nonce, and will decrypt the message with AES-GCM if and only if the GHASH matches, and will refuse to decrypt if there have been changes to the key or any blocks.state_array, and other classes. They aren’t important to the fundamental understanding of AES, so feel free to ignore them if they aren’t mentioned here.Diffie-Hellman is particularly clever because it relies on the associativity of multiplication. The basic steps are this:
Diffie-Hellman relies on a the idea that, with large enough values $p,a,b$, trying to manually determine values $a,b$ from only knowing $g^a \mod p, g^b \mod p$ is infeasible. For more information: see https://en.wikipedia.org/wiki/Discrete_logarithm
The Diffie-Hellman Key Exchange implementation is located in exchange.h, within the exchange namespace. Unlike aes, there isn’t near as many members:
compute_intermediary function takes the public values $p$ and $g$, alongside a private key $k$ and computes the intermediary value that is sent to the other peer.exchange_keys function generates the private and public keys, and establishes a shared key between another computer by communicating over a socket.>[!note] >While the primary Diffie-Hellman algorithm is implemented as exchange_keys, This implementation uses 64 bit keys, which is unacceptable for use within AES. Therefore, the main program actually exchanges 4 keys, totaling 256 bits. Take a look at util::construct_shared_key for the code!
Interesting in seeing AES and DH in action? This project compiles two applications that you can use to see the implementations working: aes and main
aesaes is the simpler of the two, simply providing a command line utility interfacing with our AES implementation. Simply run ./aes from the project directory in your shell of choices!
aes only supports long-style command line flags, such as --flag1=value1 --flag2=value2. Values must be separated by an =, with no white space betweenThe only required argument is --mode, which specifies which mode of AES to use. This is represented as three values separated by dashes, in the format ENC-256-GCM where:
ENC/DEC is the first string, specifying whether this is an encryption or decryption operation.128/192/256 is the second string, specifying the key size.ECB/CTR/GCM is the final string, specifying the specific AES method to use../aes --help will list all the available options, but here are the important ones:
--infile specifies the source of data. If not provided, the user will be prompted to supply a message.--outfile specifies where the output data should be send. If not provided, the output will be output to the console.--keyfile specifies a file used for the key. If not provided, the user will be prompted to supply a key.Some examples:
>[!tip] >ECB/CTR modes do not have any authenticity checks when running from the aes program, as no HMAC is generated. However, since GCM does have integrity, try encrypting data to a file, then change part of that file before asking to decrypt. GCM will immediately report the modification was detected and refuse to decrypt! You can also try this with keys: ECB/CTR will return garbage data if an incorrect key is provided, but GCM will refuse to decrypt altogether!
mainmain provides an interface for two peers to communicate over a socket, exchange a shared key using Diffie-Hellman, and then use those shared keys to securely send messages with AES encryption. No command line arguments are required, simply run from your shell!
When first starting, main performs a sanity check to ensure that the AES implementation is working correctly. You should see three sentences, each with an AES mode at the end. If these sentences look incorrect (incoherent text, characters that cannot be rendered), then there’s something wrong with the program. If you’re using the pre-compiled version (main_pre), try compiling it yourself, vice versa if you’re using a self-compiled version. If everything looks alright, press enter, if not: Ctrl+C to stop the program.
Once past that, you will be at the main interface of the program:
Status: Specifies the state of program. If you are connected to another peer, it will be CONNECTED, otherwise it will be IDLE. This determines what options you have available.0. Request New Connection Will allow you to connect to another peer who has selected Listen for a New Connection. main uses a numerical list for users to provide input. To select this option, type 0, and then ENTER. 1. Listen for New Connection listens for peers to connect to.2. Quit will close the application.The networking model of main is a two-way communication of a shared socket. On an initial connection, however, one peer will need to be be the server, selecting Listen for a New Connection, and the other will be the client, selected Request New Connection. When listening, you provide a port to listen on, and the program will wait 30 seconds for another peer to connect. When requesting, you will provide that same port, and the IP Address of the second computer.
main does not perform DNS lookup, so you need to provide the raw IP address of the listening peer. If you aren’t sure what that is, use ping! Make sure your firewall allows communication to the port you’ve picked!127.0.0.1), or just type local!main reports Failed to connect! try using a high port like 2000, and incrementing by one until you find a free port to bind to!For an example, we’ll launch two instances of main on a single computer, and will use port 5000 to listen to. We should see the program report Listening... where we have 30 seconds to connect. Don’t worry if you don’t connect in time, you can just Listen for New Connection again, and it’ll even remember the port!
With the second program, we’ll provide port 5000, and then local because these are both running on the same computer. You should immediately see the program report Exchaging Keys..., and then Complete! The two peers just used Diffie-Hellman to exchange a shared key! If you see an error, you’ll be brought back to the home page, and you can try to connect again.
Now, you should be brought back to the home page, but the status should report CONNECTED. Now, you have some new options:
Shared Key provides you a truncated version of the shared key that was negotiated. Sometimes, a blip in the network communication can lead to values being dropped or changed in transit. If this happens during the key exchange, you won’t be able to communicate. Therefore, look at the value, and ensure that they are identical between both peers.0. Listen for Request: The networking between peers is simplistic, which means that communication is done in a similar way to the initial handshake. One peer will Listen for Requests, which will put the program in an idle state for 30 seconds as it awaits a request from the other peer. In this time, the other peer will use one of the other options.1. Send an Encrypted Message: Use AES to send an encrypted message using the shared key to the other peer. More details on this below.2. Re-Exchange Keys: If the shared keys do not match, request that new shared keys be generated and shared.3. Terminate Connection: Terminate your connection with the peer.So, if our shared keys don’t match, and we need to re-exchange new values, peer 1 will type 0 to Listen for Request, and peer 2 will type 2 to Re-Exchange Keys. Peer 1 will be prompted to accept the exchange, and if they accept a new key-exchange will be performed.
To send an encrypted message, Peer 1 will type 0 to Listen, and peer 2 will type 1 to send an encrypted message. As with the initial connection, Listening has a 30 second timeout, but if you timeout the first peer setting up the message, you can just listen again!
For peer 2, you will firstly need to provide a message. This can be of any size, but ends with a newline, or the enter key. Next, you’ll have to provide the key size, which can either be 128/192/256. Finally, select which AES mode to use, either ECB/CTR/GCM. Peer 2 will then wait for Peer 1, and once the peer has accepted, will send the ciphertext over. Peer 1 can then decrypt it with the shared key, and the program will print out the Message. Have fun!
Quit option or with CTRL+C, and relaunch the program to try again!