tdd-deciphered.com

created by @parsingphase

Chapter 8: Enciphering with offsets

We noted in the previous chapter that a rotor's behaviour depended on its core mapping, and on the rotation offset of that core with regards to the wheel, specified either alphabetically or numerically.

We didn't say, however, what that offset actually meant. Where are we offsetting from?

On the physical rotor, there's a catch on the core that clips into a location next to any letter on the alphabet ring. That letter gives the alphabetic offset, or alternately a numeric offset where 1 is A, 2 is etc (considered as 'distance from Z'). The core mapping we’ve looked at previously is specified as the mapping through the rotor at the "default" offset of 'A' or ‘1’, but to simulate the electrical behaviour of the rotor as an encryptor, we need to be able to determine the output character that corresponds to any input character, whether at the the default position or at another offset.

We know we can now specify and test our rotor’s identity and setting; now we look at testing its behaviour. We’ll use the trivial rotor with the identity mapping that we’ve looked at previously. Whatever the ring offset, this will always output the same letter that’s input to it, and we can test this by adding the following test method:

function testCoreIdentityMappingReturnsInputAtDefaultOffset()
{
    $rotor = new Rotor();
    $coreMapping=array(
        'A'=>'A','B'=>'B','C'=>'C','D'=>'D','E'=>'E','F'=>'F','G'=>'G',
        'H'=>'H','I'=>'I','J'=>'J','K'=>'K','L'=>'L','M'=>'M','N'=>'N',
        'O'=>'O','P'=>'P','Q'=>'Q','R'=>'R','S'=>'S','T'=>'T','U'=>'U',
        'V'=>'V','W'=>'W','X'=>'X','Y'=>'Y','Z'=>'Z'
    );
    $offset=1; //anything will do for now
    $rotor->setRingOffset($offset);
    $rotor->setCoreMapping($coreMapping);

    $testCharacter=‘V'; // random choice, just need same out as in

    $this->assertSame(
        $testCharacter,
        $rotor->getOutputCharacterForInputCharacter($testCharacter)
    );
}

Test fail with the message:

PHP Fatal error: Call to undefined method Phase\Enigma\Rotor::getOutputCharacterForInputCharacter()

because we’ve yet to implement the function we’ve been talking about requiring in our interface.

We’ll add that to src/Phase/Enigma/EncryptorInterface.php:

/**
 * Return the output for the given encryptor input in its current state
 *
 * @param string $inputCharacter Single character, uppercase
 * @return string Single character, uppercase
 */
public function getOutputCharacterForInputCharacter($inputCharacter);

and implement it in Rotor.php:

/**
 * Return the output for the given encryptor input in its current state
 *
 * @param string $inputCharacter Single character, uppercase
 * @return string Single character, uppercase
 */
public function getOutputCharacterForInputCharacter($inputCharacter)
{
    return ($this->coreMapping[strtoupper($inputCharacter)]);
}

Again, our tests are working.

Getting the same letter out that we put in isn’t entirely convincing, so let’s test a cipher from way back in history, a Caeser cipher (http://en.wikipedia.org/wiki/Caesar_cipher) with an offset of 13, known more recently as a “ROT 13” cipher:

public function testRot13Mapping()
{
    $rotor = new Rotor();
    $coreMapping=array(
        'A'=>'N','B'=>'O','C'=>'P','D'=>'Q','E'=>'R','F'=>'S','G'=>'T',
        'H'=>'U','I'=>'V','J'=>'W','K'=>'X','L'=>'Y','M'=>'Z','N'=>'A',
        'O'=>'B','P'=>'C','Q'=>'D','R'=>'E','S'=>'F','T'=>'G','U'=>'H',
        'V'=>'I','W'=>'J','X'=>'K','Y'=>'L','Z'=>'M'
    );
    $offset=1; // doesn't matter for this rotor
    $rotor->setRingOffset($offset);
    $rotor->setCoreMapping($coreMapping);

    $testInputCharacter='V';
    $testOutputCharacter='I';

    $this->assertSame(
        $testOutputCharacter,
        $rotor->getOutputCharacterForInputCharacter($testInputCharacter)
    );
}

Now, let’s come back to the Second World War for a real test that takes the ring setting into account. We’ll set up a test with the mapping from the Axis’ Rotor I, which has the mapping:

'A'=>'E','B'=>'K','C'=>'M','D'=>'F','E'=>'L','F'=>'G','G'=>'D', 'H'=>'Q','I'=>'V','J'=>'Z','K'=>'N','L'=>'T','M'=>'O','N'=>'W', 'O'=>'Y','P'=>'H','Q'=>'X','R'=>'U','S'=>'S','T'=>'P', 'U'=>'A','V'=>'I','W'=>'B','X'=>'R','Y'=>'C','Z'=>'J'

Unlike previous rotors, this one will deliver different results depending on the Ringstellung, so we want to add that as a parameter as well as the ring input and output. We should test a few combinations of input and offset, which we'll need to work out manually to start with.

To work out what happens in the rotor here, we'll use a diagram that lets us look at the wheel's input an output; together with the mapping table above we can use this to work out visually how a signal will be routed at any ring offset. Being able to check this visually will be essential for us to create and trust our tests.

First, let's look how a 'V' routes at the default offset. This shows us that, with the mapping above, and with the latch at A, a signal entering at 'V' on the ring (remember, the letters are on the ring) still maps to the 'Virtual V' on the core. Checking our mapping above tells us this will exit the core at 'I', which in turn maps to 'I' on the wheel.

Diagram 1

That's not really a revelation; but if we move the ring in the diagram to a new offset (eg Offset B), the use becomes clearer:

Diagram 2

The ring’s “V” is now the core’s “U”. This maps to the core output of “A”, which is the ring’s “B”. We'll pick up one more data point for our tests, using a larger offset and a different input:

Diagram 3

So at offset 'P' or '16' , G=>R=>U=>J

If we wrap this up in another dataProvider, we can test this wheel at the various offsets:

/**
 * Test routing of data through ring one at various offsets
 * @param int $offset
 * @param string $testInputCharacter
 * @param string $testOutputCharacter
 * @dataProvider rotorIDataProvider
 */
function testRotorIMappingOffset($offset, $testInputCharacter, $testOutputCharacter)
{
    $rotor = new Rotor();
    $coreMapping=array(
        'A'=>'E','B'=>'K','C'=>'M','D'=>'F','E'=>'L','F'=>'G','G'=>'D',
        'H'=>'Q','I'=>'V','J'=>'Z','K'=>'N','L'=>'T','M'=>'O','N'=>'W',
        'O'=>'Y','P'=>'H','Q'=>'X','R'=>'U','S'=>'S','T'=>'P',
        'U'=>'A','V'=>'I','W'=>'B','X'=>'R','Y'=>'C','Z'=>'J'
    );

    $rotor->setRingOffset($offset);
    $rotor->setCoreMapping($coreMapping);


    $this->assertSame($testOutputCharacter,
        $rotor->getOutputCharacterForInputCharacter($testInputCharacter));
}

public function rotorIDataProvider()
{
    return [
        [1, 'V', 'I'],
        [2, 'V', 'B'],
        [16, 'G', 'J']
    ];
}

Of course, we haven't actually written code to handle offsets yet, so the tests are red.

The code we need to handle the offsets can be added to Rotor.php:

/**
 * Return the output for the given encryptor input in its current state
 *
 * @param string $inputCharacter Single character, uppercase
 * @return string Single character, uppercase
 */
public function getOutputCharacterForInputCharacter ($inputCharacter)
{
    $inputCharacter = strtoupper($inputCharacter);

    $coreInputCharacter = $this->getCharacterOffsetBy(
        $inputCharacter,
        $this->ringOffset - 1
    );

    $coreOutputCharacter
        = $this->coreMapping[$coreInputCharacter];

    $outputCharacter = $this->getCharacterOffsetBy(
        $coreOutputCharacter,
        0 - ($this->ringOffset - 1)
    );

    return ($outputCharacter);
}

protected function getCharacterOffsetBy($character, $offset)
{
    $charAsInt =
        $this->charToAlphabetPosition($character);

    $newInteger = (26 + $charAsInt - $offset) % 26;

    if ($newInteger == 0) {
        $newInteger = 26;
    }

    $newCharacter =
        $this->alphabetPositionToCharacter($newInteger);

    return ($newCharacter);
}

protected function charToAlphabetPosition($char)
{
    $position = (ord($char) - 64);

    return $position;
}

protected function alphabetPositionToCharacter($position)
{
    $char = (chr($position + 64));

    return $char;
}

We've unwrapped this into a few internal methods for clarity, but essentially the flow is:

  • Convert the input character to a number
  • Add the offset in modulo-26 maths
  • Convert back to character
  • Do the wheel encryption
  • Convert the output character to a number
  • Subtract the offset in modulo-26 maths
  • Convert back to character

If we now test, we’re green – we’ve implemented the Ringstellung correctly. Now we’re really making some progress on the simulation, so I’ll commit the code for safety.

The tag on github is: https://github.com/parsingphase/enigma-simulator/tree/Chapter8-1-RingCiphers

We’ve added more code than in previous steps here, but it’s all been in order to fix some already-existing tests. We’ve added no new functionality beyond that, and all the functions we’ve added are private, so there are no new calls that can be made into the Rotor. We don’t need to test all these functions as they’re encapsulated within the “unit” of functionality we’re already testing, and can never be used as a distinct unit themselves.

Let’s take a look at our alphanumeric converter though: return (ord($char)-64);

This is obviously very simple, but that simplicity gives a hint of possible problems. This function isn’t just a letter-to-number converter, it’ll convert any ASCII character to an integer, even if it’s not a letter. Is this a good idea? What happens if we give it a numeric input?

As with setting a bad mapping above, we need to throw an exception on bad input data, and we’ll want to test that that exception gets thrown.

We’ll add the following code to \Phase\Enigma\Rotor::getOutputCharacterForInputCharacter:

    if(!preg_match('/^[A-Z]$/',$inputCharacter)) {
        throw new \InvalidArgumentException;
    }

and test it with:

/**
 * Try and encrypt something invalid
 * @dataProvider badCharacterIDataProvider
 * @expectedException \InvalidArgumentException
 */
public function testEncryptBadCharacter($characters)
{
    $rotor = new Rotor();
    $coreMapping = [
        'A'=>'E','B'=>'K','C'=>'M','D'=>'F','E'=>'L','F'=>'G','G'=>'D',
        'H'=>'Q','I'=>'V','J'=>'Z','K'=>'N','L'=>'T','M'=>'O','N'=>'W',
        'O'=>'Y','P'=>'H','Q'=>'X','R'=>'U','S'=>'S','T'=>'P','U'=>'A',
        'V'=>'I','W'=>'B','X'=>'R','Y'=>'C','Z'=>'J'
    ];
    $offset = 0;
    $rotor->setRingOffset($offset);
    $rotor->setCoreMapping($coreMapping);

    // and this should cause the exception:
    $rotor->getOutputCharacterForInputCharacter($characters);
}


public function badCharacterIDataProvider()
{
    return [[' '], [''], [2], ['toolong']];
}

Finally, we can co-opt our charToAlphabetPosition function to allow us to accept alphabetic ring offsets too, revising \Phase\Enigma\Rotor::setRingOffset to:

/**
 * @param string|int $ringOffset
 */
public function setRingOffset($ringOffset)
{
    if (preg_match('/^[A-Z]$/', $ringOffset)) {
        $ringOffset = $this->charToAlphabetPosition($ringOffset);
    }

    if (!is_integer($ringOffset) || ($ringOffset < 1) || ($ringOffset > 26)) {
        throw new \InvalidArgumentException("Offset must be integer in range 1..26");
    }
    $this->ringOffset = $ringOffset;
}

and check that these are accepted by adding a new test:

/**
 * Ensure we accept character ring offset values
 *
 * @dataProvider validRingOffsetProvider
 */
public function testSetAndRememberCharacterRingOffset()
{
    $rotor = new Rotor();
    $rotor->setRingOffset('M');
    $this->assertSame(13, $rotor->getRingOffset());
}

Tag: Chapter8-2-CharRingOffset

And that’s about it for the Rotor. It’s taken us a while to get here as we’ve examined our code very closely and introduced quite a few new tricks, but we’ve got some code for this system element that we can be very confident in.

We've also written by far the most complex part of the code now; from here on it all gets a little simpler. In the next chapter, we'll look at how the rotor sits in the slot, and notice that it's fairly similar logic to what we've seen today.