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.
Files changed (39) hide show
  1. spicebag-1.0.0/LICENSE +21 -0
  2. spicebag-1.0.0/PKG-INFO +309 -0
  3. spicebag-1.0.0/README.md +276 -0
  4. spicebag-1.0.0/pyproject.toml +50 -0
  5. spicebag-1.0.0/setup.cfg +4 -0
  6. spicebag-1.0.0/spicebag/__init__.py +1 -0
  7. spicebag-1.0.0/spicebag/app/__init__.py +1 -0
  8. spicebag-1.0.0/spicebag/app/cli.py +70 -0
  9. spicebag-1.0.0/spicebag/app/handlers/__init__.py +1 -0
  10. spicebag-1.0.0/spicebag/app/handlers/decode.py +249 -0
  11. spicebag-1.0.0/spicebag/app/handlers/encode.py +555 -0
  12. spicebag-1.0.0/spicebag/app/handlers/savePath.py +187 -0
  13. spicebag-1.0.0/spicebag/app/handlers/screenshot.py +195 -0
  14. spicebag-1.0.0/spicebag/app/screens/__init__.py +1 -0
  15. spicebag-1.0.0/spicebag/app/screens/help.py +748 -0
  16. spicebag-1.0.0/spicebag/app/screens/main.py +1789 -0
  17. spicebag-1.0.0/spicebag/app/screens/warning.py +216 -0
  18. spicebag-1.0.0/spicebag/app/tree.py +338 -0
  19. spicebag-1.0.0/spicebag/app/tui.py +81 -0
  20. spicebag-1.0.0/spicebag/app/tui.tcss +192 -0
  21. spicebag-1.0.0/spicebag/app/widgets/__init__.py +1 -0
  22. spicebag-1.0.0/spicebag/app/widgets/optionsBar.py +216 -0
  23. spicebag-1.0.0/spicebag/app/widgets/secureInput.py +348 -0
  24. spicebag-1.0.0/spicebag/constants/__init__.py +1 -0
  25. spicebag-1.0.0/spicebag/constants/defaults.py +93 -0
  26. spicebag-1.0.0/spicebag/constants/theme.py +278 -0
  27. spicebag-1.0.0/spicebag/core/__init__.py +1 -0
  28. spicebag-1.0.0/spicebag/core/decoder.py +244 -0
  29. spicebag-1.0.0/spicebag/core/generator.py +400 -0
  30. spicebag-1.0.0/spicebag/utils/__init__.py +1 -0
  31. spicebag-1.0.0/spicebag/utils/colors.py +130 -0
  32. spicebag-1.0.0/spicebag/utils/dependencyCheck.py +64 -0
  33. spicebag-1.0.0/spicebag/utils/networkDetect.py +242 -0
  34. spicebag-1.0.0/spicebag.egg-info/PKG-INFO +309 -0
  35. spicebag-1.0.0/spicebag.egg-info/SOURCES.txt +37 -0
  36. spicebag-1.0.0/spicebag.egg-info/dependency_links.txt +1 -0
  37. spicebag-1.0.0/spicebag.egg-info/entry_points.txt +2 -0
  38. spicebag-1.0.0/spicebag.egg-info/requires.txt +7 -0
  39. 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.
@@ -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).
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ # Package initialization
@@ -0,0 +1 @@
1
+ # Package initialization