Skip to main content

Spicebag

Visual Mnemonic Encoder / Decoder

Stars CI Python OS License

Spicebag encodes cryptocurrency wallet seed phrases into color-coded PNG images and decodes them back. Each word in the mnemonic maps to a unique RGB color cell, producing a compact grid image that visually represents the seed, and can be optionally encrypted with a user-provided salt.


Features

  • Encode a seed phrase into a single PNG image or bulk-generate multiple variants into a ZIP archive.

  • Decode a color-coded PNG back into the original seed phrase.

  • Salt-based encryption: an optional passphrase processed through Argon2id key derivation adds XOR masking and grid shuffling, making the image unreadable without the salt.

  • Multi-standard support:

    Standard Word Counts Wordlist Size
    BIP-39 12, 15, 18, 21, 24 2048
    Electrum 12, 24 2048
    SLIP-39 20, 33 1024
  • PNG integrity checks: rejects images with forbidden chunks (e.g. PLTE, tRNS, iCCP) and verifies cell-level monochromatic consistency to detect lossy compression.

  • Checksum validation on decode ensures the recovered mnemonic is valid before output.


How It Works

  1. Word → Index: Each seed word is looked up in its standard's wordlist.
  2. XOR Masking: If a salt is provided, an Argon2id-derived master key is expanded via HKDF into subkeys. A per-word mask is computed and XORed with the word index.
  3. Index → RGB: The masked index is packed into a 24-bit value, split into R/G/B channels, shifted by a per-word constant, and the channels are permuted.
  4. Grid Layout: Each color fills a square cell in a grid whose dimensions match the word count (e.g. 3×4 for 12 words, 4×6 for 24 words). When a salt is used, cell positions are deterministically shuffled.
  5. Decoding: reverses all steps: un-permute, un-shift, un-mask, and look up the word by index.

Configuration Space

How many visually distinct images can encode the same seed phrase under the same salt?

Once the salt is fixed, almost everything is deterministic:

Component Source Free?
Cell positions Grid shuffled by random.Random(permKey[:8]) ❌ Fixed, one layout per salt
XOR mask deriveMask(maskKey, wordPosition, maxIdx) ❌ Fixed
Channel shift RGB_VALUE_SHIFTS[wordPosition] ❌ Fixed
Channel permutation Selected by (R+G+B) % 6 ❌ Derived
Block offset secrets.SystemRandom().randrange(blockSize) ✅ Free

The block offset is the only free variable. It occupies the low bits of the 24-bit value left over after the word index is packed into the high bits:

blockSize = 1 << (24 - (maxIdx.bit_length() - 1))
Standard Wordlist Size Block Size Colors per Word
BIP-39, Electrum 2048 2¹³ 8,192
SLIP-39 1024 2¹⁴ 16,384

Every offset produces a distinct color: the channel shift is a bijective mod-256 addition, and the permutation only reorders an already-distinct triple. No two offsets collide.

Since each grid holds exactly one cell per word, the total is blockSize ^ wordCount:

Phrase Entropy Distinct Images per Salt
12-word BIP-39 / Electrum 156 bits 9.13 × 10⁴⁶
15-word BIP-39 195 bits 5.02 × 10⁵⁸
18-word BIP-39 234 bits 2.76 × 10⁷⁰
20-word SLIP-39 280 bits 1.94 × 10⁸⁴
21-word BIP-39 273 bits 1.52 × 10⁸²
24-word BIP-39 / Electrum 312 bits 8.34 × 10⁹³
33-word SLIP-39 462 bits 1.19 × 10¹³⁹

Notes:

  • Positions across salts: the layout dimension only opens up when the salt changes, contributing up to wordCount! arrangements (4.79 × 10⁸ for 12 words, 6.20 × 10²³ for 24). Within a single salt it collapses to one.
  • Interactive preview: the clickable color-space editor rejects any color matching the cell's current color or an orthogonal neighbor, trimming at most 5 of 8,192 candidates per cell. The reduction is under 0.07%.
  • No salt: the grid is left in natural reading order and all masks are zero, but the per-word color count is unchanged.

Valid PNG Requirements

To successfully decode, a PNG must pass all of the following checks:

Aspect Ratio

The image's width-to-height ratio determines the grid size and expected word count:

Aspect Ratio (W:H) Grid (cols × rows) Word Count Standard
3:4 3 × 4 12 BIP-39, Electrum
3:5 3 × 5 15 BIP-39
1:2 3 × 6 18 BIP-39
4:5 4 × 5 20 SLIP-39
3:7 3 × 7 21 BIP-39
2:3 4 × 6 24 BIP-39, Electrum
3:11 3 × 11 33 SLIP-39

Image dimensions must be evenly divisible by their grid's column and row count (i.e. every cell must be the same whole-pixel size).

PNG Chunks

Status Chunk Types
❌ Forbidden PLTE, tRNS, bKGD, sBIT, iCCP
✅ Allowed Everything else, including IHDR, IDAT, IEND, tIME, tEXt, iTXt, zTXt, sRGB, gAMA, pHYs

Validation is a blocklist, not an allowlist: any forbidden chunk causes immediate rejection, and every other chunk type passes. This ensures the image uses a direct RGB color model with no palette, transparency, or embedded ICC profile.

Color Mode

  • Must be RGB (3 channels) or RGBA (4 channels) with alpha fixed at 255.
  • Bit Depth: Varying bit depths are acceptable only if they are semantically/exactly identical to standard 8-bit channels upon extraction.
  • Strictly Prohibited:
    • Custom color models or color profiles (e.g., Adobe RGB).
    • Monochromatic / Grayscale images.
  • All channel values must be integers in the range 0–255.

Cell Integrity

Every pixel within a single grid cell must be exactly the same color. Any variation (e.g. from JPEG re-compression, anti-aliasing, or screenshot artifacts) will fail validation.


Requirements

  • Python 3.10+
  • A terminal with 24-bit color support. See Terminal

Terminal

Spicebag draws a full-screen interface and leans on four terminal capabilities. Each one degrades on its own, so a terminal missing some of them still runs the app:

Capability Used for Without it
24-bit color Color-space grids, seed cells, gradients Colors quantize to 256 and distinct cells can look identical
Mouse reporting Clicking a sample cell to reroll it, hover states Unreachable; keyboard still works
OSC 8 hyperlinks Ctrl/Cmd-clicking a saved PNG or ZIP to open it Filenames print as plain text; browse to ~/Spicebag
OSC 10/11/4 queries Screenshots that match your real terminal colors Screenshots fall back to a fixed dark palette

Every terminal below covers all four. The versions listed are where the full set is reliably present, not the oldest build that runs the app at all.

OS Terminal Minimum
Windows Windows Terminal 1.18
macOS Ghostty 1.0
macOS iTerm2 3.4
Linux Ghostty 1.0
Linux Kitty 0.21
Linux Konsole 20.04
Linux GNOME Terminal VTE 0.50
Any WezTerm recent stable
Any Alacritty 0.12

On Windows, 1.18 is the release where Windows Terminal began answering palette queries. Earlier builds render identically but export screenshots against the fallback palette.

Known limitations. Spicebag still runs on all of these. They cost comfort, not function:

  • macOS Terminal.app caps out at 256 colors. Gradients band and neighboring cells can render as the same color, which matters because cell colors carry the encoded data. It also does not implement OSC 8, so saved files are not clickable: Terminal.app linkifies literal URLs for Cmd-click, but Spicebag shows the filename with the file:// target behind it, leaving no visible URL to detect. Use one of the macOS entries above instead.
  • Windows legacy console host (conhost.exe) handles 24-bit color but answers no palette queries, so screenshots use the fallback. It has no OSC 8 support either, so Ctrl-clicking a saved file does nothing. It also intercepts some control keys before the app sees them, which is why the screenshot shortcut is F12 rather than a Ctrl combination.
  • tmux and screen hide palette queries from the terminal underneath and need explicit configuration for 24-bit color. Under tmux, set terminal-features for your terminal and enable allow-passthrough.

Block glyphs (█ ▀ ▂ ░) and box-drawing characters are used throughout, so pick a monospace font that includes the Block Elements range. Cascadia Code, JetBrains Mono, Fira Code, and any Nerd Font patch all qualify.

Dependencies

typer
rich
textual
pillow
mnemonic
shamir-mnemonic
argon2-cffi

Installation

pip install spicebag

This installs the spicebag command on your PATH. Run it with no arguments to open the TUI, which is the only interface to encoding and decoding.

From source

Clone the repository, then let the launcher build the virtual environment for you:

git clone https://github.com/Enkhoder/Spicebag.git
cd Spicebag
run.bat

run.bat creates .venv/, installs requirements.txt, verifies dependencies, and launches the app.

To set it up on Linux and macOS, where run.bat does not apply:

python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install -r requirements.txt
export PYTHONPATH=.              # Windows: set PYTHONPATH=.
python spicebag/app/cli.py

All imports are absolute (from spicebag.xxx import yyy), so PYTHONPATH must include the repository root. Editors that do not read PYTHONPATH need the repository root added to their own analysis path, or you can pip install -e . and skip the variable entirely.


Usage

Interactive TUI

Run with no arguments to open the full-screen interface:

spicebag            # installed via pip
run.bat             # from a source checkout on Windows

Type a command at the prompt:

Command Action
encode Walk through encoding a seed phrase into a PNG, or bulk-generate a ZIP of variants
decode Recover a seed phrase from a PNG, with optional .txt export
help Open the in-app reference
banner Toggle the ASCII banner (preference persists across sessions)
clear Clear the scrollback
exit Quit

The encode flow prompts in order for word count, phrase, salt, cell size, and save path, then shows a clickable color-space preview before writing the file.

Output location

Images, ZIP archives, screenshots, and the banner preference file are written to ~/Spicebag/.


Security Considerations

[!CAUTION] Spicebag provides no protection against malware, keyloggers, screen capture, coercion, or any form of surveillance. Use it only in a secure, private environment.

  • Salt is processed with Argon2id (time_cost=4, memory_cost=256 MB) making brute-force infeasible.
  • All randomness for color offsets uses Python's secrets module (CSPRNG).
  • The salt is not stored anywhere. Losing it means the image cannot be decoded.

Performance & Bulk Generation

  • Algorithmic Separation: Bulk image generation (bulkEncodeMnemonic) decouples the color space calculations from the resolution upscaling. This minimizes memory overhead during logic generation.
  • Strictly Unique Colors: Offsets for cells are sampled without replacement using secrets.SystemRandom().sample (out of the 8192 available colors for BIP-39) to mathematically guarantee that all bulk-generated images use unique cell colors.
  • Optimized PNG Encoding: High-resolution cell scaling (e.g. 2000px per cell) processes massive amounts of pixel data. The bulk encoder uses a fast compression level (compress_level=1) to yield a ~43% execution speedup, dropping bulk generation times significantly.

Contributing

docs/ARCHITECTURE.md maps the codebase layer-by-layer and documents the invariants that are not obvious from reading a single file, such as randomness source, single-sourced tables, and seed phrase handling in logs. Read it before changing anything in spicebag/core/ or spicebag/utils/colors.py.


License

This project is licensed under the MIT License.

Metadata

Release files for spicebag 2.0.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 spicebag 2.0.0
File Size Uploaded
spicebag-2.0.0.tar.gz 68.1 kB Details

Built distribution (wheel)

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

Total release size: 139.6 kB

Release files / spicebag-2.0.0.tar.gz

Download URL spicebag-2.0.0.tar.gz
Size 68.1 kB
Tags Source
SHA-256 checksum
How to use checksums
cf054f36e9d322205a69e7123aa8643a89358a386b25ef68df59d5e52c4eedd4
BLAKE2b-256 checksum
How to use checksums
c75548ed21b2ca69813eee28a05031677cad2f8ebc19c7c11d625e575d4e9274
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 17, 2026.

Transparency log

Release files / spicebag-2.0.0-py3-none-any.whl

Download URL spicebag-2.0.0-py3-none-any.whl
Size 71.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
892192ff3c1591b391950d31c3c2e3b63f306b046ae34206bec156e9deb9ac29
BLAKE2b-256 checksum
How to use checksums
5b216b268615acd574af10eee334c63ff17eea1837d2f2891e241605676077d7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 17, 2026.

Transparency log

Release history Release notifications | RSS feed

4.0.1

2 release files

4.0.0

2 release files

3.0.0

2 release files

This release

2.0.0 This release

2 release files

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