Introduction

A cipher replaces the letters of a message, the plaintext, with other letters to produce a ciphertext, and a key says exactly how. Three classical designs show nearly everything that matters about the pencil-and-paper era.

The Caesar shift. The Roman biographer Suetonius records that Julius Caesar protected private letters by replacing each letter with the one three places further along the alphabet, so that A became D, B became E, and so on; he adds that Augustus used a shift of one. With the alphabet numbered A=0,…,Z=25A=0,\dots,Z=25, encryption and decryption are

Ek(x)=(x+k) mod 26,Dk(y)=(y−k) mod 26.E_k(x) = (x + k) \bmod 26, \qquad D_k(y) = (y - k) \bmod 26 .

There are only 2525 useful keys, so an attacker simply tries them all.

Monoalphabetic substitution. Instead of sliding the alphabet, shuffle it: every plaintext letter is replaced by a fixed, arbitrary partner. The key is a permutation of the 26 letters, so there are

26!=403 291 461 126 605 635 584 000 000≈4×102626! = 403\,291\,461\,126\,605\,635\,584\,000\,000 \approx 4\times 10^{26}

keys. That is far too many to test one by one, and for many centuries the size of this number was taken as a sign of strength.

The Vigenère cipher. Use a keyword and apply a different Caesar shift to each letter in turn, repeating the keyword as needed. With the keyword K=k0k1…km−1K = k_0 k_1 \dots k_{m-1} the ii-th letter is enciphered as

ci=(pi+ki mod m) mod 26.c_i = (p_i + k_{i \bmod m}) \bmod 26 .

The same plaintext letter now lands on up to mm different ciphertext letters. The method was described by Giovan Battista Bellaso in his 1553 booklet La cifra del Sig. Giovan Battista Bellaso; it was attached to the name of the French diplomat Blaise de Vigenère only in the nineteenth century, even though his own book describes a different, autokey system. For about three centuries it was known as le chiffre indéchiffrable, the indecipherable cipher.

The statistical defence against all three came from the same place. The earliest known account of frequency analysis is the treatise on deciphering cryptographic messages written by the Baghdad scholar Al-Kindi in the ninth century. Its method is still the first thing a cryptanalyst tries: count the symbols of the ciphertext, rank them, and match them to the ranked letter frequencies of the language.

# Caesar break: choose the shift whose decryption looks most like the language
for k in 0..25:
    chi2[k] = sum over letters L of (count_k(L) - expected(L))^2 / expected(L)
return argmin_k chi2[k]

Break the Ciphers

Choose a sample text (or type your own), pick a cipher and a key, and press Encrypt. The histogram shows how often each letter occurs in the text; the orange marks are the typical frequencies of English or Spanish.

<!-- Layout: sample and cipher pickers, key, input, actions, output, frequency chart -->
<p class="hint">Pick a sample text, a cipher and a key, then encrypt. The chart compares the letter frequencies of the text with the usual frequencies of the language (orange marks). Then try the break button on the ciphertext: it uses nothing but statistics. Accents are removed and only the letters A to Z are enciphered.</p>
<div class="row">
  <label class="lbl">Sample text
    <select id="samplesel"><option value="en">English</option><option value="es">Spanish</option></select>
  </label>
  <label class="lbl">Cipher
    <select id="ciphersel"><option value="caesar">Caesar</option><option value="subst">Substitution</option><option value="vig">Vigenère</option></select>
  </label>
</div>
<div class="row">
  <label class="lbl grow"><span id="keylbl"></span>
    <input id="key" type="text" spellcheck="false" autocomplete="off" aria-label="Cipher key">
  </label>
  <button id="rnd" type="button" class="ghost">Random key</button>
</div>
<label class="blk">Input text
  <textarea id="inp" rows="5" spellcheck="false"></textarea>
</label>
<div class="btns">
  <button id="enc" type="button">Encrypt</button>
  <button id="dec" type="button">Decrypt with key</button>
  <button id="brk" type="button" class="warn"></button>
  <button id="swap" type="button" class="ghost">Output to input</button>
</div>
<div class="status" id="status" aria-live="polite"></div>
<label class="blk">Output text
  <textarea id="out" rows="5" spellcheck="false"></textarea>
</label>
<div class="row">
  <label class="lbl">Chart of
    <select id="chartsel"><option value="in">input</option><option value="out">output</option></select>
  </label>
  <label class="lbl">Reference
    <select id="refsel"><option value="en">English</option><option value="es">Spanish</option></select>
  </label>
</div>
<div class="chart" id="hist" role="img" aria-label="Letter frequency histogram, A to Z, text against reference"></div>
<div class="legend"><span class="sw bar"></span>text <span class="sw mark"></span>reference <span id="hstat"></span></div>
<div id="analysis" hidden>
  <div class="at" id="an1t"></div>
  <div class="chart small" id="an1"></div>
  <div class="at" id="an2t"></div>
  <div class="chart small" id="an2"></div>
</div>
/* Colours follow the light or dark scheme of the page */
:root { --fg:#1f2933; --muted:#52606d; --bg:#ffffff; --box:#eef2f6; --boxb:#cdd9e3; --acc:#1d3557;
        --bar:#5a7fa8; --mark:#e76f51; --hi:#e76f51; --ok:#0a7d33; --bad:#c92f3c; }
@media (prefers-color-scheme: dark) {
  :root { --fg:#e4e9ee; --muted:#a3b1bf; --bg:#14181d; --box:#1e262f; --boxb:#3a4a5a; --acc:#8fb3dd;
          --bar:#6f95bf; --mark:#f4a261; --hi:#f4a261; --ok:#4cc38a; --bad:#ff7b84; }
}
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: var(--fg); background: var(--bg); margin: 0; padding: 4px; }
.hint { font-size: .86rem; color: var(--muted); margin: .2rem 0 .6rem; line-height: 1.45; }
.row { display: flex; flex-wrap: wrap; gap: .4rem 1rem; margin-bottom: .45rem; align-items: center; }
.lbl { font-size: .85rem; display: flex; align-items: center; gap: .35rem; }
.lbl.grow { flex: 1 1 200px; }
.lbl.grow input { flex: 1; min-width: 0; }
.blk { display: block; font-size: .85rem; margin-bottom: .4rem; }
select, input, textarea { font: 13px ui-monospace, monospace; color: var(--fg); background: var(--box);
  border: 1px solid var(--boxb); border-radius: 6px; padding: .3rem .4rem; }
textarea { display: block; width: 100%; margin-top: .2rem; resize: vertical; line-height: 1.4; }
.btns { display: flex; gap: .45rem; flex-wrap: wrap; margin: .3rem 0 .4rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .8rem; border: 1px solid var(--acc);
  background: var(--acc); color: var(--bg); border-radius: 8px; cursor: pointer; }
button.ghost { background: transparent; color: var(--acc); }
button.warn { background: var(--hi); border-color: var(--hi); color: #1a1a1a; }
.status { font-size: .88rem; font-weight: 600; min-height: 1.3em; margin: .2rem 0 .4rem; }
.status.ok { color: var(--ok); }
.status.bad { color: var(--bad); }
.chart { display: flex; align-items: flex-end; gap: 2px; height: 96px; padding: 4px 2px 16px; margin: .3rem 0;
  background: var(--box); border: 1px solid var(--boxb); border-radius: 8px; }
.chart.small { height: 76px; }
.col { position: relative; flex: 1; height: 100%; min-width: 0; }
.bar { position: absolute; left: 12%; right: 12%; bottom: 0; background: var(--bar); border-radius: 2px 2px 0 0; min-height: 1px; }
.bar.hi { background: var(--hi); }
.mark { position: absolute; left: 0; right: 0; height: 2px; background: var(--mark); }
.lab { position: absolute; left: 0; right: 0; bottom: -15px; text-align: center; font: 9px ui-monospace, monospace; color: var(--muted); }
.legend { font-size: .78rem; color: var(--muted); display: flex; flex-wrap: wrap; align-items: center; gap: .2rem .4rem; }
.sw { display: inline-block; width: 12px; height: 8px; }
.sw.bar { position: static; background: var(--bar); }
.sw.mark { position: static; height: 2px; background: var(--mark); }
.at { font-size: .82rem; font-weight: 600; margin-top: .6rem; }
[hidden] { display: none !important; }
const A = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const REF = {
  en: [8.167,1.492,2.782,4.253,12.702,2.228,2.015,6.094,6.966,0.153,0.772,4.025,2.406,6.749,7.507,1.929,0.095,5.987,6.327,9.056,2.758,0.978,2.360,0.150,1.974,0.074],
  es: [12.53,1.42,4.68,5.86,13.68,0.69,1.01,0.70,6.25,0.44,0.02,4.97,3.15,6.71,8.68,2.51,0.88,6.87,7.98,4.63,3.93,0.90,0.01,0.22,0.90,0.52]
};
const norm = (s) => s.normalize("NFD").replace(/[̀-ͯ]/g, "").toUpperCase();
const lettersOf = (s) => norm(s).replace(/[^A-Z]/g, "");
function mapText(s, fn) {
  let i = 0;
  return norm(s).replace(/[A-Z]/g, (ch) => fn(ch.charCodeAt(0) - 65, i++));
}
const mod26 = (n) => ((n % 26) + 26) % 26;
const caesar = (s, k) => mapText(s, (c) => A[mod26(c + k)]);
function validSubKey(k) {
  const u = lettersOf(k);
  return u.length === 26 && new Set(u).size === 26;
}
function subst(s, key, dec) {
  const k = lettersOf(key);
  const inv = [];
  for (let i = 0; i < 26; i++) inv[k.charCodeAt(i) - 65] = A[i];
  return mapText(s, (c) => (dec ? inv[c] : k[c]));
}
function vig(s, key, dir) {
  const k = lettersOf(key).split("").map((ch) => ch.charCodeAt(0) - 65);
  if (!k.length) return norm(s);
  return mapText(s, (c, i) => A[mod26(c + dir * k[i % k.length])]);
}
function counts(t) {
  const c = new Array(26).fill(0);
  for (const ch of t) c[ch.charCodeAt(0) - 65]++;
  return c;
}
function chiShift(c, n, ref, k) {
  const tot = ref.reduce((a, b) => a + b, 0);
  let x = 0;
  for (let p = 0; p < 26; p++) {
    const e = (n * ref[p]) / tot;
    const o = c[(p + k) % 26];
    x += ((o - e) * (o - e)) / e;
  }
  return x;
}
function breakCaesar(text, ref) {
  const t = lettersOf(text);
  const c = counts(t);
  const chis = [];
  for (let k = 0; k < 26; k++) chis.push(chiShift(c, t.length, ref, k));
  let best = 0;
  for (let k = 1; k < 26; k++) if (chis[k] < chis[best]) best = k;
  return { shift: best, chis };
}
function ic(t) {
  const n = t.length;
  if (n < 2) return 0;
  const c = counts(t);
  let s = 0;
  for (const v of c) s += v * (v - 1);
  return s / (n * (n - 1));
}
function columns(t, L) {
  const cols = [];
  for (let j = 0; j < L; j++) {
    let s = "";
    for (let i = j; i < t.length; i += L) s += t[i];
    cols.push(s);
  }
  return cols;
}
function kasiski(t, maxL) {
  const pos = {};
  for (let i = 0; i + 3 <= t.length; i++) (pos[t.substr(i, 3)] = pos[t.substr(i, 3)] || []).push(i);
  const dists = [];
  const votes = new Array(maxL + 1).fill(0);
  for (const g in pos) {
    const p = pos[g];
    for (let a = 0; a < p.length; a++)
      for (let b = a + 1; b < p.length; b++) {
        const d = p[b] - p[a];
        dists.push(d);
        for (let f = 2; f <= maxL; f++) if (d % f === 0) votes[f]++;
      }
  }
  return { dists, votes };
}
function breakVigenere(text, ref, maxL) {
  const t = lettersOf(text);
  const kas = kasiski(t, maxL);
  const ics = [0];
  for (let L = 1; L <= maxL; L++) {
    const cols = columns(t, L);
    ics.push(cols.reduce((a, s) => a + ic(s), 0) / L);
  }
  const mx = Math.max(...ics);
  let L = 1;
  while (L < maxL && ics[L] < 0.9 * mx) L++;
  let key = "";
  for (const col of columns(t, L)) key += A[breakCaesar(col, ref).shift];
  return { kas, ics, L, key, plain: vig(text, key, -1) };
}

const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ""));
// Shortcut to find an element by id
const $ = (id) => document.getElementById(id);
const SAMPLES = { en: "Secrecy has always been a race between the people who hide messages and the people who want to read them. A general who sends orders across a battlefield cannot stop the courier from being caught, so he scrambles the words and hopes that the enemy will fail to unscramble them. The simplest scrambling just slides every letter a fixed distance along the alphabet. It is easy to carry out by hand and it feels mysterious, but it has only twenty-five useful keys, and a patient clerk can try them all before lunch. A better idea is to shuffle the alphabet completely, so that every letter has its own private disguise. The number of possible shuffles is astronomically large, far beyond any search, and for centuries that number made such ciphers look unbreakable. The weakness was never in the count of keys. It was in the language itself. In every ordinary text some letters appear far more often than others, and the disguise of a frequent letter is frequent too. A curious reader who counts the symbols and compares them with the usual habits of the language soon sees which symbol stands for the most common letter, and the rest of the message follows like a crossword puzzle. To hide those habits the cipher must use several alphabets in turn, switching according to a secret word, and for a long time that remedy seemed to be perfect. Yet the secret word repeats, and repetition leaves a trace that a careful analyst can measure.", es: "El secreto siempre ha sido una carrera entre quienes ocultan los mensajes y quienes quieren leerlos. Un general que envía órdenes a través de un campo de batalla no puede impedir que el mensajero sea capturado, así que revuelve las palabras y espera que el enemigo no logre ordenarlas de nuevo. La mezcla más sencilla desplaza cada letra una distancia fija a lo largo del alfabeto. Es fácil de hacer a mano y parece misteriosa, pero solo tiene veinticinco claves útiles, y un escribiente paciente puede probarlas todas antes del almuerzo. Una idea mejor consiste en barajar el alfabeto por completo, de modo que cada letra tenga su propio disfraz. El número de barajas posibles es astronómico, mucho más de lo que cualquier búsqueda podría recorrer, y durante siglos esa cifra hizo que estos sistemas parecieran inquebrantables. La debilidad nunca estuvo en la cantidad de claves, sino en el idioma mismo. En cualquier texto corriente algunas letras aparecen mucho más que otras, y el disfraz de una letra frecuente también es frecuente. Un lector curioso que cuenta los símbolos y los compara con las costumbres habituales de la lengua descubre pronto cuál representa a la letra más común, y el resto del mensaje se va resolviendo como un crucigrama. Para ocultar esas costumbres el cifrado debe usar varios alfabetos por turnos, cambiando según una palabra secreta, y durante mucho tiempo ese remedio pareció perfecto. Pero la palabra secreta se repite, y la repetición deja una huella que un analista cuidadoso puede medir." };
const keys = { caesar: "3", subst: "QWERTYUIOPASDFGHJKLZXCVBNM", vig: "QUIPU" };
let cur = "caesar";

// Draws a bar chart; an optional marker line shows the reference value
function drawBars(el, vals, labels, opts) {
  opts = opts || {};
  const mx = Math.max(...vals, ...(opts.ref || [0]), 1e-9);
  el.innerHTML = "";
  vals.forEach((v, i) => {
    const col = document.createElement("div");
    col.className = "col";
    const bar = document.createElement("div");
    bar.className = "bar" + (opts.hi === i ? " hi" : "");
    bar.style.height = (100 * v) / mx + "%";
    col.appendChild(bar);
    if (opts.ref) {
      const m = document.createElement("div");
      m.className = "mark";
      m.style.bottom = (100 * opts.ref[i]) / mx + "%";
      col.appendChild(m);
    }
    const l = document.createElement("span");
    l.className = "lab";
    l.textContent = labels[i];
    col.appendChild(l);
    el.appendChild(col);
  });
}

// Shows a short message under the buttons
function say(msg, cls) {
  const s = $("status");
  s.textContent = msg;
  s.className = "status " + (cls || "");
}
const refNow = () => REF[$("refsel").value];

// Letter frequencies of the chosen text against the language reference
function drawHist() {
  const t = lettersOf($(($("chartsel").value === "out") ? "out" : "inp").value);
  const c = counts(t);
  const n = Math.max(t.length, 1);
  const ref = refNow();
  const tot = ref.reduce((a, b) => a + b, 0);
  drawBars($("hist"), c.map((v) => (100 * v) / n), A.split(""), { ref: ref.map((v) => (100 * v) / tot) });
  $("hstat").textContent = i18nFmt("%n% letters · index of coincidence %ic%", { n: t.length, ic: ic(t).toFixed(4) });
}

function readKey() {
  const raw = $("key").value;
  if (cur === "caesar") {
    const k = parseInt(raw, 10);
    return Number.isNaN(k) ? null : mod26(k);
  }
  if (cur === "subst") return validSubKey(raw) ? raw : null;
  return lettersOf(raw).length ? raw : null;
}
function badKeyMsg() {
  return cur === "caesar" ? "The shift must be a whole number." : cur === "subst" ? "The alphabet must use each of the 26 letters exactly once." : "The keyword needs at least one letter.";
}
function run(dec) {
  const k = readKey();
  if (k === null) { say(badKeyMsg(), "bad"); return; }
  const s = $("inp").value;
  let o;
  if (cur === "caesar") o = caesar(s, dec ? -k : k);
  else if (cur === "subst") o = subst(s, k, dec);
  else o = vig(s, k, dec ? -1 : 1);
  $("out").value = o;
  if (!dec) $("chartsel").value = "out";
  $("analysis").hidden = true;
  say(dec ? "Decrypted with the key." : "Encrypted. The chart now shows the ciphertext.", "ok");
  drawHist();
}

// Automatic attacks: chi-squared for Caesar, Kasiski plus index of coincidence for Vigenère
function doBreak() {
  const s = $("inp").value;
  const t = lettersOf(s);
  if (t.length < 40) { say("Put at least 40 letters in the input box so there is something to analyse.", "bad"); return; }
  const ref = refNow();
  const an = $("analysis");
  an.hidden = false;
  $("an1").innerHTML = "";
  $("an2").innerHTML = "";
  if (cur === "subst") {
    an.hidden = true;
    say("A substitution key has about 4 x 10^26 possibilities, so nobody tries them all. Instead, match the tallest bars of the chart to the most frequent letters of the language, then fix the rest from context.", "");
    return;
  }
  if (cur === "caesar") {
    const r = breakCaesar(s, ref);
    $("an1t").textContent = "Chi-squared for each possible shift (lowest bar wins)";
    $("an2t").textContent = "";
    drawBars($("an1"), r.chis, r.chis.map((_, i) => String(i)), { hi: r.shift });
    $("out").value = caesar(s, -r.shift);
    say(i18nFmt("Best shift: %k% (chi-squared %x%). The output shows the text decrypted with it.", { k: r.shift, x: r.chis[r.shift].toFixed(1) }), "ok");
    return;
  }
  const r = breakVigenere(s, ref, 20);
  const votes = r.kas.votes.map((v, i) => [i, v]).filter((p) => p[0] >= 2).sort((a, b) => b[1] - a[1]).slice(0, 3);
  $("an1t").textContent = i18nFmt("Kasiski: %n% distances between repeated trigrams, votes per divisor 2 to 20", { n: r.kas.dists.length });
  drawBars($("an1"), r.kas.votes.slice(2), r.kas.votes.slice(2).map((_, i) => String(i + 2)), { hi: votes[0][0] - 2 });
  $("an2t").textContent = "Average index of coincidence of the columns for key lengths 1 to 20";
  drawBars($("an2"), r.ics.slice(1), r.ics.slice(1).map((_, i) => String(i + 1)), { hi: r.L - 1 });
  $("out").value = r.plain;
  say(i18nFmt("Key length %L% (Kasiski favours %v%); key found: %key%. The output shows the decrypted text.", { L: r.L, key: r.key, v: votes.map((p) => p[0]).join(", ") }), "ok");
}

function setCipher(c) {
  keys[cur] = $("key").value;
  cur = c;
  $("key").value = keys[c];
  $("keylbl").textContent = c === "caesar" ? "Shift (0 to 25)" : c === "subst" ? "Alphabet (26 letters)" : "Keyword";
  $("analysis").hidden = true;
  $("brk").textContent = c === "caesar" ? "Break Caesar (chi-squared)" : c === "vig" ? "Break Vigenère (Kasiski + IC)" : "Break substitution?";
  say("", "");
}
function randomKey() {
  if (cur === "caesar") $("key").value = String(1 + Math.floor(Math.random() * 25));
  else if (cur === "vig") {
    let k = "";
    const L = 3 + Math.floor(Math.random() * 5);
    for (let i = 0; i < L; i++) k += A[Math.floor(Math.random() * 26)];
    $("key").value = k;
  } else {
    const a = A.split("");
    for (let i = 25; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; }
    $("key").value = a.join("");
  }
}
function loadSample() {
  const l = $("samplesel").value;
  $("inp").value = SAMPLES[l];
  $("refsel").value = l;
  $("out").value = "";
  $("chartsel").value = "in";
  $("analysis").hidden = true;
  say("", "");
  drawHist();
}

$("samplesel").addEventListener("change", loadSample);
$("ciphersel").addEventListener("change", (e) => setCipher(e.target.value));
$("refsel").addEventListener("change", drawHist);
$("chartsel").addEventListener("change", drawHist);
$("inp").addEventListener("input", drawHist);
$("out").addEventListener("input", drawHist);
$("enc").addEventListener("click", () => run(false));
$("dec").addEventListener("click", () => run(true));
$("brk").addEventListener("click", doBreak);
$("rnd").addEventListener("click", randomKey);
$("swap").addEventListener("click", () => {
  $("inp").value = $("out").value;
  $("out").value = "";
  $("chartsel").value = "in";
  $("analysis").hidden = true;
  drawHist();
});
$("key").value = keys.caesar;
setCipher("caesar");
loadSample();
Result
Console

Things to try:

  • Caesar. Encrypt with a shift of your choice, then press Output to input and Break Caesar. The whole histogram has simply slid sideways, and the chi-squared bar chart shows a single deep minimum at the true shift.
  • Substitution. Encrypt with the default or a random alphabet. The tallest bars no longer sit on E and A, but the shape of the histogram is intact: a few tall bars, a long tail of rare letters. The index of coincidence under the chart does not change at all.
  • Vigenère. Encrypt with a keyword and look at the histogram: it flattens, and the index of coincidence drops toward the random value 1/26≈0.0381/26 \approx 0.038. Now break it. The Kasiski chart counts how many repeated-trigram distances are divisible by each number, the second chart shows the average index of coincidence of the columns for each key length, and the keyword is recovered column by column.

The Real Complexity

Counting keys is the wrong measure of strength. The table compares what the designer hoped for with what the attacker needs.

Cipher Keys Attack Cost on nn letters
Caesar 2525 useful try every shift, or chi-squared O(n+262)O(n + 26^2)
Substitution 26!≈4×102626! \approx 4\times 10^{26} frequency matching, then context essentially O(n)O(n) counting plus a short search
Vigenère, keyword length mm 26m26^m Kasiski or index of coincidence for mm, then mm Caesar breaks O(n⋅mmax⁡+26 n)O(n \cdot m_{\max} + 26\,n)
One-time pad 26n26^n none works not applicable

Frequency matching. If cac_a is the observed count of letter aa in nn letters and ea=n pae_a = n\,p_a the count expected from the language's letter probabilities pap_a, the chi-squared statistic

χ2=∑a=AZ(ca−ea)2ea\chi^2 = \sum_{a=A}^{Z} \frac{(c_a - e_a)^2}{e_a}

is small when a text looks like the language. For a Caesar ciphertext, evaluate it for the 26 shifted histograms and keep the smallest. One pass over the text builds the counts, then 26 shifts cost 26×2626 \times 26 operations.

Index of coincidence. William F. Friedman introduced it in 1922. For a text with letter counts cac_a,

IC=∑aca(ca−1)n(n−1),\mathrm{IC} = \frac{\sum_a c_a (c_a - 1)}{n (n - 1)},

the probability that two letters drawn at random are equal. Ordinary English gives about ∑apa2≈0.066\sum_a p_a^2 \approx 0.066, Spanish about 0.0750.075, and uniformly random letters give 1/26≈0.0381/26 \approx 0.038. A substitution cipher only renames letters, so it keeps the IC of the language. A Vigenère ciphertext mixes mm alphabets, so its IC sits between the two values. If you split the ciphertext into LL columns (every LL-th letter) and the column IC jumps back up toward the language value, then LL is a multiple of the key length: each column is a plain Caesar cipher.

Kasiski examination. If the same trigram of plaintext meets the same part of the keyword twice, it is enciphered identically twice. So the distance between repeated ciphertext trigrams is usually a multiple of the key length; counting which small numbers divide many of the distances points at mm. Friedrich Kasiski published it in 1863 in Die Geheimschriften und die Dechiffrir-Kunst. Charles Babbage had found the same attack privately earlier, but his notes went unpublished and were recognised only much later.

Unicity distance. Shannon's Communication Theory of Secrecy Systems (1949) asks how much ciphertext makes the key unique. It is about U≈H(K)/DU \approx H(K)/D, the key entropy divided by the redundancy of the language per letter. For a monoalphabetic substitution H(K)=log⁡226!≈88.4H(K) = \log_2 26! \approx 88.4 bits, while English carries roughly three bits of redundancy per letter, so a few dozen letters of ciphertext already have essentially one sensible decryption. The huge key space is irrelevant once the text is that long.

Perfect secrecy. In the same 1949 paper Shannon proved that a cipher is perfectly secret when the ciphertext carries no information about the plaintext, and that this requires a key at least as long as the message, chosen uniformly at random and never reused. That is the one-time pad, built on the teleprinter cipher of Gilbert Vernam (1917) and made unbreakable in principle by Joseph Mauborgne's addition of a truly random, non-repeating key. Everything shorter leaks structure, which is exactly what the attacks above exploit.

Where It Matters

Nobody protects data with a Vigenère cipher today, yet the ideas still shape the field.

  • Kerckhoffs's principle. In La cryptographie militaire (1883), Auguste Kerckhoffs argued that a system must remain secure even if everything about it except the key becomes known to the enemy. Modern cryptography takes this as a design rule: the algorithms of AES are public and only the key is secret.
  • From Vigenère to stream ciphers. A repeating keyword is a very weak keystream. Replace it with a long pseudorandom stream and the same additive structure becomes a stream cipher; reuse the keystream and the old attacks come straight back, which is how two-time-pad failures in real protocols are broken.
  • Diffusion and confusion. A substitution by itself leaves the letter statistics untouched, which is why modern block ciphers combine substitution with long-range mixing so that no single symbol statistic survives.
  • Machines. Rotor machines were an attempt to build a huge number of alphabets mechanically; the Enigma machine shows both how far that idea went and why it still fell to mathematics.
  • Information theory. Redundancy in language is what makes classical ciphers breakable, and measuring it is the business of Shannon entropy.
  • Puzzles and teaching. Cryptograms in newspapers and escape games are solved with exactly these tools, and the Caesar break is a first example of a statistical test used as a decoder.

Conclusion

A shift has 2525 keys, a shuffled alphabet has about 4×10264\times 10^{26}, and a keyword multiplies the possibilities again. None of that counted, because the plaintext's own statistics leaked through every one of these designs: Al-Kindi's ranking of letters, chi-squared on a shifted histogram, Kasiski's repeated trigrams and Friedman's index of coincidence are the same idea in four forms.

The lesson is the one Shannon proved: secrecy is not about how many keys there are, but about how much structure the ciphertext keeps. Hide all of it with a random key as long as the message, as in the one-time pad, or accept that a determined analyst will find whatever is left.

Share this article

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

Comments

Loading comments...

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