Metadata-Version: 2.4
Name: spicebag
Version: 2.0.0
Summary: Visual Mnemonic Encoder / Decoder
Author: Enkhoder
License-Expression: MIT
Project-URL: Homepage, https://github.com/Enkhoder/Spicebag
Project-URL: Repository, https://github.com/Enkhoder/Spicebag
Project-URL: Issues, https://github.com/Enkhoder/Spicebag/issues
Keywords: mnemonic,bip39,slip39,electrum,seed phrase,steganography,tui,crypto
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: Utilities
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: typer
Requires-Dist: rich
Requires-Dist: textual
Requires-Dist: pillow
Requires-Dist: mnemonic
Requires-Dist: shamir-mnemonic
Requires-Dist: argon2-cffi
Dynamic: license-file

<p align="center">
  <img src="docs/assets/banner.svg" alt="Spicebag">
</p>

<p align="center"><strong>Visual Mnemonic Encoder / Decoder</strong></p>

<p align="center">
  <a href="https://github.com/Enkhoder/Spicebag/stargazers"><img src="https://img.shields.io/github/stars/Enkhoder/Spicebag?style=for-the-badge&logo=github&logoColor=white&label=Stars&color=ECD251" alt="Stars"></a>
  <a href="https://github.com/Enkhoder/Spicebag/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/Enkhoder/Spicebag/ci.yml?branch=main&style=for-the-badge&logo=githubactions&logoColor=white&label=CI" alt="CI"></a>
  <a href="https://github.com/Enkhoder/Spicebag/blob/main/pyproject.toml"><img src="https://img.shields.io/badge/Python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-88A4E9?style=for-the-badge&logo=python&logoColor=white" alt="Python"></a>
  <a href="https://github.com/Enkhoder/Spicebag/blob/main/.github/workflows/ci.yml"><img src="https://img.shields.io/badge/OS-Windows%20%7C%20macOS%20%7C%20Linux-D787EF?style=for-the-badge" alt="OS"></a>
  <a href="https://github.com/Enkhoder/Spicebag/blob/main/LICENSE"><img src="https://img.shields.io/github/license/Enkhoder/Spicebag?style=for-the-badge&color=909090" alt="License"></a>
</p>

**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)

### 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 <kbd>Cmd</kbd>-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 <kbd>Ctrl</kbd>-clicking a saved
  file does nothing. It also intercepts some control keys before the app sees them, which is why the
  screenshot shortcut is <kbd>F12</kbd> 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

```bash
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:

```bat
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:

```bash
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:

```bash
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`](https://github.com/Enkhoder/Spicebag/blob/main/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](https://github.com/Enkhoder/Spicebag/blob/main/LICENSE).
