Cryptopals Challenge 1.5: Implement Repeating-Key XOR

This challenge asks us to implement repeating-key XOR encryption. I massively overthought the solution at first with switch cases, flags, and pointer arrays, only to realize it’s literally one line of modulo arithmetic.

Challenge

After three challenges of breaking XOR ciphers, we are now asked to implement one ourselves. This time with a multi-character key that repeats.

Description

Here is what the challenge asks us to do:

Here is the opening stanza of an important work of the English language:

Burning 'em, if you ain't quick and nimble
I go crazy when I hear a cymbal

Encrypt it, under the key “ICE”, using repeating-key XOR.

In repeating-key XOR, you’ll sequentially apply each byte of the key; the first byte of plaintext will be XOR’d against I, the next C, the next E, then I again for the 4th byte, and so on.

It should come out to:

0b3637272a2b2e63622c2e69692a23693a2a3c6324202d623d63343c2a26226324272765272
a282b2f20430a652e2c652a3124333a653e2b2027630c692b20283165286326302e27282f

Encrypt a bunch of stuff using your repeating-key XOR function. Encrypt your mail. Encrypt your password file. Your .sig file. Get a feel for it. I promise, we aren’t wasting your time with this.

So the idea is straightforward: instead of XOR’ing every byte with the same key (like in Challenge 3), we cycle through the characters of the key. Byte 0 gets XOR’d with ’I’, byte 1 with ’C’, byte 2 with ’E’, byte 3 with ’I’ again, and so on.

Solution

My Initial Plan (Way Too Complicated)

Before writing any code, I sat down and thought about how to solve this. Here was my original plan:

  1. Take the input (hardcoded or from a file)
  2. Create an array of pointers with three elements (“I”, “C”, “E”)
  3. Turn these into raw bytes, if there is no decoder for ASCII strings, turn the text into hex first and then decode that
  4. Write a while loop that checks if each byte of the plaintext has been read, and inside the loop:
    • Have an iterator that counts to 3, and when it hits 3, reset it to 0
    • Have a switch/case statement that XOR’s with the correct key byte based on the iterator value
    • Store each result in a result array with a separate index
  5. After the loop, print the result array

I even wrote down “potential considerations” like counting bytes to prevent array overflow and using a boolean flag for when we reach the last byte.

Looking back at this, I was massively overthinking it. Let me explain the three things I got wrong in my planning.

The Fixes

After I started actually writing the code, I realized:

Fix 1: Strings are already raw bytes

When I take the key “ICE” as a std::string, it’s already stored as raw bytes in memory. The character ’I’ is byte 73, ’C’ is byte 67, ’E’ is byte 69. There is no need for hex encoding and decoding the key — I was overcomplicating it because of the hex decoding we did in previous challenges. Those challenges had hex-encoded input, but our key here is just a plain string.

Fix 2: Modulo replaces the switch case

I don’t need an iterator that counts to 3 with a reset, and I definitely don’t need a switch/case with three nearly identical blocks. The expression i % key.size() does all of that in one shot. When i is 0, 0 % 3 = 0 (gives ’I’). When i is 1, 1 % 3 = 1 (gives ’C’). When i is 2, 2 % 3 = 2 (gives ’E’). When i is 3, 3 % 3 = 0 (back to ’I’). This also means the code works for any key length, not just 3.

Fix 3: No flags or overflow protection needed

I was worried about array overflow for the result and thought I needed a flag for the last byte. But std::string in C++ grows automatically when you append to it with +, so none of that is necessary.

The Actual Code

After all those realizations, the final code is almost embarrassingly simple:

#include <cryptopp/hex.h>
#include <cryptopp/filters.h>
#include <cryptopp/base64.h>
#include <cstring>
#include <iostream>
#include <fstream>

using namespace CryptoPP;

The entire encryption is just a single for loop:

std::string plaintext = "Burning 'em, if you ain't quick and nimble\nI "
                        "go crazy when I hear a cymbal";
std::string key = "ICE";

std::string result;
for (size_t i = 0; i < plaintext.size(); i++) {
    result += plaintext[i] ^ key[i % key.size()];
}

That’s it. That’s the entire encryption. One loop, one XOR, one modulo. All those switch cases, iterators, flags, and pointer arrays I planned? Replaced by key[i % key.size()].

Then we hex-encode the output for display (lowercase, same as Challenge 2):

std::string hex_output;
StringSource ss(result, true,
                new HexEncoder(new StringSink(hex_output), false));
             
std::cout << "Plaintext: " << plaintext << "\n";
std::cout << "Key: " << key << "\n";
std::cout << "Cyphertext: " << hex_output << "\n";

Running this produces the expected output and matches the hex string from the challenge description.

You can reach the full code at my GitHub: https://github.com/AydoganArslantash/Cryptopals-Solutions/blob/main/set1/chal5.cpp