A tiny, dependency-free TypeScript monoalphabetic substitution / cryptogram auto-solver. Give it ciphertext and it recovers the plaintext and the substitution alphabet — with no key known — by searching the 26! ≈ 4×10²⁶ key space for the decryption that reads most like English.
Powers the free, in-browser solver at Text Machine → Substitution Cipher Solver — paste a cryptogram, solve it instantly, nothing leaves your browser.
🔎 Not sure your ciphertext is even a simple substitution? Run it through the Cipher Identifier first — it fingerprints unknown ciphertext (letter frequency, Index of Coincidence, χ² tests) and points you to the right decoder. For polyalphabetic ciphers, see the Vigenère Solver; to just visualise the statistics, the Frequency Analysis tool.
npm install substitution-cipher-solverOr just copy src/solver.ts + src/trigrams.ts
into your project — they have zero runtime dependencies.
import { solveSubstitution } from "substitution-cipher-solver";
// A simple-substitution cryptogram — no key needed to solve it:
const cipher =
"Zit qkz gy ltektz vkozofu iql yqleofqztr htghst ygk dqfn etfzxkotl, qfr " +
"zit dtzigrl xltr zg iort q dtllqut iqct ukgvf lztqrosn dgkt estctk gctk " +
"zodt. Q lodhst lioyz gy zit qshiqwtz vql gfet tfgxui zg hxmmst q eqlxqs " +
"ktqrtk, wxz leigsqkl lggf stqkftr zg egxfz igv gyztf tqei stzztk qhhtqktr " +
"qfr zg uxtll zit iorrtf hqzztkf ykgd ziqz qsgft.";
const { plaintext, key, letters } = solveSubstitution(cipher);
console.log(plaintext);
// → "The art of secret writing has fascinated people for many centuries, and the
// methods used to hide a message have grown steadily more clever over time. A
// simple shift of the alphabet was once enough to puzzle a casual reader, but
// scholars soon learned to count how often each letter appeared and to guess
// the hidden pattern from that alone."
console.log(key); // recovered cipher→plain alphabet: "JXVMCNOPHQRSZYIKADLEGWBUFT"
console.log(letters); // 275 (A–Z letters scored)Short inputs (a single pangram, a few words) are intractable — the letter and trigram statistics need text to lock onto. Aim for ~150+ letters; ~250+ is comfortably solved.
The solve is deterministic: the same ciphertext always yields the same result (the only randomness is a seeded PRNG). Pass options to tune it:
solveSubstitution(cipher, { runs: 8, seed: 12345 });| Export | Signature | Purpose |
|---|---|---|
solveSubstitution |
(text, opts?) => SubstitutionSolution |
Crack a cryptogram — returns { plaintext, key, score, letters, runs }. |
decodeWithKey |
(text, key) => string |
Decode with a known 26-letter cipher→plain alphabet (case + punctuation preserved). |
scoreText |
(text) => number |
English-likeness (summed trigram log-probability) of any text. |
letterCounts |
(text) => number[] |
A–Z frequency counts (index 0 = A … 25 = Z). |
MIN_LETTERS |
number |
Below this many letters the statistics get unreliable (still attempted). |
DEFAULT_RUNS |
number |
Default number of annealing runs. |
SolveOptions = { runs?: number; seed?: number }.
SubstitutionSolution = { plaintext: string; key: string; score: number; letters: number; runs: number }.
- Fitness. Every candidate key decrypts the ciphertext, and the decryption is
scored by its trigram fitness — the summed log-probability of its letter
triples against real English (
trigrams.ts, an add-one-smoothed 26³ table built from a ~1.14M-letter corpus). Correct English is dense in common triples (THE,AND,ING); any wrong mapping injects rare ones and scores strictly lower. Trigram (not single-letter) fitness is essential — a near-miss can match letter frequencies yet cannot fake a whole sheet of real triples. - Search. Fitness is maximised by simulated annealing, the textbook attack
on the substitution cipher. From a frequency-matched seed (most common cipher
letter →
E, next →T, …) it repeatedly swaps two letters' targets, always accepting an improvement and sometimes accepting a setback (probabilitye^{Δ/T}) so it escapes the local optima that trap a plain greedy hill-climb. The temperature cools to zero; a final greedy pass locks in the nearest optimum. Several runs from different starts are raced and the best decryption wins. - Determinism. The only randomness is the annealing, driven by a seeded
mulberry32PRNG — neverMath.random— so results are reproducible, shareable and testable.
Realistic cryptograms of ~150+ letters solve at ≥95%; even pathological texts land ≥93% in well under two seconds.
Part of the open cryptanalysis toolset behind Text Machine — a free, no-signup, in-browser collection of 140+ text & cipher tools:
- Cipher Identifier — fingerprint unknown ciphertext
- Vigenère Solver — break polyalphabetic ciphers with no key
- Frequency Analysis — letter/bigram/trigram statistics & IoC
- Caesar · Atbash · Playfair and more
Also on npm: classical-ciphers · bazeries-cipher.
MIT © Text Machine