Step 0: Little/Big Endian
If you’ve implemented the rest of AES encryption, this is literally the hardest part of the challenge.
You have a number represented by one byte. Which way do you read the bits? Most significant (leftmost bit) to least significant (rightmost bit).
Now say you have a number represented by two bytes. You know which way to read the bits, but which way do you read the bytes?
\x0F\x01? Both of these numbers can represent either 31, if the left one (
\x01\x0F) is read as big-endian and the right one (
\x0F\x01) is read as little-endian. More from wikipedia.
The format specified for the Stream Cipher is:
format=64 bit unsigned little endian nonce, 64 bit little endian block count (byte count / 16)
Unsigned means that none of the bytes in the nonce are used to specify positive or negative. All of them represent numbers.
Little endian means that the least significant or smallest values come first. That means that if we’re representing the value 15 for block count, the block looks like this:
\x0F\x00\x00\x00\x00\x00\x00\x00 instead of this:
I wrote a function to “little-endianize” a number into its hex representation.
The perceptive reader will notice that I restrict super large numbers but quip, “Wait tho, that’s smaller than a quadword!” You are right, dear reader. Technically, a quadword can hold a number up to
The counter that we encrypt, which increments by 1 for every 16 bytes, is really, really unlikely to ever get that large (if it does, we throw an error and stop encrypting). We’d have to be encrypting about 200 petabytes (PB) 1 PB = 1015 bytes = 1,000 terabytes, and we’d be using the same nonce to do it. No no.
Step 1: Let’s Stream That (Block) Cipher
Here’s info on CTR mode (wikipedia). Notable points:
- Encryption and decryption both use the AES encryption process on the keystream bytes.
- All we do to encrypt or decrypt the plaintext/ciphertext is XOR with this encrypted keystream.
- CTR converts our block cipher into a stream cipher.
- No plaintext padding is required.
- Just keep XORing with fresh keystream until you run out of plaintext or ciphertext.
I create a simple object that will give me 16 bytes of encrypted keystream each time I call
Then, here’s all that I have to do to encrypt (or decrypt – the process is identical!):
Chunk the plaintext up into 16 byte blocks and convert to hexadecimal. Get a block of AES-encrypted keystream. XOR each plaintext byte with its corresponding keystream byte. Done! Literally that simple.