Vigenere cipher solver: FFT periodogram + Kasiski + twist++ + beam search + interpolated n-gram LM, with learned posterior weights and an adaptive 'best' ensemble.
Project description
vigenere
A production-grade Vigenere cipher solver that combines classical
cryptanalysis (Kasiski, index of coincidence, FFT coincidence
periodogram, twist / twist++) with modern decoding (per-strip beam
search guided by an interpolated character n-gram language model) and a
learned softmax posterior over key lengths. Pure-Python core, numpy
for the FFT, optional rich for live progress.
On the easy regime (random English plaintext, key length 4–12, 400+
characters) the best decoder hits 100 % key recovery on 30 random
samples and the learned key-length posterior reaches 96.7 % top-1 /
100 % top-3 out of 29 candidates.
- PyPI: https://pypi.org/project/vigenere-solver-ng/
- Source: https://github.com/supasuge/VigenereSolver-ng
- Docs:
docs/THEORY.md·docs/API.md·docs/CLI.md·docs/BUILDING.md
Install
# from PyPI (recommended)
pip install vigenere-solver-ng
# or
uv pip install vigenere-solver-ng
uv tool install vigenere-solver-ng
# as a globally available CLI via git clone locally
git clone https://github.com/supasuge/VigenereSolver-ng
cd VigenereSolver-ng
uv tool install . -e
Requires Python ≥ 3.10. Runtime deps: numpy, rich.
For a dev install (editable, with tests), see
docs/BUILDING.md.
Quickstart — library
from vigenere import encrypt, decrypt, solve
ct = encrypt("the quick brown fox jumps over the lazy dog ...", "LEMON")
result = solve(ct, decoder="best")
print(result.key) # "LEMON"
print(result.plaintext)
print(result.keylen_posterior) # [(5, 0.41), (10, 0.18), ...]
print(result.confidence) # 0.92 — strong unique winner
print(result.match_against("LEMON")) # MatchResult(kind="exact", ...)
print(result.pretty()) # formatted summary
Force a known key length to skip estimation:
solve(ct, decoder="classic", forced_keylens=[7])
Custom n-gram order or weights:
solve(ct, decoder="classic", scorer_kwargs={
"order": 5,
"lambdas": [0.02, 0.08, 0.20, 0.30, 0.40],
"alpha": 1e-4,
})
For a full end-to-end CTF-style walkthrough (Base64-wrapped ciphertext →
clean → solve → round-trip verification) see
src/core/cipher.py — runnable with
python -m core.cipher.
Quickstart — CLI
# Encrypt
echo "the quick brown fox" | vigenere encrypt --key LEMON
# Solve (adaptive auto-mode: fast preset first, escalates only if confidence
# is below threshold).
vigenere solve --in ciphertext.txt
vigenere solve --in ciphertext.txt --no-progress --json
vigenere solve --in ciphertext.txt --decoder classic --keylens 7
# Inspect raw key-length signals
vigenere explain --in ciphertext.txt --max-k 40
Full subcommand reference (including bench, compare, tune,
optimize, gen-corpus) lives in docs/CLI.md.
Public API at a glance
from vigenere import (
encrypt, decrypt,
clean_letters, random_key,
solve, solve_auto,
SolveResult,
)
| symbol | what it does |
|---|---|
encrypt |
Vigenere encryption; preserves non-letters, normalizes case |
decrypt |
inverse |
clean_letters |
strip non-A–Z, upper-case (use this on raw challenge input) |
random_key |
uniform random A–Z key (deterministic with a seeded Random) |
solve |
full attack pipeline → SolveResult |
solve_auto |
fast preset, escalates to best ensemble only if low confidence |
SolveResult |
dataclass: key, plaintext, keylen_posterior, candidates, confidence, match_against, … |
Detailed signatures and every internal module: docs/API.md.
How it works (one-paragraph summary)
For each candidate key length $k$ the solver computes four signals —
average per-strip IoC, Kasiski divisor votes, FFT coincidence
periodogram with harmonic suppression, and twist++ (Barr & Simoes 2015)
— z-scores them, combines them with learned weights, and softmaxes
to a posterior $P(m=k \mid \text{ct})$. The top key lengths feed a
per-strip Caesar beam search guided by Jensen–Shannon divergence
against the English unigram prior; the top keys per beam are re-ranked
under an interpolated 1..5-gram character LM. The best decoder runs
every scorer, pools their candidates, and re-ranks the union under the
n-gram LM — strictly $\geq$ any single decoder on accuracy. Full
derivations: docs/THEORY.md.
Measured impact
compare harness, 30 trials, key length 4–12, ≥500 chars:
| decoder | key_acc | exact_acc | char_acc | mean_sec |
|---|---|---|---|---|
best |
1.000 | 1.000 | 1.000 | 0.12 |
classic |
1.000 | 0.733 | 1.000 | 0.08 |
tiny-lm |
0.967 | 0.700 | 0.999 | 0.05 |
legacy |
0.93 | 0.000 | 0.93 | 0.04 |
On harder regimes (very long keys against short ciphertexts — each strip has <20 letters) accuracy degrades because the information isn't there; that's a property of the cipher problem, not the solver.
Build, test, release
See docs/BUILDING.md for editable installs,
running the test suite (pytest -v — ~101 tests), building wheels
(uv build), and publishing to PyPI (twine upload dist/*).
License
MIT. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file vigenere_solver_ng-0.1.1.tar.gz.
File metadata
- Download URL: vigenere_solver_ng-0.1.1.tar.gz
- Upload date:
- Size: 53.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1e380805909e63ef6a25dd9cd4ee7889e436e19c2b3394f27bbbdf605883c5fa
|
|
| MD5 |
9e848d785b1b55978cb68fa3446dd8d2
|
|
| BLAKE2b-256 |
425b2450fa9ab773c9be688820ef84046e1a94fc54444f2d9b84b1fa9f67e1e8
|
File details
Details for the file vigenere_solver_ng-0.1.1-py3-none-any.whl.
File metadata
- Download URL: vigenere_solver_ng-0.1.1-py3-none-any.whl
- Upload date:
- Size: 48.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d9762d28cd40a515ea1649417be54e5923635f0174164331dd94b1aa3f0e71dd
|
|
| MD5 |
2b7b45f8ab9fc20e5d43120fd7fa0d05
|
|
| BLAKE2b-256 |
ee46aa66f276b64cf9850669a5573fc8698fdbd932df5156a128ac73c184c695
|