Skip to main content

MorseKey — type a phrase in Morse, get a BIP39 mnemonic. And back. Offline, deterministic, open-source.

Project description

MorseKey logo

MorseKey

also known as bip39-morse (the package / repo / CLI name)
BIP39 seed phrases ⇄ Morse code · offline · deterministic · open-source

CI Release PyPI License: MIT Python 3.11+

Русская версия

Interactive TUI that converts a Morse-typed phrase into a BIP39 mnemonic (and back), with pluggable per-locale Morse alphabets.

Demo

[!CAUTION] The phrase below is the iroha — a classical Heian-era pangram of the kana, globally known public text. The wallet derived from it is therefore public too: anyone running the same command produces the same mnemonic. This is illustration, not a usage recipe.

Animated terminal demo: forward-mode generation of a 12-word BIP39 mnemonic from the iroha typed as Morse entropy

If the SVG above does not animate in your viewer, the final frame as PNG:

Final state: shell prompt, iroha echoed, BIP39 mnemonic

The iroha (色は匂へど…) is a 9th–10th c. Buddhist meditation on impermanence. Its 47 syllables use each kana exactly once, making it simultaneously a famous poem and a pangram of the syllabary. Approximate translation:

Beautiful colours fade away — who in this world endures forever? Today I cross the deep mountain of impermanence, and shall not dream shallow dreams, nor be intoxicated.

The demo feeds the iroha kana-by-kana through the Japanese Wabun alphabet (loaded from examples/japanese.txt). The 47 kana produce 186 bits, well past the 132 bits needed for a 12-word seed — the indicator turns green on the 36th kana when entropy_bits=128 is reached, the rest is overflow that goes only into the visual buffer.

Reproduce locally:

bip39-morse --morse-table examples/japanese.txt --length 12 --ascii
# then type the iroha and press Enter

Reverse direction — mnemonic → Morse text

Same demo run in --reverse mode. The 12 BIP39 words from above are typed in (prefix-autocomplete commits each word as soon as it's unique), and the tool decodes the resulting 128 entropy bits back into Morse-shaped Latin text. The final print is formatted as --group-size 4 --per-line 4, i.e. blocks of 4 characters, 4 blocks per row:

Animated terminal demo: reverse-mode decoding of the iroha-derived mnemonic into Morse text, formatted as 4x4 blocks Reverse final state: shell prompt, then the decoded Morse text in 4x4 blocks

The decoded text round-trips through forward mode: feeding it back as a Morse phrase reproduces the same 128 entropy bits, hence the same first 11 mnemonic words (BIP39's last-word-checksum behaviour is the usual caveat). See the round-trip self-check under the security warning.

Pipeline

The full demo pipeline is in docs/generate_demo.pypython3 docs/generate_demo.py regenerates both casts (forward + reverse) and renders them via termtosvg (loop delay 5 s, so the final frame lingers long enough to read) plus watermark injection, then rasterises each static SVG to PNG via rsvg-convert. To rebuild from scratch: pip install termtosvg and brew install librsvg.

Security warning — read this before generating anything real

Generating a real BIP39 phrase on a general-purpose computer is inherently risky and is done entirely at your own risk. Only proceed on a machine you are completely certain is not under an attacker's control — and even then, treat the result as compromised the moment anything unexpected happens during the session.

  • Windows 11 is strongly discouraged as the host OS. Telemetry, Recall snapshots, OneDrive auto-sync, the clipboard history service, screen-capture pipelines, third-party update agents, and the long tail of unaudited drivers all make it a poor environment for handling seed entropy. The same caution applies, in varying degrees, to any consumer OS that you have not personally hardened.

  • Recommended setup. Boot a clean live OS (e.g. Tails or a minimal Linux ISO) from a USB stick on a known-trusted machine. Disconnect the network. Run the tool. Never persist the phrase, the mnemonic, or any intermediate hex to a disk or cloud drive — type it directly into the offline cold-storage device that will hold the seed.

  • Round-trip self-check before you trust the output. This tool is fully deterministic and reversible — verify that yourself, on the spot:

    1. Run forward mode on your typed phrase → note the entropy hex (the part to the left of on the green-indicator line) and the resulting mnemonic.
    2. Run --reverse with the same --length/--wordlist, enter the mnemonic → it prints the decoded "reverse trash" text.
    3. Run forward mode again on that reverse-trash text → it must produce the same entropy hex as step 1 (and hence the same mnemonic).

    If the hex in steps 1 and 3 does not match exactly, do not use the mnemonic — something in the environment is interfering with the tool.

  • Wipe traces. After verification, shut down the live OS cleanly so swap and tmpfs are gone with it. Do not screenshot the TUI, do not paste anything into a chat or a note-taking app, and remember that anything you typed may still be in scrollback until the terminal process exits.

How it works

The script is a bridge between two representations of the same entropy:

  • a phrase in Morse code (a sequence of characters whose International Morse codes, written as 0/1 for ./-, concatenate into a bit string), and
  • a BIP39 mnemonic (a list of 12/18/24 words drawn from a 2048-word dictionary, where each word encodes 11 bits of a longer string formed by entropy ‖ SHA256(entropy)).

It runs as an interactive TUI (prompt_toolkit) and has two opposite directions:

Morse alphabet

bip39_morse/morse.py keeps three kinds of lookup data:

  • Letters are organised as a layered registry: an ordered list of (locale, mapping) pairs. Two layers are built in — en (LATIN, 26 codes) and ru (CYRILLIC, 33 codes incl. ё, sharing the code of е). Extra layers can be appended at runtime via --morse-table PATH (see below). When two layers define the same character, the later layer wins.
  • Digits 0–9 — all 5-symbol ITU codes (shared across all locales).
  • Punctuation., ,, -, !, ?, : (5–6 symbols, shared).

For any character, char_to_bits(ch) looks up its Morse code in the merged forward map (every loaded layer collapsed into one char → morse dict) and rewrites it as a bit string (.0, -1). Space yields the empty bit string — it acts only as a visual word separator and contributes no entropy.

Reverse decoding (bits_to_text(bits, locale=...)) reads only the layers tagged with the chosen locale, so the same bit pattern can decode into different alphabets depending on which locale you ask for.

A key property exploited later for the built-in en/ru locales: both e/е map to the single bit 0 and both t/т map to the single bit 1, so every possible bit string is decodable into letters without padding. Custom locales need not satisfy this; the decoder falls back to digits, then punctuation, if no letter matches at any length.

Custom Morse tables

Pass --morse-table PATH (repeatable) to extend or override the built-ins from a file. Format:

# locale: jp
# name: Japanese Wabun code (optional)
# Comments and blank lines are ignored.
# Lines: <single-char> <whitespace> <morse>      (morse uses '.' and '-')
イ .-
ロ .-.-
ハ -...
...
  • The # locale: <code> header is required and assigns the layer to a locale (any short string — jp, de, es, …).
  • Each subsequent definition of the same character — within the file or across files — overrides earlier ones (including the built-in en/ru layers, so you can patch a single letter if needed).
  • Letters are stored case-insensitively (forward_table() keys are lowercased; lookup also lowercases).

A ready example lives at examples/japanese.txt — the standard 48-katakana Wabun code plus dakuten/handakuten/long-vowel marks, transcribed from ITU-R M.1677-1 and the JARL Wabun standard. Load it with:

python -m bip39_morse --morse-table examples/japanese.txt --reverse --lang jp

Then reverse-mode output will be rendered in katakana. (The same bit pattern 01 decodes as A for --lang en, А for --lang ru, for --lang jp, and Α for --lang elbits_to_text only reads layers tagged with the requested locale.)

A second new-alphabet example: examples/greek.txt — the 24-letter Greek alphabet (Α–Ω) from ITU-R M.1677-1, section 1.1.4. Like Japanese, it's a standalone locale (# locale: el), not a layer over Latin. Load it with:

python -m bip39_morse --morse-table examples/greek.txt --reverse --lang el

Greek mirrors the Latin alphabet's 1-bit coverage: Ε (.) and Τ (-) cover the single-bit codes 0 and 1, so reverse decoding in el has the same letter-only round-trip guarantee as the built-in en/ru locales — the digit and punctuation fallbacks are never reached. The file also includes final sigma ς as an encode-only convenience entry sharing σ's code (...).

Latin accent extensions (German / French / Spanish / Polish)

Several Western-European languages don't need a separate alphabet — they just add a handful of accented letters on top of A–Z. The repository ships four ITU-R M.1677-1 extensions tagged with locale: en, so they layer onto the built-in English alphabet instead of creating a new locale:

File Adds
examples/german.txt Ä, Ö, Ü, ß
examples/french.txt À, Ç, É, È
examples/spanish.txt Ñ, Ü
examples/polish.txt Ą, Ć, Ę, Ł, Ń, Ó, Ś, Ź, Ż

You can load any combination — they don't conflict with each other (the few overlapping letters, e.g. Ü in both German and Spanish, share the same ITU code). Polish is a special case: six of its nine letters share a bit-string with another extension (ĄÄ, ĆÇ, ĘÉ, ŁÈ, ŃÑ, ÓÖ). Forward direction is unaffected; in reverse direction the first-loaded file's character wins for any shared code, so reorder the --morse-table flags if you want a particular language to dominate the decoded output.

Example: French session
python -m bip39_morse --morse-table examples/french.txt --length 12

After this, the forward-mode TUI accepts à / ç / é / è (both cases) alongside the regular A–Z. Type a French passphrase:

Слов: 12 (128 бит энтропии)
🟢 A3F2 B1C4 0911 7E2D 5C8A 6F03 D4E1 9B25 │ 1100
   ████████████████████████████████████████  132/128
›  Le café à Paris est très bon_

Press Enter:

LE CAFÉ À PARIS EST TRÈS BON
island legal forest above ... (12 words)

Because the file extends locale: en, no --lang flag is needed — --reverse still defaults to en, and the decoder may emit Ä / É / etc. where their ITU codes don't collide with a base Latin letter.

Caveat. ITU-R M.1677-1 defines official codes only for the accented letters listed in each example file. Other diacritics (e.g. Â, Ê, Î, Ô, Ï, Ë, Ù, Û in French; Á, Í, Ó, Ú in Spanish) have no standard Morse code — radio operators traditionally drop the accent. If your convention requires them, add the rows yourself; later entries override earlier ones in the merged forward table.

Bit math

For N words the BIP39 layout is:

total_bits = 11 · N
entropy_bits = total_bits · 32 / 33   (= 128, 192, or 256)
checksum_bits = entropy_bits / 32     (= 4, 6, or 8)

The TUI accepts a --length of 12/18/24 and derives entropy_bits from this table.

Forward mode — phrase → mnemonic

Implemented in bitstream.py + tui.py. The data flow on each keystroke:

  1. The character is mapped to its Morse bit string via char_to_bits.
  2. BitStream.push(char, bits) appends the bits to an internal accumulator and the displayed character to a visual buffer. Each push is also recorded on a stack so Backspace can roll the exact same bits back off.
  3. While accumulated_bits < entropy_bits, the hex view shows the completed bytes of the accumulator (groups of 4 hex chars). The indicator is red.
  4. As soon as accumulated_bits ≥ entropy_bits, the first entropy_bits bits are treated as the entropy; SHA-256(entropy) is computed and its top checksum_bits bits are appended. The hex view switches to entropy_hex │ checksum_bits and the indicator turns green. Extra characters typed after that point still appear in the visual buffer and contribute to normalization, but they are ignored for mnemonic derivation.
  5. On Enter (only while green) the entropy ‖ checksum string is split into 11-bit groups, each group indexes the wordlist, and the program prints two lines: the normalized phrase (uppercased, whitespace collapsed to single spaces) and the BIP39 mnemonic.

Pasting via Cmd/Ctrl+V iterates the pasted text and feeds each allowed character through the same push pipeline; unsupported characters are skipped with a hint.

Reverse mode — words → Morse text

Implemented in reverse.py + tui_reverse.py. Here the user types words from the BIP39 wordlist and the script renders the resulting bits as readable Morse text.

  1. Word entryWordEntry.push_char appends a character to the current partial word and checks how many wordlist entries still start with it:

    • 1 match → the word is auto-completed and committed (a space is implicit; the next char starts a new word).
    • 0 matches → the character is rejected.
    • 2+ matches → it stays in the buffer; up to 8 candidates are shown live as a hint. For words that are themselves a prefix of longer ones (e.g. van vs vanish) auto-complete cannot fire, so Space, Tab, and Enter call commit_current() to commit the exact typed word.
  2. PasteWordEntry.paste_text(text) splits the pasted string on any of space/tab/CR/LF, prepends the current partial buffer to the first token, then resolves each token as either an exact wordlist word (winning over prefix-of-others) or a unique prefix. It stops at the first ambiguous/unknown token.

  3. Bits — once N words are committed, their 11-bit indices concatenate into the same entropy ‖ checksum layout that forward mode produces.

  4. Bits → textbits_to_text(bits, locale) walks the first entropy_bits bits left-to-right. At every position it considers all letter codes that prefix the remaining bits at any length 1..6, picks the least-used letter so far (tie-break: longest code, then alphabetical), and consumes that many bits. If no letter matches at any length, the algorithm falls back to digits, then punctuation. Because letters always match at length 1, the bit string is always exhausted exactly — no padding is needed and no backtracking can occur. The least-used rule spreads the output across letters: 128 zero bits become HSIE HSIE … HSE rather than a long run of H.

  5. Display & finalize — the TUI shows the live Morse text, a hex dump (same format as forward mode, with the user-provided checksum after the once all words are entered), a progress bar N_completed / N_target, and the red/green indicator. Enter while green prints the decoded Morse text.

Round-trip

Forward and reverse are inverses with one caveat. If the user types a Morse phrase, derives a mnemonic, and then in reverse mode re-enters those words, the printed Morse text — fed back into forward mode — will reproduce exactly the same entropy, hence the same first N−1 words. The last word may differ only when the original word set did not have a valid BIP39 checksum (forward mode always recomputes the checksum, so an inconsistent input is silently corrected). This is the standard BIP39 invariant, not a quirk of this tool.

Installation

pip install -e ".[test]"

Usage

python -m bip39_morse [--length {12|18|24}] [--wordlist english|russian|<path>] [--reverse] [--morse-table PATH ...] [--lang LOCALE] [--group-size N] [--per-line N] [--ascii]

Or via the installed entry point:

bip39-morse --length 12

Options

Flag Description
--length Number of mnemonic words: 12, 18, or 24. Default: 24.
--wordlist Wordlist to use: english (default), russian, or a path to a custom 2048-word file.
--reverse Reverse mode: type BIP39 words with autocompletion, render the resulting Morse-decoded text.
--morse-table PATH Load an extra Morse alphabet from a file (see Custom Morse tables). Repeatable; later files override earlier ones (and the built-in tables) for any character defined more than once.
--lang LOCALE Output Morse alphabet for --reverse (locale code: en, ru, or any locale registered via --morse-table, e.g. jp). Default: the locale of the last loaded --morse-table file if any; otherwise auto-detected from --wordlist (russian → ru, else en).
--group-size N For --reverse output: split the printed Morse text into space-separated groups of N characters. Default: 5.
--per-line N For --reverse output: insert a newline after every N groups. If omitted, all groups are printed on a single line.
--ascii Use ANSI-colored instead of emoji indicators (for terminals without UTF-8 emoji support).

Forward mode (default)

Type a phrase → Morse bits → BIP39 mnemonic.

Keys

Key Action
Latin/Cyrillic letters, digits 0-9, ., ,, -, !, ?, :, Space Add character to phrase
Paste (Cmd/Ctrl+V) Bulk-insert text — unknown characters are skipped with a hint
Backspace Remove last character and roll back its bits
Enter Finalize and print mnemonic (only when indicator is green)
Ctrl+C Exit without output (exit code 130)

Example session

Слов: 12 (128 бит энтропии)
🟢 A3F2 B1C4 0911 7E2D 5C8A 6F03 D4E1 9B25 │ 1100
   ████████████████████████████████████████  132/128
›  Привет, мир!_

After pressing Enter:

ПРИВЕТ, МИР!
island cat forest above ... (12 words)

Reverse mode (--reverse)

Type BIP39 words (with prefix-autocomplete) → indices → bits → Morse-decoded text.

  • As you type a word, up to 8 wordlist candidates are shown (the first 8 of the wordlist before you start typing).
  • When a single candidate matches the prefix, the word is committed automatically and a space is appended.
  • For words that are themselves prefixes of other words (e.g. van vs vanish), press Space or Tab (or Enter) to commit the typed word explicitly.
  • Paste (Cmd/Ctrl+V) accepts a full mnemonic with any whitespace separators (space, tab, CR, LF). Each pasted token may be either a complete BIP39 word or a prefix that uniquely identifies one. The first pasted token is concatenated with any partial input already in the buffer. Paste stops on the first unresolvable (ambiguous or unknown) token, leaving everything before it committed.
  • A hex dump of the accumulated bits is shown live, identical in style to forward mode (entropy_hex │ checksum_bits once all words are entered).
  • The decoded Morse text is displayed live above the input.
  • After the last word, the indicator turns green; press Enter to print the text.

Output formatting: the decoded Morse text printed after Enter is split into space-separated groups of --group-size characters (default 5). If --per-line N is given, a newline is inserted after every N groups so the output lays out as a block — useful when you need to memorise or write down a long reverse-trash string and man -k chunking instinct kicks in:

$ bip39-morse --reverse --length 24 --group-size 4 --per-line 5
... (enter mnemonic) ...
ХФЬД НЯЙБ ТЮЖУ ЛЧПЗ БВЬИ
ЦГСЫ РАЕД НПКЯ ХВЖЛ МЭЦК
СЩФЩ УРЙЮ АИЪТ ЕФБГ ЬЗДЦ
ПЖКУ ВЯЙЛ ЫРСМ АИНТ ЕЯН

Without --per-line (default) the same text is printed on a single line as ХФЬДН ЯЙБТЮ ЖУЛЧП ЗБВЬИ ЦГСЫР АЕДНП …. The TUI itself always shows the live preview on one line — these flags only shape the final printed text.

Morse alphabet selection: explicit --lang <locale> takes precedence. If omitted, the default is the locale of the last loaded --morse-table file (so --morse-table examples/japanese.txt without --lang renders in katakana); if no extra tables were loaded, it falls back to auto-detection from --wordlist (russian ⇒ Cyrillic, else Latin). This lets you mix — e.g. enter a Russian mnemonic and render the Morse text in Latin alphabet, or in any custom locale you have loaded.

Decoding strategy: at each position, the algorithm picks the least-used letter so far (tie-break: longer code wins, then alphabetical). This rotates through letters instead of collapsing into long runs (e.g. 128 zero bits become HSIE HSIE … HSE rather than HHHH…). If no letter matches at any length, it falls back to digits, then punctuation. Since e/е and t/т cover the 1-bit cases, letters always match, so the output is letters-only and no backtracking is needed.

The text round-trips through forward mode: feeding it back reproduces the same first 23 words; the last word may differ if the original 24-word input did not have a valid BIP39 checksum (which is standard BIP39 behavior).

Running tests

pytest

Coverage report is printed automatically. The minimum threshold is 85% for morse, bitstream, and bip39 modules.

Project structure

bip39_morse/
  __main__.py      entry point for python -m
  cli.py           argparse + main()
  morse.py         Morse tables (layered registry) + char↔bits converters
  bitstream.py     bit accumulator, hex display, backspace, checksum
  bip39.py         wordlist loading, entropy→mnemonic
  reverse.py       word-entry model with prefix-autocomplete
  tui.py           prompt_toolkit TUI (forward mode)
  tui_reverse.py   prompt_toolkit TUI (reverse mode)
  wordlists/
    english.txt    2048-word BIP39 English wordlist
    russian.txt    2048-word BIP39 Russian wordlist
examples/
  japanese.txt    Sample Wabun (Japanese) Morse table (locale: jp)
  greek.txt       ITU-R M.1677-1 Greek alphabet (locale: el)
  german.txt      ITU-R M.1677-1 German accent extension (locale: en)
  french.txt      ITU-R M.1677-1 French accent extension (locale: en)
  spanish.txt     ITU-R M.1677-1 Spanish accent extension (locale: en)
  polish.txt      ITU-R M.1677-1 Polish accent extension (locale: en)
docs/
  generate_demo.py        Full demo pipeline (forward + reverse, SVG + PNG, watermark)
  demo.cast               asciinema v2 source for the forward (iroha → mnemonic) demo
  demo.svg                animated SVG of the forward demo
  demo-final.svg          static SVG of the forward final state
  demo-final.png          static PNG of the forward final state
  demo-reverse.cast       asciinema v2 source for the reverse (mnemonic → Morse) demo
  demo-reverse.svg        animated SVG of the reverse demo
  demo-reverse-final.svg  static SVG of the reverse final state
  demo-reverse-final.png  static PNG of the reverse final state
tests/
  test_morse.py
  test_morse_tables.py
  test_bitstream.py
  test_bip39.py
  test_reverse.py
  test_e2e.py
  vectors/
    trezor_vectors.json   official Trezor test vectors

Security notes

These are properties of the tool itself; see the security warning at the top for the much larger question of the environment you run it in.

  • No entropy is saved to disk.
  • No network calls at runtime.
  • No randomness injected — entropy comes exclusively from your typed phrase.
  • Do not share your phrase; it is the seed for your mnemonic.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bip39_morse-1.0.2.tar.gz (65.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bip39_morse-1.0.2-py3-none-any.whl (44.2 kB view details)

Uploaded Python 3

File details

Details for the file bip39_morse-1.0.2.tar.gz.

File metadata

  • Download URL: bip39_morse-1.0.2.tar.gz
  • Upload date:
  • Size: 65.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for bip39_morse-1.0.2.tar.gz
Algorithm Hash digest
SHA256 b16f05168a9d4b9daa58b2fbb87ca3e7113f6ab24e5cb1a3bc084f2e87029d53
MD5 cd152960dae069066daaa982e4dd13e4
BLAKE2b-256 dcdd93689659e846591f5eda0ed284c396c45a76704c6c13fffcda00b4cc12aa

See more details on using hashes here.

Provenance

The following attestation bundles were made for bip39_morse-1.0.2.tar.gz:

Publisher: release-pypi.yml on sftwnd/bip39-morse

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file bip39_morse-1.0.2-py3-none-any.whl.

File metadata

  • Download URL: bip39_morse-1.0.2-py3-none-any.whl
  • Upload date:
  • Size: 44.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for bip39_morse-1.0.2-py3-none-any.whl
Algorithm Hash digest
SHA256 88f3c5bf1cca98d098216d19ce32ef3b9d734965930dcd433d9cf471c6e85227
MD5 95167cd49a73138af983d2ffc241a749
BLAKE2b-256 8d97f4e79efa3ff7e3ba4f825e471593bc2f08ddbd0f8ed395cdc167a86b7118

See more details on using hashes here.

Provenance

The following attestation bundles were made for bip39_morse-1.0.2-py3-none-any.whl:

Publisher: release-pypi.yml on sftwnd/bip39-morse

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page