Building an Enigma Machine From ScratchProject
I built a working Enigma I simulator in Python from the wiring specs up: plugboard, rotor stack, reflector, and the double-step turnover, then wrote a crib-based brute-force search that recovers unknown machine settings from ciphertext alone, profiled it, and parallelized the slowest search across every CPU core for an 11.3x speedup. Try it below, or read the source.
The machine itself is a straight simulation, no shortcuts: real rotor wiring tables for rotors I-V, Beta, and Gamma, real reflectors A/B/C, and the actual signal path a keypress takes: plugboard, through the rotor stack right to left, through the reflector, back through the stack left to right, and out through the plugboard again.
Try it
Type a message and see what a real Enigma machine turns it into, using the classic default setup: rotors I-II-III, reflector B, no plugboard.
Play with the rotors
Same machine, every setting exposed: which rotor sits in each slot, its ring setting and starting position, the reflector, and up to 10 plugboard swaps.
How it works
- A keypress goes through the plugboard, right to left through however many rotors are fitted, through the reflector, then left to right back through the same rotors and out through the plugboard again. That round trip is why the machine is symmetric: the same settings both encode and decode.
- Rotors carry a ring setting and a position, so a given wiring table gets shifted twice, once by which letter the rotor is sitting at, once by the ring, and the two shifts partly cancel out. Getting that offset arithmetic wrong is the easiest way to build an Enigma that "mostly" works and then silently mismatches every real machine's output.
- Stepping follows the historical double-step anomaly: the rightmost rotor always advances, the middle rotor advances if either the rightmost rotor or the middle rotor itself is on its notch, and the leftmost rotor only advances off the middle rotor's own notch. That middle case is what makes an Enigma's period longer than a naive three-wheel odometer would suggest.
Breaking it
The same repo includes a codebreaker that takes ciphertext plus a known or guessed crib (a short stretch of plaintext expected to appear somewhere in the message) and brute-forces the settings: every rotor order, every reflector, every ring setting, every plugboard pairing that's consistent with the crib actually decoding to the crib. It's the same idea Bletchley Park's cryptanalysts worked by hand, run as an exhaustive search instead of an electromechanical Bombe. One harder variant assumes the reflector itself has been physically rewired (two of its internal wire-pairs swapped) and searches every way that rewiring could have happened on top of the usual rotor/ring/plugboard search, around 17,000 reflector variants per base reflector.
Building a fresh rotor and plugboard object for every candidate turned out to matter more than it looked. Early on, the search reused the same rotor objects across thousands of candidates to save allocations, since a rotor's position only changes as characters are encoded through it. That was the bug: position keeps advancing with every character encoded, so reusing an object across candidates left it holding whatever position the previous candidate's encoding had rotated it to, silently corrupting every candidate after the first against the wrong starting position. The fix was rebuilding rotors and the plugboard fresh on every iteration.
Measuring it, then parallelizing it
The brute-force searches process one character at a time with O(1) rotor lookups (wiring tables are inverted once up front, so decoding is a plain array index rather than a linear scan), which predicts search time scaling as roughly search space × message length. Rather than leave that as a claim, I timed every search and checked the throughput against it:
| Search | Search space | Time | Throughput |
|---|---|---|---|
| Smallest | 3 | 0.001s | ~3,000 checks/sec |
| Rotor + position search | 17,576 | 4.35s | ~4,043 checks/sec |
| Slowest single-threaded | 36,864 | 13.89s | ~2,654 checks/sec |
| Small plugboard search | 132 | 0.045s | ~2,933 checks/sec |
| Rewired-reflector search | 514,800 | 10.72s | ~48,004 checks/sec |
Four of the five land in a tight band, ~2,600-4,000 checks/sec, which is the model holding. The fifth looked like an anomaly at ~48,000 checks/sec until I checked what it was actually counting: that search only runs a full machine encode once per reflector variant, then checks the same decoded output against ten candidate cribs without re-encoding. The "514,800" search space over-counts the expensive work almost tenfold; the real number of encode operations is 51,480, which brings its throughput back to ~4,800 checks/sec, right in line with the others. The model was correct throughout, the metric I was feeding it wasn't.
Every candidate check in these searches builds its own machine and touches no shared state, exactly the property that makes brute force "embarrassingly parallel." I split the slowest search across all CPU cores with Python's multiprocessing.Pool, since the GIL rules out real parallelism from threads on CPU-bound work: 13.601s single-threaded down to 1.202s in parallel, an 11.3x speedup, sub-linear against raw core count because of process-startup overhead but still a large win for work this size. On Windows specifically, the default start method is spawn rather than fork, which means the worker function has to live at module level so a fresh interpreter in each process can re-import it by name, and the pool-launching code needs an if __name__ == "__main__": guard, otherwise every spawned worker re-executes the whole script from the top and recursively spawns its own pool.
Sources
- Full source, including the codebreaker: github.com/DragonsBones/enigma