Introduction

In February 1918 the German engineer Arthur Scherbius filed a patent for a cipher machine built around rotors: wired wheels that scramble the alphabet and turn after every letter, so the substitution changes constantly. Sold commercially as Enigma in the 1920s, it was adopted by the German Navy and Army and, by the early 1930s, refined into the military Enigma I that the Wehrmacht used throughout the Second World War.

The Enigma I is an electrical circuit you can hold on your lap:

  • Keyboard and lampboard. Press a key and one of 26 lamps lights up. The operator reads the lamp and writes down the cipher letter; a second operator radios it in Morse.
  • Plugboard (Steckerbrett). Cables swap pairs of letters before and after the rotors. Standard practice from 1939 was 10 cables, i.e. 20 letters swapped and 6 left alone.
  • Three rotors, chosen from a box of five (I–V from December 1938). Each rotor is a fixed permutation of the alphabet; its ring setting (Ringstellung) shifts the wiring relative to the letters on the tyre and to the turnover notch.
  • Reflector (Umkehrwalze B). It pairs letters up and sends the current back through the three rotors a second time.

Before the current flows, the rotors step like an odometer — with one mechanical quirk. The right rotor moves on every key press. When it passes its notch, it pushes the middle rotor. But the pawl that pushes the left rotor also catches the middle rotor's own notch, so when the middle rotor reaches its turnover position it steps again on the next key press, together with the left rotor. That is the famous double step: with rotors I-II-III, the windows go ADU → ADV → AEW → BFX.

One key press, in pseudocode:

press(key):
    step the rotors                 # right always; middle if right is at its notch;
                                    # middle + left if middle is at its own notch
    c = plugboard(key)
    c = R(c); c = M(c); c = L(c)    # forward through the rotors
    c = reflector(c)                # UKW-B: fixed pairs, no letter maps to itself
    c = L⁻¹(c); c = M⁻¹(c); c = R⁻¹(c)   # back through the rotors
    return plugboard(c)             # the lamp that lights

Written as permutations, the encryption of the ii-th letter is a conjugate of the reflector: if σi\sigma_i is the path through the plugboard and the rotors at that moment, then Ei=σi−1 U σiE_i = \sigma_i^{-1}\,U\,\sigma_i. Two consequences follow immediately. Because UU is an involution, so is EiE_i: the same setting that encrypts also decrypts. And because UU has no fixed points, no letter is ever encrypted to itself. The first property made the machine convenient; the second helped to lose a war.

Try It

This is a faithful Enigma I with the historical wirings of rotors I–V and reflector B. Set the rotor order, the ring settings and the start position in the windows, plug some cables, then click the keys (or type on your keyboard).

<!-- Enigma I emulator: rotor settings, plugboard, lampboard, signal path and a crib-dragging tool. -->
<p class="hint">A working <b>Enigma I</b>: three rotors chosen from I–V, reflector B, plugboard and ring settings, with the real double-step. Click the keys (or type on your keyboard) and watch a lamp light up — never the same letter you pressed.</p>
<div class="box">
  <div class="lbl">Rotors (left · middle · right), ring settings and start position</div>
  <div class="rotors" id="rotors"></div>
  <label class="plugrow">Plugboard pairs
    <input id="plugs" type="text" autocomplete="off" spellcheck="false" placeholder="e.g. AV BS CG DL">
  </label>
  <div class="btns">
    <button id="presetTest" type="button">Preset: I-II-III · AAA</button>
    <button id="presetBarb" type="button">Preset: Barbarossa 1941</button>
    <button id="selftest" type="button" class="ghost">Run test vectors</button>
  </div>
</div>
<div class="box">
  <div class="lbl">Lampboard</div>
  <div class="board" id="lamps"></div>
  <div class="lbl">Keyboard (click or type)</div>
  <div class="board" id="keys"></div>
  <div class="status" id="status">Ready. Press a key: a different lamp will light.</div>
  <div class="lbl">Signal path of the last key</div>
  <div class="path" id="path"></div>
  <div class="tape"><span class="tl">In</span><code id="tin"></code></div>
  <div class="tape"><span class="tl">Out</span><code id="tout"></code></div>
  <div class="btns">
    <button id="reset" type="button" class="ghost">Back to start position</button>
    <button id="clear" type="button" class="ghost">Clear tape</button>
  </div>
  <div class="lbl">Encrypt or decrypt a whole text</div>
  <textarea id="txt" rows="3" placeholder="Paste letters here (spaces and punctuation are ignored)"></textarea>
  <div class="btns"><button id="runText" type="button">Run through the machine</button></div>
</div>
<div class="box">
  <div class="lbl">Crib dragging: where can a guessed word NOT go?</div>
  <p class="small">Because Enigma never encrypts a letter to itself, a guessed plaintext word (a crib) cannot sit at any offset where one of its letters equals the ciphertext letter above it. Green offsets survive; red ones are ruled out.</p>
  <label class="col">Ciphertext<textarea id="cribCt" rows="2" spellcheck="false"></textarea></label>
  <label class="col">Crib (guessed plaintext)<input id="cribWord" type="text" autocomplete="off" spellcheck="false"></label>
  <div class="btns"><button id="useOut" type="button" class="ghost">Use the tape output</button></div>
  <div class="status" id="cribSum"></div>
  <div class="cells" id="cribCells"></div>
  <div class="status" id="cribDetail"></div>
  <div class="align" id="cribAlign"></div>
</div>
/* Base layout and panels */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #1f2933; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.small { font-size: .82rem; color: #52606d; margin: .2rem 0 .5rem; line-height: 1.4; }
.box { border: 1px solid #d5dde5; border-radius: 10px; padding: .6rem; margin-bottom: .7rem; background: #f8fafc; }
.lbl { font: 700 .78rem system-ui, sans-serif; text-transform: uppercase; letter-spacing: .04em; color: #52606d; margin: .35rem 0 .3rem; }
/* Rotor slots with their visible window */
.rotors { display: grid; grid-template-columns: repeat(3, 1fr); gap: .4rem; }
.slot { display: flex; flex-direction: column; align-items: center; gap: 3px; background: #fff; border: 1px solid #d5dde5; border-radius: 8px; padding: .35rem .2rem; }
.slot-name { font-size: .75rem; color: #52606d; font-weight: 600; }
.win { width: 44px; border: 2px solid #1d3557; border-radius: 6px; background: #fffdf5; text-align: center; font: 700 15px ui-monospace, monospace; line-height: 1.15; padding: 2px 0; }
.win .cur { display: block; font-size: 22px; color: #1d3557; }
.win .side { display: block; color: #9aa5b1; font-size: 12px; }
.ring-l { font-size: .7rem; color: #52606d; }
select, input, textarea { font: 600 14px ui-monospace, monospace; border: 1px solid #b8c4d0; border-radius: 6px; padding: .25rem .35rem; background: #fff; color: #1f2933; max-width: 100%; }
textarea, .plugrow input, .col input { width: 100%; }
.plugrow, .col { display: block; font-size: .82rem; font-weight: 600; color: #3e4c59; margin: .5rem 0 .2rem; }
.arrow { font: 600 12px system-ui, sans-serif; padding: 0 .7rem; line-height: 1.4; }
/* Lampboard and keyboard in the German QWERTZ layout */
.board { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.row { display: flex; gap: 4px; }
.lamp, .key { width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; font: 700 13px ui-monospace, monospace; user-select: none; }
.lamp { border-radius: 50%; background: #2b2f36; color: #6b7280; border: 1px solid #111; transition: background .15s, color .15s, box-shadow .15s; }
.lamp.on { background: #ffd23f; color: #3a2e00; box-shadow: 0 0 10px 3px rgba(255, 210, 63, .8); }
.key { border-radius: 50%; background: #1d3557; color: #fff; border: 2px solid #0e1d30; cursor: pointer; padding: 0; }
.key:active, .key.down { transform: translateY(1px); background: #457b9d; }
/* Signal-path chips, tapes and buttons */
.path { display: flex; flex-wrap: wrap; gap: 3px; align-items: center; min-height: 2.2em; }
.chip { display: inline-flex; flex-direction: column; align-items: center; min-width: 34px; padding: 2px 4px; border-radius: 6px; background: #e4e7eb; border: 1px solid #cbd2d9; opacity: .35; transition: opacity .15s, background .15s; }
.chip.lit { opacity: 1; background: #fff3c4; border-color: #e0b400; }
.chip.refl.lit { background: #ffd6d6; border-color: #c92f3c; }
.chip b { font: 700 15px ui-monospace, monospace; }
.chip small { font-size: 10px; color: #52606d; white-space: nowrap; }
.sep { color: #9aa5b1; font-size: 12px; }
.tape { display: flex; gap: .4rem; align-items: baseline; margin: .25rem 0; }
.tl { font-size: .75rem; font-weight: 700; color: #52606d; min-width: 2.6em; }
.tape code { font: 600 14px ui-monospace, monospace; word-break: break-all; color: #1d3557; }
.status { font-size: .9rem; font-weight: 600; margin: .45rem 0; min-height: 1.3em; line-height: 1.4; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .4rem; flex-wrap: wrap; margin: .35rem 0; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .75rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
/* Crib-dragging offsets and alignment */
.cells { display: flex; flex-wrap: wrap; gap: 3px; }
.cell { min-width: 28px; height: 26px; font: 600 11px ui-monospace, monospace; border-radius: 5px; border: 1px solid transparent; cursor: pointer; padding: 0 3px; }
.cell.ok { background: #d3f5df; color: #0a5c27; border-color: #8fd8a8; }
.cell.bad { background: #fde0e0; color: #9b1c25; border-color: #f2a5a5; }
.cell.sel { outline: 2px solid #1d3557; }
.align { overflow-x: auto; }
.arow { display: flex; gap: 2px; margin: 2px 0; }
.ac { width: 22px; height: 24px; display: flex; align-items: center; justify-content: center; font: 700 13px ui-monospace, monospace; border-radius: 4px; background: #e4e7eb; flex: 0 0 auto; }
.ac.crib { background: #dbe9f6; }
.ac.clash { background: #c92f3c; color: #fff; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Historical wirings of rotors I–V and the letter shown in the window when each one kicks the next rotor (turnover).
const AZ = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const ROTORS = {
  I:   { w: 'EKMFLGDQVZNTOWYHXUSPAIBRCJ', n: 'Q' },
  II:  { w: 'AJDKSIRUXBLHWTMCQGZNPYFVOE', n: 'E' },
  III: { w: 'BDFHJLCPRTXVZNYEIWGAKMUSQO', n: 'V' },
  IV:  { w: 'ESOVPZJAYQUIRHXLNFTGKDCMWB', n: 'J' },
  V:   { w: 'VZBRGITYUPSDNHLXAWMJQOFECK', n: 'Z' }
};
const NAMES = ['I', 'II', 'III', 'IV', 'V'];
// Reflector B (UKW-B): a fixed pairing of the 26 letters with no letter left alone.
const UKW_B = 'YRUHQSLDPXNGOKMIEBFZCWVJAT';
// Key and lamp rows of the real machine (QWERTZ).
const ROWS = ['QWERTZUIO', 'ASDFGHJK', 'PYXCVBNML'];
const SLOTS = ["Left", "Middle", "Right"];
// Part 1 of a German Army message from 7 July 1941, a classic published test case.
const BARB_CT = 'EDPUDNRGYSZRCXNUYTPOMRMBOFKTBZREZKMLXLVEFGUEYSIOZVEQMIKUBPMMYLKLTTDEISMDICAGYKUACTCDOMOHWXMUUIAUBSTSLRNBZSZWNRFXWFYSSXJZVIJHIDISHPRKLKAYUPADTXQSPINQMATLPIFSVKDASCTACDPBOPVHJK';
const idx = (c) => c.charCodeAt(0) - 65;
const mod = (x) => ((x % 26) + 26) % 26;
const $ = (id) => document.getElementById(id);
const letters = (s) => s.toUpperCase().replace(/[^A-Z]/g, '');
const groups = (s) => s.replace(/(.{5})/g, '$1 ').trim();
const identity = () => AZ.split('').map((_, i) => i);

// Machine state: rotor order (left, middle, right), ring settings, window positions and the plugboard map.
const S = { order: ['I', 'II', 'III'], rings: [0, 0, 0], pos: [0, 0, 0], start: [0, 0, 0], plug: identity() };
let tapeIn = '', tapeOut = '', lastLamp = -1;

// Parse cable pairs such as AV BS; reject reused letters. Returns null on error.
function parsePlugs(text) {
  const map = identity();
  const pairs = text.toUpperCase().split(/[^A-Z]+/).filter(Boolean);
  const used = new Set();
  for (const p of pairs) {
    if (p.length !== 2 || p[0] === p[1] || used.has(p[0]) || used.has(p[1])) return null;
    used.add(p[0]); used.add(p[1]);
    map[idx(p[0])] = idx(p[1]); map[idx(p[1])] = idx(p[0]);
  }
  return { map, n: pairs.length };
}

// Stepping happens BEFORE the current flows. The right rotor always moves; the middle moves when the right sits on its notch;
// if the middle itself sits on its notch, it moves together with the left rotor — the famous double step.
function step(st) {
  const atNotch = (k) => AZ[st.pos[k]] === ROTORS[st.order[k]].n;
  const m = atNotch(1), r = atNotch(2);
  if (m) { st.pos[0] = mod(st.pos[0] + 1); st.pos[1] = mod(st.pos[1] + 1); }
  else if (r) st.pos[1] = mod(st.pos[1] + 1);
  st.pos[2] = mod(st.pos[2] + 1);
  return m;
}
// One pass through a rotor: offset by (position − ring), apply the wiring forward or inverse, undo the offset.
function through(st, k, c, inverse) {
  const w = ROTORS[st.order[k]].w, sh = st.pos[k] - st.rings[k];
  const e = mod(c + sh);
  const o = inverse ? w.indexOf(AZ[e]) : idx(w[e]);
  return mod(o - sh);
}
// Full key press: plugboard → right → middle → left → reflector → left⁻¹ → middle⁻¹ → right⁻¹ → plugboard.
function encipher(st, ch) {
  const dbl = step(st);
  const path = [];
  let c = idx(ch); path.push(c);
  c = st.plug[c]; path.push(c);
  for (const k of [2, 1, 0]) { c = through(st, k, c, false); path.push(c); }
  c = idx(UKW_B[c]); path.push(c);
  for (const k of [0, 1, 2]) { c = through(st, k, c, true); path.push(c); }
  c = st.plug[c]; path.push(c);
  return { out: AZ[c], path, dbl };
}
function fresh(order, rings, pos, plugText) {
  const p = parsePlugs(plugText || '');
  return { order: order.slice(), rings: rings.split('').map(idx), pos: pos.split('').map(idx), plug: p ? p.map : identity() };
}
const runString = (st, s) => s.split('').map((ch) => encipher(st, ch).out).join('');

function setStatus(t, cls) { const s = $('status'); s.textContent = t; s.className = 'status ' + (cls || ''); }

// User interface: rotor selectors, windows with up/down buttons and ring selectors.
function mkBtn(label, aria, fn) {
  const b = document.createElement('button'); b.type = 'button'; b.className = 'ghost arrow';
  b.textContent = label; b.setAttribute('aria-label', aria); b.addEventListener('click', fn); return b;
}
function buildRotors() {
  const box = $('rotors'); box.innerHTML = '';
  for (let k = 0; k < 3; k++) {
    const slot = document.createElement('div'); slot.className = 'slot';
    const nm = document.createElement('div'); nm.className = 'slot-name'; nm.textContent = SLOTS[k];
    const sel = document.createElement('select'); sel.setAttribute('aria-label', SLOTS[k]);
    NAMES.forEach((n) => { const o = document.createElement('option'); o.value = n; o.textContent = n; sel.appendChild(o); });
    sel.value = S.order[k];
    sel.addEventListener('change', () => setRotor(k, sel.value));
    const up = mkBtn('▲', "Turn rotor forward", () => turn(k, 1));
    const win = document.createElement('div'); win.className = 'win'; win.id = 'win' + k;
    const dn = mkBtn('▼', "Turn rotor back", () => turn(k, -1));
    const rl = document.createElement('div'); rl.className = 'ring-l'; rl.textContent = "Ring";
    const ring = document.createElement('select'); ring.setAttribute('aria-label', "Ring");
    for (let i = 0; i < 26; i++) { const o = document.createElement('option'); o.value = String(i); o.textContent = AZ[i] + ' ' + String(i + 1).padStart(2, '0'); ring.appendChild(o); }
    ring.value = String(S.rings[k]);
    ring.addEventListener('change', () => { S.rings[k] = +ring.value; });
    slot.append(nm, sel, up, win, dn, rl, ring);
    box.appendChild(slot);
  }
  refreshWindows();
}
function refreshWindows() {
  for (let k = 0; k < 3; k++) {
    const p = S.pos[k];
    $('win' + k).innerHTML = '<span class="side">' + AZ[mod(p - 1)] + '</span><span class="cur">' + AZ[p] + '</span><span class="side">' + AZ[mod(p + 1)] + '</span>';
  }
}
// Each rotor exists once: choosing a rotor already in use swaps the two slots.
function setRotor(k, name) {
  const other = S.order.indexOf(name);
  if (other !== -1 && other !== k) S.order[other] = S.order[k];
  S.order[k] = name;
  buildRotors();
}
function turn(k, d) { S.pos[k] = mod(S.pos[k] + d); S.start = S.pos.slice(); refreshWindows(); }

function applyPlugs() {
  const r = parsePlugs($('plugs').value);
  if (!r) { S.plug = identity(); setStatus("Plugboard ignored: use pairs like AV BS, each letter at most once.", 'bad'); return false; }
  S.plug = r.map; return true;
}

// Build the lampboard and the clickable keyboard.
function buildBoards() {
  const lamps = $('lamps'), keys = $('keys');
  ROWS.forEach((row) => {
    const lr = document.createElement('div'); lr.className = 'row';
    const kr = document.createElement('div'); kr.className = 'row';
    row.split('').forEach((ch) => {
      const l = document.createElement('div'); l.className = 'lamp'; l.id = 'lamp' + ch; l.textContent = ch; lr.appendChild(l);
      const b = document.createElement('button'); b.type = 'button'; b.className = 'key'; b.textContent = ch;
      b.addEventListener('click', () => pressKey(ch)); kr.appendChild(b);
    });
    lamps.appendChild(lr); keys.appendChild(kr);
  });
}

// Show every intermediate letter of the last key press, lighting the chips in order.
function drawPath(path) {
  const box = $('path'); box.innerHTML = '';
  const inv = '⁻¹';
  const labels = ["key", "plugboard", S.order[2], S.order[1], S.order[0], "reflector B", S.order[0] + inv, S.order[1] + inv, S.order[2] + inv, "plugboard", "lamp"];
  const vals = path.concat([path[path.length - 1]]);
  const chips = [];
  labels.forEach((lab, i) => {
    if (i) { const s = document.createElement('span'); s.className = 'sep'; s.textContent = '→'; box.appendChild(s); }
    const c = document.createElement('div'); c.className = 'chip' + (i === 5 ? ' refl' : '');
    c.innerHTML = '<b>' + AZ[vals[i]] + '</b><small></small>';
    c.querySelector('small').textContent = lab;
    box.appendChild(c); chips.push(c);
  });
  chips.forEach((c, i) => setTimeout(() => c.classList.add('lit'), 45 * i));
}

function renderTape() { $('tin').textContent = groups(tapeIn); $('tout').textContent = groups(tapeOut); }
function light(ch) {
  if (lastLamp !== -1) $('lamp' + AZ[lastLamp]).classList.remove('on');
  if (ch) { $('lamp' + ch).classList.add('on'); lastLamp = idx(ch); } else lastLamp = -1;
}

function pressKey(ch) {
  if (!applyPlugs()) return;
  const r = encipher(S, ch);
  tapeIn += ch; tapeOut += r.out;
  refreshWindows(); renderTape(); light(r.out); drawPath(r.path);
  const msg = i18nFmt("%k% → %o% (the lamp can never be %k% itself).", { k: ch, o: r.out });
  if (r.dbl) setStatus(msg + ' ' + "Double step: the middle rotor sat on its notch, so it moved again and pushed the left rotor.", 'ok'); else setStatus(msg, '');
}

// Push a whole text through the machine, letter by letter, from the current rotor positions.
function runText() {
  if (!applyPlugs()) return;
  const s = letters($('txt').value);
  let last = null;
  for (const ch of s) { last = encipher(S, ch); tapeIn += ch; tapeOut += last.out; }
  refreshWindows(); renderTape();
  if (last) { light(last.out); drawPath(last.path); }
  setStatus(i18nFmt("Processed %n% letters.", { n: s.length }), 'ok');
  renderCrib();
}

function load(order, rings, pos, plugs) {
  S.order = order.slice(); S.rings = rings.split('').map(idx); S.pos = pos.split('').map(idx); S.start = S.pos.slice();
  $('plugs').value = plugs; applyPlugs();
  tapeIn = ''; tapeOut = ''; light(null); $('path').innerHTML = '';
  buildRotors(); renderTape();
}

// Known-answer tests run on fresh machines, independent of the settings on screen.
function selfTest() {
  const fails = [];
  const a = runString(fresh(['I', 'II', 'III'], 'AAA', 'AAA'), 'AAAAA');
  if (a !== 'BDZGO') fails.push('AAAAA→' + a);
  const b = runString(fresh(['I', 'II', 'III'], 'BBB', 'AAA'), 'AAAAA');
  if (b !== 'EWTYX') fails.push('BBB: AAAAA→' + b);
  const st = fresh(['I', 'II', 'III'], 'AAA', 'ADU');
  const seq = [];
  for (let i = 0; i < 3; i++) { step(st); seq.push(st.pos.map((x) => AZ[x]).join('')); }
  if (seq.join(',') !== 'ADV,AEW,BFX') fails.push('ADU→' + seq.join('→'));
  const pt = runString(fresh(['II', 'IV', 'V'], 'BUL', 'BLA', 'AV BS CG DL FU HZ IN KM OW RX'), BARB_CT);
  if (pt.indexOf('AUFKLXABTEILUNG') !== 0) fails.push('Barbarossa');
  if (fails.length) setStatus(i18nFmt("✗ Test vector failed: %what%", { what: fails.join('; ') }), 'bad');
  else setStatus("✓ All test vectors pass: AAAAA→BDZGO, rings BBB→EWTYX, double step ADU→ADV→AEW→BFX, and the 1941 Barbarossa message decrypts.", 'ok');
}

// Crib dragging: an offset is impossible if any crib letter equals the ciphertext letter at the same place.
let cribSel = -1;
function renderCrib() {
  const ct = letters($('cribCt').value), crib = letters($('cribWord').value);
  const cells = $('cribCells'); cells.innerHTML = ''; $('cribAlign').innerHTML = ''; $('cribDetail').textContent = '';
  if (!crib || ct.length < crib.length) { $('cribSum').textContent = "Enter a ciphertext at least as long as the crib."; $('cribSum').className = 'status bad'; return; }
  const total = ct.length - crib.length + 1;
  const clashes = [];
  for (let i = 0; i < total; i++) {
    let n = 0;
    for (let j = 0; j < crib.length; j++) if (ct[i + j] === crib[j]) n++;
    clashes.push(n);
  }
  const ok = clashes.filter((n) => n === 0).length;
  $('cribSum').textContent = i18nFmt("%ok% of %total% offsets are still possible; %bad% are ruled out by a letter that would map to itself.", { ok, total, bad: total - ok });
  $('cribSum').className = 'status';
  if (cribSel < 0 || cribSel >= total) cribSel = clashes.indexOf(0) === -1 ? 0 : clashes.indexOf(0);
  clashes.forEach((n, i) => {
    const b = document.createElement('button'); b.type = 'button';
    b.className = 'cell ' + (n ? 'bad' : 'ok') + (i === cribSel ? ' sel' : '');
    b.textContent = String(i);
    b.addEventListener('click', () => { cribSel = i; renderCrib(); });
    cells.appendChild(b);
  });
  const n = clashes[cribSel];
  const verdict = n ? i18nFmt("impossible — %n% letter(s) would encrypt to themselves.", { n }) : "possible — no letter collides.";
  $('cribDetail').textContent = i18nFmt("Offset %i%: %verdict%", { i: cribSel, verdict });
  $('cribDetail').className = 'status ' + (n ? 'bad' : 'ok');
  const r1 = document.createElement('div'); r1.className = 'arow';
  const r2 = document.createElement('div'); r2.className = 'arow';
  for (let j = 0; j < crib.length; j++) {
    const c = ct[cribSel + j], p = crib[j];
    const a = document.createElement('div'); a.className = 'ac' + (c === p ? ' clash' : ''); a.textContent = c; r1.appendChild(a);
    const d = document.createElement('div'); d.className = 'ac crib' + (c === p ? ' clash' : ''); d.textContent = p; r2.appendChild(d);
  }
  $('cribAlign').append(r1, r2);
}

// Buttons and inputs.
$('presetTest').addEventListener('click', () => { load(['I', 'II', 'III'], 'AAA', 'AAA', ''); setStatus("Rotors I-II-III, reflector B, rings AAA, start AAA, no cables. Type AAAAA — you should get BDZGO.", 'ok'); });
$('presetBarb').addEventListener('click', () => { load(['II', 'IV', 'V'], 'BUL', 'BLA', 'AV BS CG DL FU HZ IN KM OW RX'); $('txt').value = groups(BARB_CT); setStatus("Barbarossa message loaded (rotors II-IV-V, rings BUL, start BLA, 10 cables). Press “Run through the machine” to read the German plaintext.", 'ok'); });
$('selftest').addEventListener('click', selfTest);
$('reset').addEventListener('click', () => { S.pos = S.start.slice(); tapeIn = ''; tapeOut = ''; light(null); $('path').innerHTML = ''; refreshWindows(); renderTape(); setStatus("Ready. Press a key: a different lamp will light.", ''); });
$('clear').addEventListener('click', () => { tapeIn = ''; tapeOut = ''; renderTape(); });
$('runText').addEventListener('click', runText);
$('plugs').addEventListener('change', () => { const r = parsePlugs($('plugs').value); if (r) { S.plug = r.map; setStatus(i18nFmt("Plugboard set: %n% cable(s).", { n: r.n }), 'ok'); } else applyPlugs(); });
$('useOut').addEventListener('click', () => { if (tapeOut) { $('cribCt').value = groups(tapeOut); cribSel = -1; renderCrib(); } });
$('cribCt').addEventListener('input', () => { cribSel = -1; renderCrib(); });
$('cribWord').addEventListener('input', () => { cribSel = -1; renderCrib(); });
// Typing on a physical keyboard presses the Enigma keys (except while editing a field).
document.addEventListener('keydown', (e) => {
  const t = e.target && e.target.tagName;
  if (t === 'INPUT' || t === 'TEXTAREA' || t === 'SELECT' || e.ctrlKey || e.metaKey || e.altKey) return;
  const ch = (e.key || '').toUpperCase();
  if (ch.length === 1 && ch >= 'A' && ch <= 'Z') { e.preventDefault(); pressKey(ch); }
});

buildBoards();
buildRotors();
renderTape();
$('cribCt').value = groups(BARB_CT.slice(0, 60));
$('cribWord').value = 'ABTEILUNG';
cribSel = 6;
renderCrib();
Result
Console

Things to try:

  • Press Preset: I-II-III · AAA and type AAAAA. You should read BDZGO — the standard test vector every Enigma emulator must reproduce. Run test vectors checks it, together with the double step and a real 1941 message, on a fresh machine.
  • Watch the signal path: the letter you press goes through the plugboard, three rotors, the reflector and back. The lamp is never the key you pressed.
  • Go back to the start position and type the ciphertext: you get the plaintext back. Enigma is its own inverse.
  • Load Barbarossa 1941, a German Army message from 7 July 1941, and run it through the machine: it starts AUFKL X ABTEILUNG X VON X KURTINOWA… ("reconnaissance unit from Kurtinowa…").
  • In the crib-dragging panel, slide the guessed word ABTEILUNG along the ciphertext. Every offset where some letter would have to encrypt to itself turns red. That simple rule was the codebreakers' first filter.

The Real Complexity

How many keys? For the Army/Air Force Enigma I with three rotors out of five and 10 plugboard cables:

  • rotor order: 5⋅4⋅3=605 \cdot 4 \cdot 3 = 60
  • start positions: 263=17,57626^{3} = 17{,}576
  • plugboard with 10 cables: 26!6! ⋅ 10! ⋅ 210=150,738,274,937,250\dfrac{26!}{6!\,\cdot\,10!\,\cdot\,2^{10}} = 150{,}738{,}274{,}937{,}250

Multiplied together that is 158,962,555,217,826,360,000≈1.59×1020158{,}962{,}555{,}217{,}826{,}360{,}000 \approx 1.59 \times 10^{20}, about 2672^{67}. The ring settings add another 26226^{2} (the left ring's notch never matters), which only changes when the middle and left rotors turn over; counting them gives roughly 1.07×10231.07 \times 10^{23}. Either way, trying keys one by one was out of the question in the 1940s — even at a million keys per second, 1.59×10201.59 \times 10^{20} keys take about five million years.

The codebreakers never searched that space. They split it: the plugboard, which contributes almost all of the size (≈247\approx 2^{47}), was eliminated by mathematics or logic, leaving only the rotor part — 60×17,576≈10660 \times 17{,}576 \approx 10^{6} settings — for exhaustive search.

Poland, 1932–1939. Marian Rejewski, a young mathematician at the Polish Cipher Bureau, reconstructed the wiring of the military rotors in December 1932, using the theory of permutations plus operator documents obtained by French intelligence. German operators then sent each message key twice at the start of a message. The six permutations A1,…,A6A_1,\dots,A_6 that encrypted those six letters gave products A1A4A_1A_4, A2A5A_2A_5, A3A6A_3A_6 whose cycle structure — the "characteristic" of the day — does not depend on the plugboard, because the plugboard acts by conjugation and conjugate permutations have the same cycle type. So the Poles could catalogue the characteristics of all 6×17,576=105,4566 \times 17{,}576 = 105{,}456 rotor settings (only three rotors existed then), look the day's characteristic up, and recover the plugboard afterwards. When the procedure changed in 1938, Henryk Zygalski introduced perforated sheets and Rejewski designed the bomba, an electromechanical machine that searched rotor positions. On 15 December 1938 the Germans added rotors IV and V, multiplying the rotor orders from 6 to 60 and overwhelming Polish resources; in July 1939, at Pyry near Warsaw, the Poles handed everything to the French and British.

Bletchley Park, 1940 onward. The doubled indicator was dropped in May 1940, so a new method was needed. Alan Turing's bombe (the first went into service in 1940) works from a crib: a guessed piece of plaintext, such as a weather report or a stock phrase. The rule you can see in the demo places the crib — no letter encrypts to itself, so many alignments are impossible. The crib's letter pairs form a menu, a graph whose loops constrain the plugboard. For each rotor setting the bombe assumes one plugboard pair, propagates all its implications through the menu electrically, and rejects the setting on a contradiction. Gordon Welchman's diagonal board (1940) added the symmetry of the plugboard (A↔BA \leftrightarrow B implies B↔AB \leftrightarrow A), making the deductions far stronger. A run checks all 17,576 positions for one rotor order; the few survivors were tested by hand.

In complexity terms: brute force is Θ(∣K∣)\Theta(|K|) with ∣K∣≈267|K| \approx 2^{67}; the attacks cost about 60⋅263≈22060 \cdot 26^{3} \approx 2^{20} rotor settings, each checked in time polynomial in the crib length. A cipher is only as strong as the part of its key you cannot separate from the rest.

Where It Matters

In the war. Intelligence from broken Enigma traffic, code-named Ultra, reached Allied commanders on land, in the air and at sea. The fight was hardest over the naval Enigma: in February 1942 the U-boats switched to the four-rotor M4, and the Atlantic went dark for most of the year, until documents recovered from the sinking U-559 in October 1942 helped Bletchley Park break back in that December. Historians still debate how much Ultra changed; the official historian Harry Hinsley argued it shortened the war by at least two years.

In computing. The bombes were among the first large machines to automate logical search, and Turing's wartime work sits next to his foundational paper on the halting problem. (Colossus, the famous electronic machine at Bletchley, attacked a different cipher, the Lorenz teleprinter cipher — not Enigma.)

In cryptography today. Enigma's failure reads like a list of modern design rules:

  • Kerckhoffs's principle. The wiring was reconstructed by the enemy; security must rest on the key alone, as it does in AES and RSA.
  • No structural bias. "Never maps a letter to itself" is a distinguisher. Modern block ciphers are required to look like random permutations, with no such property.
  • Resistance to known plaintext. Cribs were Enigma's undoing; modern ciphers must stay secure even when the attacker knows or chooses huge amounts of plaintext.
  • Procedures matter. Repeated message keys, predictable greetings and reused settings did more damage than any weakness of the rotors. A stream cipher with a reused keystream fails for the same reason, and only the one-time pad is unconditionally secure — at the cost of a key as long as the message.

Conclusion

Enigma is the classic proof that a big key space is not the same as security. Its ≈1.59×1020\approx 1.59 \times 10^{20} settings looked impregnable, but the machine leaked structure: an involution with no fixed points, a plugboard that left cycle structure untouched, and operators who sent predictable words. Rejewski turned that structure into algebra; Turing and Welchman turned it into logic and wired it into machines.

Today the whole attack fits in a browser tab — the demo above is a complete Enigma I. The lesson it carries is still the first one taught in cryptography: assume the enemy knows your machine, assume they can guess some of your plaintext, and make sure that even then, the only way in is to search a space too large to search.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/enigma-machine/Content licensed under CC BY-NC 4.0.