Skip to main content

Disypher

Classical cipher analysis that refuses to guess.

Paste an encrypted text and the engine tries every known method, scores each result, and ranks the hypotheses by confidence. When twenty characters are not enough to choose, it says so rather than putting an invention first.

$ disypher "Jnxr hc Arb. Gur Zngevk unf lbh."

> ROT13          63%  shift 13  [en]
    Wake up Neo. The Matrix has you.

(6 more — use --top 7)
24 letters · index of coincidence 0.036 · 7 hypotheses
Short sample (24 letters). Statistical analysis is unreliable below 40 letters.

Live demo: bredouane.github.io/disypher · Install: pip install disypher · No dependencies, standard library only.

What makes it different

It can say "I don't know." Most tools rank something first no matter what. This one reports a short sample, an ambiguous result or an already-readable text as answers in their own right. A verdict you cannot trust is worse than no verdict.

Nothing is truncated silently. Every hypothesis and every alternative setting is returned. A list cut at an arbitrary point looks exhaustive without being it — the reader stops wondering whether the answer was among the ones hidden.

Every parameter accepted when encrypting is accepted when decrypting. Otherwise the tool produces text it cannot read back. That symmetry is checked, not assumed.

It tells you what it did not touch. ROT13 on a phone number leaves the digits in the clear; Morse deletes punctuation and capitals for good. Both are reported, on your text, measured rather than described.

The documentation is measured, not written. disypher info runs each cipher on a probe text to determine whether it is its own inverse, which character classes it changes, and what it destroys. A hand-written description eventually contradicts the code.

Ciphers

Family Methods
Substitution Caesar, ROT5, ROT13, ROT18, ROT47, Atbash, Affine
Polyalphabetic Vigenère, Beaufort
Transposition Rail fence, Columnar, Reversed
Encoding Base64, Hexadecimal, Binary, Morse

Vigenère, Beaufort and columnar transposition are broken without the key — the first two by estimating the period from the index of coincidence then solving each column, the third by enumerating permutations, since only the key's alphabetical ordering matters.

Command line

disypher list                    every cipher, its family and its parameters
disypher banks                   loaded languages and their vocabulary
disypher info NAME               what one cipher does, measured from the code
disypher decrypt TEXT            analyse an encrypted text
disypher encrypt TEXT -c ...     encrypt through a chain

disypher TEXT on its own is a shortcut for disypher decrypt TEXT. If the installed command is not on PATH — common on Windows — python -m disypher always works.

Decrypting

Option
--only caesar,affine restrict the search to these ciphers
--set caesar.shift=3,7,13 impose a setting instead of searching for it; repeatable
--top 7 how many hypotheses to show
--variants also list each hypothesis's alternative settings
--bank es load a vocabulary bank; repeatable
--peel N re-analyse hypothesis N instead of printing it
--json the full report, machine-readable

Long or multi-line messages come from standard input, which a shell would otherwise mangle:

disypher decrypt - < message.txt

--peel strips one layer of a text encrypted several times over. Combine it with --only when you know which layer comes first:

$ disypher encrypt "Wake up Neo" -c caesar:shift=7 -c reverse
> 2. reverse
    oaU wb lrhD

$ disypher decrypt "oaU wb lrhD" --only reverse --peel 1
> Caesar         83%  shift 7  [en]
    Wake up Neo

Encrypting

Each -c adds one step, applied in order. Removing a step means not writing it: a command line has no state to edit.

$ disypher encrypt "Attack at dawn, now!" -c caesar:shift=7 -c reverse -c base64 --stages

  1. caesar     shift=7
    Haahjr ha khdu, uvd!
  2. reverse
    !dvu ,udhk ah rjhaaH
> 3. base64
    IWR2dSAsdWRoayBhaCByamhhYUg=

Parameters follow the cipher name, separated by colons — -c caesar:shift=7, -c vigenere:key=lemon:autokey=true. Colons rather than commas, because a comma is itself a legitimate separator value: -c "hex:sep=," has to remain expressible.

Steps that lose or leak information say so:

$ disypher encrypt "Attack at dawn, now!" -c morse

> 1. morse
    .- - - .- -.-. -.- / .- - / -.. .- .-- -.
    ! permanently lost: punctuation, case

Inspecting a cipher

$ disypher info railfence

RAILFENCE
  Classical transposition

  Family                transposition
  Encrypt parameters    rails=3, offset=0
  Decrypt settings      rails, offset
  Changes               letters, digits
  Own inverse           no
  Solved without a key  yes

HOW IT WORKS
  Here no letter is replaced. They are all kept; only their order changes.
  ...
    rail 1  A...C...D...
    rail 2  .T.A.K.T.A.N
    rail 3  ..T...A...W.

    read ->  ACDTAKTANTAW

HISTORY
  ...

SEE ALSO
  disypher info caesar, disypher info columnar

--brief stops after the table. Rail fence, columnar transposition and Vigenère come with a diagram, since those three resist explanation in prose — and the diagrams are drawn by running the engine, so they cannot depict something the code no longer does.

Three ciphers accept different settings on each side, and disypher list shows both columns:

Encrypt only Decrypt only Why
columnar pad width padding is stripped on reading; width bounds the permutation search
base64 urlsafe — both alphabets are recognised automatically
hex upper — hexadecimal reading is case-insensitive

Parameter names are validated, so a typo is reported rather than silently ignored.

Library

import disypher

report = disypher.analyse("Jnxr hc Arb. Gur Zngevk unf lbh.")
best = report.candidates[0]

best.cipher        # 'caesar'
best.params        # {'shift': 13}
best.plaintext     # 'Wake up Neo. The Matrix has you.'
best.confidence    # 0.63
report.reliable    # False — 24 letters is not enough
report.reason      # 'short_sample'

Reasons come back as codes rather than sentences, so any interface can render them in its own language. report.note gives the English rendering.

disypher.analyse(text, ["caesar", "affine"])
disypher.analyse(text, ["caesar"], {"caesar": {"shift": [3, 7, 13]}})

disypher.encode_stages(text, [
    {"cipher": "caesar", "params": {"shift": 7}},
    {"cipher": "reverse"},
    {"cipher": "base64"},
])

disypher.describe("vigenere")      # measured facts
disypher.CIPHER_NOTES["vigenere"]  # written notes and history
disypher.round_trip_loss(text, "morse")   # ['punctuation', 'case']

How confidence is computed

Three signals, each covering a weakness of the others:

  • chi-squared on letter frequencies — reliable on long text, unmeasurable on short text. Its weight follows its own measurability rather than being scored as a failure when it cannot be computed;
  • the ratio of recognised words — decisive on short text, blind to quantity;
  • the weight of recognised words, where each match counts for its length. A random two-letter string has one chance in 676 of forming a given word; a seven-letter string, one in eight billion.

It is a heuristic score between 0 and 1, not a probability, and the tool does not pretend otherwise.

Language banks

English and French are built in. Spanish, German, Italian and a technical vocabulary bank ship with the package and load on demand:

disypher.load_builtin_bank("es")
disypher.load_builtin_bank("technical")   # dev and unit vocabulary

technical is not a language: it holds terms such as "commit", "kg" or "frontend" and extends every language already loaded rather than registering a new one. More vocabulary means better recognition on short texts, and one more language the engine can identify.

Web interface

index.html runs the engine in the browser through Pyodide. It fetches disypher/__init__.py as-is rather than reimplementing it in JavaScript, so the page and the published package can never drift apart. Serve it over HTTP:

python -m http.server 8000

Scope

These are classical ciphers: historical, educational, and broken. Nothing here protects anything, and none of it should be used to. The interesting part is the analysis — deciding what a text is, and admitting when the evidence does not support a decision.

Licence

MIT. See LICENSE.

Metadata

Release files for disypher 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for disypher 0.2.0
File Size Uploaded
disypher-0.2.0.tar.gz 87.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for disypher 0.2.0
File Interpreter ABI Platform
disypher-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 136.7 kB

Release files / disypher-0.2.0.tar.gz

Download URL disypher-0.2.0.tar.gz
Size 87.3 kB
Tags Source
SHA-256 checksum
How to use checksums
b36b5b5f2affc6d9f64f174d7698798a40ad445300a8065b755b1a071dc0dc69
BLAKE2b-256 checksum
How to use checksums
7e96f975fd32c0ac6eff6f00305f7a0055605eddeb6b4cf1bde70b1909c2b12f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.3

Release files / disypher-0.2.0-py3-none-any.whl

Download URL disypher-0.2.0-py3-none-any.whl
Size 49.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e6f7cd3ce32feb9ef07b91cbdf9458d7b69e5909482f2cb6e0073e034c399ebb
BLAKE2b-256 checksum
How to use checksums
9fa5dcb2b2f87989513fb38524e2fe5781edd06cbd5d588825e78dc89bae874a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.3

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page