spicebag 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spicebag-1.0.0/LICENSE +21 -0
- spicebag-1.0.0/PKG-INFO +309 -0
- spicebag-1.0.0/README.md +276 -0
- spicebag-1.0.0/pyproject.toml +50 -0
- spicebag-1.0.0/setup.cfg +4 -0
- spicebag-1.0.0/spicebag/__init__.py +1 -0
- spicebag-1.0.0/spicebag/app/__init__.py +1 -0
- spicebag-1.0.0/spicebag/app/cli.py +70 -0
- spicebag-1.0.0/spicebag/app/handlers/__init__.py +1 -0
- spicebag-1.0.0/spicebag/app/handlers/decode.py +249 -0
- spicebag-1.0.0/spicebag/app/handlers/encode.py +555 -0
- spicebag-1.0.0/spicebag/app/handlers/savePath.py +187 -0
- spicebag-1.0.0/spicebag/app/handlers/screenshot.py +195 -0
- spicebag-1.0.0/spicebag/app/screens/__init__.py +1 -0
- spicebag-1.0.0/spicebag/app/screens/help.py +748 -0
- spicebag-1.0.0/spicebag/app/screens/main.py +1789 -0
- spicebag-1.0.0/spicebag/app/screens/warning.py +216 -0
- spicebag-1.0.0/spicebag/app/tree.py +338 -0
- spicebag-1.0.0/spicebag/app/tui.py +81 -0
- spicebag-1.0.0/spicebag/app/tui.tcss +192 -0
- spicebag-1.0.0/spicebag/app/widgets/__init__.py +1 -0
- spicebag-1.0.0/spicebag/app/widgets/optionsBar.py +216 -0
- spicebag-1.0.0/spicebag/app/widgets/secureInput.py +348 -0
- spicebag-1.0.0/spicebag/constants/__init__.py +1 -0
- spicebag-1.0.0/spicebag/constants/defaults.py +93 -0
- spicebag-1.0.0/spicebag/constants/theme.py +278 -0
- spicebag-1.0.0/spicebag/core/__init__.py +1 -0
- spicebag-1.0.0/spicebag/core/decoder.py +244 -0
- spicebag-1.0.0/spicebag/core/generator.py +400 -0
- spicebag-1.0.0/spicebag/utils/__init__.py +1 -0
- spicebag-1.0.0/spicebag/utils/colors.py +130 -0
- spicebag-1.0.0/spicebag/utils/dependencyCheck.py +64 -0
- spicebag-1.0.0/spicebag/utils/networkDetect.py +242 -0
- spicebag-1.0.0/spicebag.egg-info/PKG-INFO +309 -0
- spicebag-1.0.0/spicebag.egg-info/SOURCES.txt +37 -0
- spicebag-1.0.0/spicebag.egg-info/dependency_links.txt +1 -0
- spicebag-1.0.0/spicebag.egg-info/entry_points.txt +2 -0
- spicebag-1.0.0/spicebag.egg-info/requires.txt +7 -0
- spicebag-1.0.0/spicebag.egg-info/top_level.txt +1 -0
spicebag-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Enkhoder
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
spicebag-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spicebag
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Visual Mnemonic Encoder / Decoder
|
|
5
|
+
Author: Enkhoder
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Enkhoder/Spicebag
|
|
8
|
+
Project-URL: Repository, https://github.com/Enkhoder/Spicebag
|
|
9
|
+
Project-URL: Issues, https://github.com/Enkhoder/Spicebag/issues
|
|
10
|
+
Keywords: mnemonic,bip39,slip39,electrum,seed phrase,steganography,tui,crypto
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Security :: Cryptography
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: typer
|
|
26
|
+
Requires-Dist: rich
|
|
27
|
+
Requires-Dist: textual
|
|
28
|
+
Requires-Dist: pillow
|
|
29
|
+
Requires-Dist: mnemonic
|
|
30
|
+
Requires-Dist: shamir-mnemonic
|
|
31
|
+
Requires-Dist: argon2-cffi
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# Spicebag
|
|
35
|
+
|
|
36
|
+
**Visual Mnemonic Encoder / Decoder** by [Enkhoder](https://github.com/Enkhoder)
|
|
37
|
+
|
|
38
|
+
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 — optionally encrypted with a user-provided salt.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
- **Encode** a seed phrase into a single PNG image or **bulk-generate** multiple variants into a ZIP archive.
|
|
45
|
+
- **Decode** a color-coded PNG back into the original seed phrase, with optional `.txt` export.
|
|
46
|
+
- **Salt-based encryption** — an optional passphrase processed through **Argon2id** key derivation adds XOR masking and grid shuffling, making the image unreadable without the salt.
|
|
47
|
+
- **Multi-standard support**:
|
|
48
|
+
|
|
49
|
+
| Standard | Word Counts | Wordlist Size |
|
|
50
|
+
|----------|-------------|---------------|
|
|
51
|
+
| BIP-39 | 12, 15, 18, 21, 24 | 2048 |
|
|
52
|
+
| Electrum | 12, 24 | 2048 |
|
|
53
|
+
| SLIP-39 | 20, 33 | 1024 |
|
|
54
|
+
|
|
55
|
+
- **PNG integrity checks** — rejects images with forbidden chunks (e.g. `PLTE`, `tRNS`, `iCCP`) and verifies cell-level monochromatic consistency to detect lossy compression.
|
|
56
|
+
- **Checksum validation** on decode ensures the recovered mnemonic is valid before output.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## How It Works
|
|
61
|
+
|
|
62
|
+
1. **Word → Index** — Each seed word is looked up in its standard's wordlist.
|
|
63
|
+
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.
|
|
64
|
+
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.
|
|
65
|
+
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.
|
|
66
|
+
5. **Decoding** reverses all steps: un-permute, un-shift, un-mask, and look up the word by index.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Configuration Space
|
|
71
|
+
|
|
72
|
+
How many visually distinct images can encode the *same* seed phrase under the *same* salt?
|
|
73
|
+
|
|
74
|
+
Once the salt is fixed, almost everything is deterministic:
|
|
75
|
+
|
|
76
|
+
| Component | Source | Free? |
|
|
77
|
+
|-----------|--------|-------|
|
|
78
|
+
| Cell positions | Grid shuffled by `random.Random(permKey[:8])` | ❌ Fixed — one layout per salt |
|
|
79
|
+
| XOR mask | `deriveMask(maskKey, wordPosition, maxIdx)` | ❌ Fixed |
|
|
80
|
+
| Channel shift | `RGB_VALUE_SHIFTS[wordPosition]` | ❌ Fixed |
|
|
81
|
+
| Channel permutation | Selected by `(R+G+B) % 6` | ❌ Derived |
|
|
82
|
+
| **Block offset** | `secrets.SystemRandom().randrange(blockSize)` | ✅ **Free** |
|
|
83
|
+
|
|
84
|
+
The block offset is the only free variable. It occupies the low bits of the 24-bit value left over after the word
|
|
85
|
+
index is packed into the high bits:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
blockSize = 1 << (24 - (maxIdx.bit_length() - 1))
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
| Standard | Wordlist Size | Block Size | Colors per Word |
|
|
92
|
+
|----------|---------------|------------|-----------------|
|
|
93
|
+
| BIP-39, Electrum | 2048 | 2¹³ | 8,192 |
|
|
94
|
+
| SLIP-39 | 1024 | 2¹⁴ | 16,384 |
|
|
95
|
+
|
|
96
|
+
Every offset produces a distinct color: the channel shift is a bijective mod-256 addition, and the permutation only
|
|
97
|
+
reorders an already-distinct triple. No two offsets collide.
|
|
98
|
+
|
|
99
|
+
Since each grid holds exactly one cell per word, the total is `blockSize ^ wordCount`:
|
|
100
|
+
|
|
101
|
+
| Phrase | Entropy | Distinct Images per Salt |
|
|
102
|
+
|--------|---------|--------------------------|
|
|
103
|
+
| 12-word BIP-39 / Electrum | 156 bits | 9.13 × 10⁴⁶ |
|
|
104
|
+
| 15-word BIP-39 | 195 bits | 5.02 × 10⁵⁸ |
|
|
105
|
+
| 18-word BIP-39 | 234 bits | 2.76 × 10⁷⁰ |
|
|
106
|
+
| 20-word SLIP-39 | 280 bits | 1.94 × 10⁸⁴ |
|
|
107
|
+
| 21-word BIP-39 | 273 bits | 1.52 × 10⁸² |
|
|
108
|
+
| 24-word BIP-39 / Electrum | 312 bits | 8.34 × 10⁹³ |
|
|
109
|
+
| 33-word SLIP-39 | 462 bits | 1.19 × 10¹³⁹ |
|
|
110
|
+
|
|
111
|
+
Notes:
|
|
112
|
+
|
|
113
|
+
- **Positions across salts** — the layout dimension only opens up when the salt changes, contributing up to
|
|
114
|
+
`wordCount!` arrangements (4.79 × 10⁸ for 12 words, 6.20 × 10²³ for 24). Within a single salt it collapses to one.
|
|
115
|
+
- **Interactive preview** — the clickable color-space editor rejects any color matching the cell's current color or
|
|
116
|
+
an orthogonal neighbour, trimming at most 5 of 8,192 candidates per cell. The reduction is under 0.07%.
|
|
117
|
+
- **No salt** — the grid is left in natural reading order and all masks are zero, but the per-word color count is
|
|
118
|
+
unchanged.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Valid PNG Requirements
|
|
123
|
+
|
|
124
|
+
To successfully decode, a PNG must pass all of the following checks:
|
|
125
|
+
|
|
126
|
+
### Aspect Ratio
|
|
127
|
+
|
|
128
|
+
The image's width-to-height ratio determines the grid size and expected word count:
|
|
129
|
+
|
|
130
|
+
| Aspect Ratio (W:H) | Grid (cols × rows) | Word Count | Standard |
|
|
131
|
+
|---------------------|---------------------|------------|----------|
|
|
132
|
+
| 3:4 | 3 × 4 | 12 | BIP-39, Electrum |
|
|
133
|
+
| 3:5 | 3 × 5 | 15 | BIP-39 |
|
|
134
|
+
| 1:2 | 3 × 6 | 18 | BIP-39 |
|
|
135
|
+
| 4:5 | 4 × 5 | 20 | SLIP-39 |
|
|
136
|
+
| 3:7 | 3 × 7 | 21 | BIP-39 |
|
|
137
|
+
| 2:3 | 4 × 6 | 24 | BIP-39, Electrum |
|
|
138
|
+
| 3:11 | 3 × 11 | 33 | SLIP-39 |
|
|
139
|
+
|
|
140
|
+
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).
|
|
141
|
+
|
|
142
|
+
### PNG Chunks
|
|
143
|
+
|
|
144
|
+
| Status | Chunk Types |
|
|
145
|
+
|--------|-------------|
|
|
146
|
+
| ❌ Forbidden | `PLTE`, `tRNS`, `bKGD`, `sBIT`, `iCCP` |
|
|
147
|
+
| ✅ Allowed | Everything else, including `IHDR`, `IDAT`, `IEND`, `tIME`, `tEXt`, `iTXt`, `zTXt`, `sRGB`, `gAMA`, `pHYs` |
|
|
148
|
+
|
|
149
|
+
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.
|
|
150
|
+
|
|
151
|
+
### Color Mode
|
|
152
|
+
|
|
153
|
+
- Must be **RGB** (3 channels) or **RGBA** (4 channels) with alpha fixed at `255`.
|
|
154
|
+
- **Bit Depth**: Varying bit depths are acceptable only if they are semantically/exactly identical to standard 8-bit channels upon extraction.
|
|
155
|
+
- **Strictly Prohibited**:
|
|
156
|
+
- Custom color models or color profiles (e.g., **Adobe RGB**).
|
|
157
|
+
- **Monochromatic** / Grayscale images.
|
|
158
|
+
- All channel values must be integers in the range `0–255`.
|
|
159
|
+
|
|
160
|
+
### Cell Integrity
|
|
161
|
+
|
|
162
|
+
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.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Requirements
|
|
167
|
+
|
|
168
|
+
- Python 3.10+
|
|
169
|
+
- A terminal with 24-bit color support (Windows Terminal, iTerm2, or any modern Linux terminal)
|
|
170
|
+
|
|
171
|
+
### Dependencies
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
typer
|
|
175
|
+
rich
|
|
176
|
+
textual
|
|
177
|
+
pillow
|
|
178
|
+
mnemonic
|
|
179
|
+
shamir-mnemonic
|
|
180
|
+
argon2-cffi
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Installation
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
pip install spicebag
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
This installs the `spicebag` command on your PATH. Run it with no arguments for the TUI, or pass a
|
|
192
|
+
subcommand for one-shot use.
|
|
193
|
+
|
|
194
|
+
### From source
|
|
195
|
+
|
|
196
|
+
Clone the repository, then let the launcher build the virtual environment for you:
|
|
197
|
+
|
|
198
|
+
```bat
|
|
199
|
+
git clone https://github.com/Enkhoder/Spicebag.git
|
|
200
|
+
cd Spicebag
|
|
201
|
+
run.bat
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`run.bat` creates `.venv/`, installs `requirements.txt`, verifies dependencies, and launches the app.
|
|
205
|
+
|
|
206
|
+
To set it up by hand instead — required on Linux and macOS, where `run.bat` does not apply:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
python -m venv .venv
|
|
210
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
211
|
+
pip install -r requirements.txt
|
|
212
|
+
export PYTHONPATH=. # Windows: set PYTHONPATH=.
|
|
213
|
+
python spicebag/app/cli.py
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
All imports are absolute (`from spicebag.xxx import yyy`), so `PYTHONPATH` must include the repository
|
|
217
|
+
root. Editors that do not read `PYTHONPATH` need the repository root added to their own analysis path,
|
|
218
|
+
or you can `pip install -e .` and skip the variable entirely.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Usage
|
|
223
|
+
|
|
224
|
+
### Interactive TUI
|
|
225
|
+
|
|
226
|
+
Run with no arguments to open the full-screen interface:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
spicebag # installed via pip
|
|
230
|
+
run.bat # from a source checkout on Windows
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Type a command at the prompt:
|
|
234
|
+
|
|
235
|
+
| Command | Action |
|
|
236
|
+
|---------|--------|
|
|
237
|
+
| `encode` | Walk through encoding a seed phrase into a PNG, or bulk-generate a ZIP of variants |
|
|
238
|
+
| `decode` | Recover a seed phrase from a PNG, with optional `.txt` export |
|
|
239
|
+
| `help` | Open the in-app reference |
|
|
240
|
+
| `banner` | Toggle the ASCII banner (preference persists across sessions) |
|
|
241
|
+
| `clear` | Clear the scrollback |
|
|
242
|
+
| `exit` | Quit |
|
|
243
|
+
|
|
244
|
+
The encode flow prompts in order for word count, phrase, salt, cell size, and save path, then shows
|
|
245
|
+
a clickable color-space preview before writing the file.
|
|
246
|
+
|
|
247
|
+
### Command line
|
|
248
|
+
|
|
249
|
+
Pass a subcommand to skip the TUI entirely:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
spicebag encode "<phrase>" <path> [--salt <s>] [--cell-px <n>]
|
|
253
|
+
spicebag decode <path> [--salt <s>]
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
From a source checkout, substitute `run.bat` for `spicebag`.
|
|
257
|
+
|
|
258
|
+
| Option | Default | Applies to | Meaning |
|
|
259
|
+
|--------|---------|------------|---------|
|
|
260
|
+
| `--salt` | *(empty)* | both | Passphrase for Argon2id key derivation |
|
|
261
|
+
| `--cell-px` | `100` | encode | Pixel width of each square color cell |
|
|
262
|
+
|
|
263
|
+
Try it against the sample images in
|
|
264
|
+
[`examples/`](https://github.com/Enkhoder/Spicebag/tree/main/examples):
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
spicebag decode examples/images/12-seedless.png
|
|
268
|
+
spicebag decode examples/images/12-seed-Z3wjBTmDso1eLQ.png --salt Z3wjBTmDso1eLQ
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Output location
|
|
272
|
+
|
|
273
|
+
Images, ZIP archives, screenshots, and the banner preference file are written to `~/Spicebag/`.
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Security Considerations
|
|
278
|
+
|
|
279
|
+
> [!CAUTION]
|
|
280
|
+
> Spicebag provides **no protection** against malware, keyloggers, screen capture, coercion, or any form of surveillance. Use it only in a secure, private environment.
|
|
281
|
+
|
|
282
|
+
- **Salt** is processed with **Argon2id** (`time_cost`=4, `memory_cost`=256 MB) making brute-force infeasible.
|
|
283
|
+
- All randomness for color offsets uses Python's `secrets` module (CSPRNG).
|
|
284
|
+
- The salt is **not stored** anywhere — losing it means the image cannot be decoded.
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Performance & Bulk Generation
|
|
289
|
+
|
|
290
|
+
- **Algorithmic Separation**: Bulk image generation (`bulkEncodeMnemonic`) decouples the color space calculations from the resolution upscaling. This minimizes memory overhead during logic generation.
|
|
291
|
+
- **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.
|
|
292
|
+
- **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.
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## Contributing
|
|
297
|
+
|
|
298
|
+
[`docs/ARCHITECTURE.md`](https://github.com/Enkhoder/Spicebag/blob/main/docs/ARCHITECTURE.md)
|
|
299
|
+
maps the codebase layer by layer and documents the invariants that are not obvious from reading a
|
|
300
|
+
single file — which randomness source the block offset must use, which tables are single-sourced,
|
|
301
|
+
and where seed phrases are deliberately kept out of the log. Read it before changing anything in
|
|
302
|
+
`spicebag/core/` or `spicebag/utils/colors.py`.
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## License
|
|
307
|
+
|
|
308
|
+
This project is licensed under the
|
|
309
|
+
[MIT License](https://github.com/Enkhoder/Spicebag/blob/main/LICENSE).
|
spicebag-1.0.0/README.md
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# Spicebag
|
|
2
|
+
|
|
3
|
+
**Visual Mnemonic Encoder / Decoder** by [Enkhoder](https://github.com/Enkhoder)
|
|
4
|
+
|
|
5
|
+
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 — optionally encrypted with a user-provided salt.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Encode** a seed phrase into a single PNG image or **bulk-generate** multiple variants into a ZIP archive.
|
|
12
|
+
- **Decode** a color-coded PNG back into the original seed phrase, with optional `.txt` export.
|
|
13
|
+
- **Salt-based encryption** — an optional passphrase processed through **Argon2id** key derivation adds XOR masking and grid shuffling, making the image unreadable without the salt.
|
|
14
|
+
- **Multi-standard support**:
|
|
15
|
+
|
|
16
|
+
| Standard | Word Counts | Wordlist Size |
|
|
17
|
+
|----------|-------------|---------------|
|
|
18
|
+
| BIP-39 | 12, 15, 18, 21, 24 | 2048 |
|
|
19
|
+
| Electrum | 12, 24 | 2048 |
|
|
20
|
+
| SLIP-39 | 20, 33 | 1024 |
|
|
21
|
+
|
|
22
|
+
- **PNG integrity checks** — rejects images with forbidden chunks (e.g. `PLTE`, `tRNS`, `iCCP`) and verifies cell-level monochromatic consistency to detect lossy compression.
|
|
23
|
+
- **Checksum validation** on decode ensures the recovered mnemonic is valid before output.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## How It Works
|
|
28
|
+
|
|
29
|
+
1. **Word → Index** — Each seed word is looked up in its standard's wordlist.
|
|
30
|
+
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.
|
|
31
|
+
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.
|
|
32
|
+
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.
|
|
33
|
+
5. **Decoding** reverses all steps: un-permute, un-shift, un-mask, and look up the word by index.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Configuration Space
|
|
38
|
+
|
|
39
|
+
How many visually distinct images can encode the *same* seed phrase under the *same* salt?
|
|
40
|
+
|
|
41
|
+
Once the salt is fixed, almost everything is deterministic:
|
|
42
|
+
|
|
43
|
+
| Component | Source | Free? |
|
|
44
|
+
|-----------|--------|-------|
|
|
45
|
+
| Cell positions | Grid shuffled by `random.Random(permKey[:8])` | ❌ Fixed — one layout per salt |
|
|
46
|
+
| XOR mask | `deriveMask(maskKey, wordPosition, maxIdx)` | ❌ Fixed |
|
|
47
|
+
| Channel shift | `RGB_VALUE_SHIFTS[wordPosition]` | ❌ Fixed |
|
|
48
|
+
| Channel permutation | Selected by `(R+G+B) % 6` | ❌ Derived |
|
|
49
|
+
| **Block offset** | `secrets.SystemRandom().randrange(blockSize)` | ✅ **Free** |
|
|
50
|
+
|
|
51
|
+
The block offset is the only free variable. It occupies the low bits of the 24-bit value left over after the word
|
|
52
|
+
index is packed into the high bits:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
blockSize = 1 << (24 - (maxIdx.bit_length() - 1))
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
| Standard | Wordlist Size | Block Size | Colors per Word |
|
|
59
|
+
|----------|---------------|------------|-----------------|
|
|
60
|
+
| BIP-39, Electrum | 2048 | 2¹³ | 8,192 |
|
|
61
|
+
| SLIP-39 | 1024 | 2¹⁴ | 16,384 |
|
|
62
|
+
|
|
63
|
+
Every offset produces a distinct color: the channel shift is a bijective mod-256 addition, and the permutation only
|
|
64
|
+
reorders an already-distinct triple. No two offsets collide.
|
|
65
|
+
|
|
66
|
+
Since each grid holds exactly one cell per word, the total is `blockSize ^ wordCount`:
|
|
67
|
+
|
|
68
|
+
| Phrase | Entropy | Distinct Images per Salt |
|
|
69
|
+
|--------|---------|--------------------------|
|
|
70
|
+
| 12-word BIP-39 / Electrum | 156 bits | 9.13 × 10⁴⁶ |
|
|
71
|
+
| 15-word BIP-39 | 195 bits | 5.02 × 10⁵⁸ |
|
|
72
|
+
| 18-word BIP-39 | 234 bits | 2.76 × 10⁷⁰ |
|
|
73
|
+
| 20-word SLIP-39 | 280 bits | 1.94 × 10⁸⁴ |
|
|
74
|
+
| 21-word BIP-39 | 273 bits | 1.52 × 10⁸² |
|
|
75
|
+
| 24-word BIP-39 / Electrum | 312 bits | 8.34 × 10⁹³ |
|
|
76
|
+
| 33-word SLIP-39 | 462 bits | 1.19 × 10¹³⁹ |
|
|
77
|
+
|
|
78
|
+
Notes:
|
|
79
|
+
|
|
80
|
+
- **Positions across salts** — the layout dimension only opens up when the salt changes, contributing up to
|
|
81
|
+
`wordCount!` arrangements (4.79 × 10⁸ for 12 words, 6.20 × 10²³ for 24). Within a single salt it collapses to one.
|
|
82
|
+
- **Interactive preview** — the clickable color-space editor rejects any color matching the cell's current color or
|
|
83
|
+
an orthogonal neighbour, trimming at most 5 of 8,192 candidates per cell. The reduction is under 0.07%.
|
|
84
|
+
- **No salt** — the grid is left in natural reading order and all masks are zero, but the per-word color count is
|
|
85
|
+
unchanged.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Valid PNG Requirements
|
|
90
|
+
|
|
91
|
+
To successfully decode, a PNG must pass all of the following checks:
|
|
92
|
+
|
|
93
|
+
### Aspect Ratio
|
|
94
|
+
|
|
95
|
+
The image's width-to-height ratio determines the grid size and expected word count:
|
|
96
|
+
|
|
97
|
+
| Aspect Ratio (W:H) | Grid (cols × rows) | Word Count | Standard |
|
|
98
|
+
|---------------------|---------------------|------------|----------|
|
|
99
|
+
| 3:4 | 3 × 4 | 12 | BIP-39, Electrum |
|
|
100
|
+
| 3:5 | 3 × 5 | 15 | BIP-39 |
|
|
101
|
+
| 1:2 | 3 × 6 | 18 | BIP-39 |
|
|
102
|
+
| 4:5 | 4 × 5 | 20 | SLIP-39 |
|
|
103
|
+
| 3:7 | 3 × 7 | 21 | BIP-39 |
|
|
104
|
+
| 2:3 | 4 × 6 | 24 | BIP-39, Electrum |
|
|
105
|
+
| 3:11 | 3 × 11 | 33 | SLIP-39 |
|
|
106
|
+
|
|
107
|
+
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).
|
|
108
|
+
|
|
109
|
+
### PNG Chunks
|
|
110
|
+
|
|
111
|
+
| Status | Chunk Types |
|
|
112
|
+
|--------|-------------|
|
|
113
|
+
| ❌ Forbidden | `PLTE`, `tRNS`, `bKGD`, `sBIT`, `iCCP` |
|
|
114
|
+
| ✅ Allowed | Everything else, including `IHDR`, `IDAT`, `IEND`, `tIME`, `tEXt`, `iTXt`, `zTXt`, `sRGB`, `gAMA`, `pHYs` |
|
|
115
|
+
|
|
116
|
+
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.
|
|
117
|
+
|
|
118
|
+
### Color Mode
|
|
119
|
+
|
|
120
|
+
- Must be **RGB** (3 channels) or **RGBA** (4 channels) with alpha fixed at `255`.
|
|
121
|
+
- **Bit Depth**: Varying bit depths are acceptable only if they are semantically/exactly identical to standard 8-bit channels upon extraction.
|
|
122
|
+
- **Strictly Prohibited**:
|
|
123
|
+
- Custom color models or color profiles (e.g., **Adobe RGB**).
|
|
124
|
+
- **Monochromatic** / Grayscale images.
|
|
125
|
+
- All channel values must be integers in the range `0–255`.
|
|
126
|
+
|
|
127
|
+
### Cell Integrity
|
|
128
|
+
|
|
129
|
+
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.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Requirements
|
|
134
|
+
|
|
135
|
+
- Python 3.10+
|
|
136
|
+
- A terminal with 24-bit color support (Windows Terminal, iTerm2, or any modern Linux terminal)
|
|
137
|
+
|
|
138
|
+
### Dependencies
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
typer
|
|
142
|
+
rich
|
|
143
|
+
textual
|
|
144
|
+
pillow
|
|
145
|
+
mnemonic
|
|
146
|
+
shamir-mnemonic
|
|
147
|
+
argon2-cffi
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Installation
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
pip install spicebag
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
This installs the `spicebag` command on your PATH. Run it with no arguments for the TUI, or pass a
|
|
159
|
+
subcommand for one-shot use.
|
|
160
|
+
|
|
161
|
+
### From source
|
|
162
|
+
|
|
163
|
+
Clone the repository, then let the launcher build the virtual environment for you:
|
|
164
|
+
|
|
165
|
+
```bat
|
|
166
|
+
git clone https://github.com/Enkhoder/Spicebag.git
|
|
167
|
+
cd Spicebag
|
|
168
|
+
run.bat
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`run.bat` creates `.venv/`, installs `requirements.txt`, verifies dependencies, and launches the app.
|
|
172
|
+
|
|
173
|
+
To set it up by hand instead — required on Linux and macOS, where `run.bat` does not apply:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
python -m venv .venv
|
|
177
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
178
|
+
pip install -r requirements.txt
|
|
179
|
+
export PYTHONPATH=. # Windows: set PYTHONPATH=.
|
|
180
|
+
python spicebag/app/cli.py
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
All imports are absolute (`from spicebag.xxx import yyy`), so `PYTHONPATH` must include the repository
|
|
184
|
+
root. Editors that do not read `PYTHONPATH` need the repository root added to their own analysis path,
|
|
185
|
+
or you can `pip install -e .` and skip the variable entirely.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Usage
|
|
190
|
+
|
|
191
|
+
### Interactive TUI
|
|
192
|
+
|
|
193
|
+
Run with no arguments to open the full-screen interface:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
spicebag # installed via pip
|
|
197
|
+
run.bat # from a source checkout on Windows
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Type a command at the prompt:
|
|
201
|
+
|
|
202
|
+
| Command | Action |
|
|
203
|
+
|---------|--------|
|
|
204
|
+
| `encode` | Walk through encoding a seed phrase into a PNG, or bulk-generate a ZIP of variants |
|
|
205
|
+
| `decode` | Recover a seed phrase from a PNG, with optional `.txt` export |
|
|
206
|
+
| `help` | Open the in-app reference |
|
|
207
|
+
| `banner` | Toggle the ASCII banner (preference persists across sessions) |
|
|
208
|
+
| `clear` | Clear the scrollback |
|
|
209
|
+
| `exit` | Quit |
|
|
210
|
+
|
|
211
|
+
The encode flow prompts in order for word count, phrase, salt, cell size, and save path, then shows
|
|
212
|
+
a clickable color-space preview before writing the file.
|
|
213
|
+
|
|
214
|
+
### Command line
|
|
215
|
+
|
|
216
|
+
Pass a subcommand to skip the TUI entirely:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
spicebag encode "<phrase>" <path> [--salt <s>] [--cell-px <n>]
|
|
220
|
+
spicebag decode <path> [--salt <s>]
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
From a source checkout, substitute `run.bat` for `spicebag`.
|
|
224
|
+
|
|
225
|
+
| Option | Default | Applies to | Meaning |
|
|
226
|
+
|--------|---------|------------|---------|
|
|
227
|
+
| `--salt` | *(empty)* | both | Passphrase for Argon2id key derivation |
|
|
228
|
+
| `--cell-px` | `100` | encode | Pixel width of each square color cell |
|
|
229
|
+
|
|
230
|
+
Try it against the sample images in
|
|
231
|
+
[`examples/`](https://github.com/Enkhoder/Spicebag/tree/main/examples):
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
spicebag decode examples/images/12-seedless.png
|
|
235
|
+
spicebag decode examples/images/12-seed-Z3wjBTmDso1eLQ.png --salt Z3wjBTmDso1eLQ
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Output location
|
|
239
|
+
|
|
240
|
+
Images, ZIP archives, screenshots, and the banner preference file are written to `~/Spicebag/`.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Security Considerations
|
|
245
|
+
|
|
246
|
+
> [!CAUTION]
|
|
247
|
+
> Spicebag provides **no protection** against malware, keyloggers, screen capture, coercion, or any form of surveillance. Use it only in a secure, private environment.
|
|
248
|
+
|
|
249
|
+
- **Salt** is processed with **Argon2id** (`time_cost`=4, `memory_cost`=256 MB) making brute-force infeasible.
|
|
250
|
+
- All randomness for color offsets uses Python's `secrets` module (CSPRNG).
|
|
251
|
+
- The salt is **not stored** anywhere — losing it means the image cannot be decoded.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Performance & Bulk Generation
|
|
256
|
+
|
|
257
|
+
- **Algorithmic Separation**: Bulk image generation (`bulkEncodeMnemonic`) decouples the color space calculations from the resolution upscaling. This minimizes memory overhead during logic generation.
|
|
258
|
+
- **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.
|
|
259
|
+
- **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.
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Contributing
|
|
264
|
+
|
|
265
|
+
[`docs/ARCHITECTURE.md`](https://github.com/Enkhoder/Spicebag/blob/main/docs/ARCHITECTURE.md)
|
|
266
|
+
maps the codebase layer by layer and documents the invariants that are not obvious from reading a
|
|
267
|
+
single file — which randomness source the block offset must use, which tables are single-sourced,
|
|
268
|
+
and where seed phrases are deliberately kept out of the log. Read it before changing anything in
|
|
269
|
+
`spicebag/core/` or `spicebag/utils/colors.py`.
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## License
|
|
274
|
+
|
|
275
|
+
This project is licensed under the
|
|
276
|
+
[MIT License](https://github.com/Enkhoder/Spicebag/blob/main/LICENSE).
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "spicebag"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Visual Mnemonic Encoder / Decoder"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [{ name = "Enkhoder" }]
|
|
14
|
+
keywords = ["mnemonic", "bip39", "slip39", "electrum", "seed phrase", "steganography", "tui", "crypto"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Intended Audience :: End Users/Desktop",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Security :: Cryptography",
|
|
26
|
+
"Topic :: Utilities"
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"typer",
|
|
30
|
+
"rich",
|
|
31
|
+
"textual",
|
|
32
|
+
"pillow",
|
|
33
|
+
"mnemonic",
|
|
34
|
+
"shamir-mnemonic",
|
|
35
|
+
"argon2-cffi"
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/Enkhoder/Spicebag"
|
|
40
|
+
Repository = "https://github.com/Enkhoder/Spicebag"
|
|
41
|
+
Issues = "https://github.com/Enkhoder/Spicebag/issues"
|
|
42
|
+
|
|
43
|
+
[project.scripts]
|
|
44
|
+
spicebag = "spicebag.app.cli:app"
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.packages.find]
|
|
47
|
+
include = ["spicebag*"]
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.package-data]
|
|
50
|
+
"spicebag.app" = ["*.tcss"]
|
spicebag-1.0.0/setup.cfg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Package initialization
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Package initialization
|