Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

substitution-cipher-solver

test license: MIT

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.

Install

npm install substitution-cipher-solver

Or just copy src/solver.ts + src/trigrams.ts into your project — they have zero runtime dependencies.

Usage

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 });

API

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 }.

How it works

  1. 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.
  2. 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 (probability e^{Δ/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.
  3. Determinism. The only randomness is the annealing, driven by a seeded mulberry32 PRNG — never Math.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.

Related

Part of the open cryptanalysis toolset behind Text Machine — a free, no-signup, in-browser collection of 140+ text & cipher tools:

Also on npm: classical-ciphers · bazeries-cipher.

License

MIT © Text Machine

About

Dependency-free TypeScript monoalphabetic substitution / cryptogram auto-solver — recovers plaintext + cipher alphabet with NO key (simulated annealing + trigram fitness). Powers textmachine.org.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages