Cryptopals: Set 1 – challenge 2

Building on our cryptopals encoding work last time, let's introduce byte-based XOR operations.

Last time, we introduced a value object used to arbitrarily encode and decode raw bytes and keep them wrapped at rest. That object is useful not just in that it simplifies working with bytes, but because it provides a great interface we can extend as we continue with cryptopals.

Our next challenge is to leverage an exclusive OR logic operation (XOR) to encrypt one string of bytes leveraging another:

Fixed XOR

Write a function that takes two equal-length buffers and produces their XOR combination.
If your function works properly, then when you feed it the string: 1c0111001f010100061a024b53535009181c

… after hex decoding, and when XOR’d against:
686974207468652062756c6c277320657965

… should produce:
746865206b696420646f6e277420706c6179

To satisfy this challenge, we need to update our Bytes object to do two things:

  1. Return the length of the byte string it wraps (for easy comparison)
  2. Return a new Bytes object when XORed against another Bytes object of the same length

Updating Bytes

The first step is really just a helper that we can add to peek at the private $bytes property of our object:

class Bytes
{
  // ...

  public function length(): int
  {
    return strlen($this->bytes);
  }

  // ...
}

Once we can reliably get the length of our internal buffer of bytes, we can compare two Bytes objects to ensure they’re the same length. This isn’t something we’ll do externally, but instead internal to an ::xor() method:

class Bytes
{
  // ...

  function xor(Bytes $other): Bytes
  {
    if ($this->length() !== $other->length()) {
      throw new LengthException('Byte objects must be of the same length!');
    }

    $newBytes = '';
    $length = strlen($this->bytes);
    for($i = 0; $i < $length; $i++) {
      $newBytes .= $this->bytes[$i] ^ $other->bytes[$i];
    }

    return new self($newBytes);
  }

  // ...
}

XOR in action

Internally, our function works on raw bytes – we should always work on raw bytes! We can pass the bytes into the system with whatever encoding we want, and we’re left with a Bytes object that lets us encode the answer however we want.

In practice, this challenge would work with our updated Bytes object as follows:

$first = Bytes::fromHex('1c0111001f010100061a024b53535009181c');
$second = Bytes::fromHex('686974207468652062756c6c277320657965');

$result = $first->xor($second);

echo $result->asHex();
// 746865206b696420646f6e277420706c6179