A CLI tool for encryption/decryption with 128-bit and 256-bit AES.
Tested on NodeJS version 5.12.0
After cloning this repository or unzipping it, run
npm install
This will install the necessary dependencies for AES.js to run
To use the program, run
node aes.js [options] [arguments]
or
npm start -- [options] [arguments]
Flag | Description |
---|---|
-h, --help | Prints out a help message to the console |
-v, --version | Prints out the version of AES.js |
Argument | Type | Description |
---|---|---|
--keysize | number | Size of the key for AES, either 128 or 256 bits |
--keyfile | filename | Filename containing the key of the specified size |
--inputfile | filename | Filename of the file containing the input text |
--outputfile | filename | Filename where the result will be written. Defaults to output.txt |
--mode | string | mode in which to run AES. Either encrypt or decrypt |
The aes.js
file is responsible for validating input, user interface, and loading up the initial files. The AES algorithm itself is implemented in src/implementation.js
.
There are two main methods in this file, encrypt()
and decrypt()
. These are mainly responsible for two things:
- Creating the key schedule with
keyExpansion()
- Breaking up the input into chunks of 16 bytes and calling
cipher()
andinverseCipher()
respectively, and then writing the output to the given filename.
cipher()
and inverseCipher()
are implemented very closely to the pseudo-code here.
This function is responsible for generating the round keys. A high level view of what the function accomplishes is outlined here, and my implementation closely follows the pseudo-code in this document.
The function first allocates 4 * <number of rounds> + 1 space. Then it copies the original key into the first 4 or 8 words, depending on the key size (128 or 256 bit).
Each subsequent key is generated by looking at the key before it, and performing these steps:
- Rotating the bytes (like so)
- Substituting each byte with its corresponding value in a look-up table (the same table as used in
subBytes()
) - If the key being generated is the kth key where k is a multiple of the key length (4 or 8 for 128 or 256 bit keys respectively) then a round constant is added using the exclusive XOR operation. This is performed using a look-up table
- If the key is 256 bit and it's the kth key where k is a multiple of 4, the key is substituted using the look-up table again
- Finally, this new key is added using exclusive XOR to the previous key to output the final key
Once the correct number of keys have been generated, the entire list of them make up the key schedule
The cipher()
function begins by copying the given input of 16 bytes into a 4 x 4 array. It then calls addRoundKey()
for the first round.
The algorithm then goes into a loop for either 10 or 14 rounds for 128-bit or 256-bit keys respectively. The loop calls these functions in order every iteration:
subBytes()
shiftRows()
mixColumns()
addRoundKey()
Finally, for the last round, cipher()
then calls subBytes()
, shiftRows()
, and addRoundKey()
in that order. The function ends after copying the 4 x 4 array to a buffer for the output.
I'll explain each function in turn:
Simply adds the round's key within the key schedule to the state using the XOR operation.
Performs a substitution for each byte in the state with a substitution table defined within src/implemenation.js
.
Shifts each row in the state array 0, 1, 2, or 3 indices according to the index of each row in the state.
This is the most complicated step. It performs matrix multiplication on the columns of the state. For the mixColumns()
function, the matrix it uses is defined here. This function doesn't use the proper multiplication as defined by finite field arithmetic, but instead uses an optimization for multiplying by 2 and 3 by using bit shifts and overflow checks. The implementation for this was inspired from here.
The inverseCipher()
function is very similar to the cipher()
function. It mainly does all the same steps except in reverse. It begins by calling addRoundKey()
for the key within the last round (10 or 14 for 128-bit or 256-bit keys respectively).
Similar to cipher()
, the function then goes into a loop for the specified number of rounds. For each iteration the loop calls:
invShiftRows()
invSubBytes()
addRoundKey()
- Note that the algorithm goes in descending order of the key scheduleinvMixColumns()
For the last round, the function calls invShiftRows()
, invSubBytes()
, and addRoundKey()
before copying the state to an output buffer.
addRoundKey()
is exactly the same as in cipher()
, except the order that the keys are added to the state is reversed.
Performs the exact opposite functionality as shiftRows()
. Where as shiftRows()
shifts left, invShiftRows()
shifts right.
Substitutes each byte in the state using a substitution table like subBytes()
, except this substitution table is the inverse of the one used in subBytes()
Performs another matrix mutliplication with the state's columns similar to mixColumns()
. This matrix is different from mixColumns()
's matrix, as defined here. To perform the multiplication over the finite field, multiply
was implemented as the algorithm defined here. An easy optimization is to implement look-up tables for mutliplication by 9, 13, and 14 but the algorithm within multiply
works just as well.