lsb-tool 2.1.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.
@@ -0,0 +1,321 @@
1
+ Metadata-Version: 2.4
2
+ Name: lsb-tool
3
+ Version: 2.1.0
4
+ Summary: Embed and extract files hidden inside PNG images using N-bit LSB steganography.
5
+ License-Expression: MIT
6
+ Keywords: steganography,lsb,png,embed,security
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.8
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Topic :: Security :: Cryptography
15
+ Classifier: Topic :: Multimedia :: Graphics
16
+ Requires-Python: >=3.8
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: Pillow>=8.0
19
+ Requires-Dist: numpy>=1.20
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=7.0; extra == "dev"
22
+
23
+ # LSB Tool
24
+
25
+ Embed files inside a PNG image or extract previously hidden files using
26
+ **N-bit LSB steganography** secured by a password.
27
+
28
+ Files are scattered across pixels in a password-derived pseudo-random order so
29
+ the hidden data is not readable by standard steganalysis tools that scan
30
+ sequential pixel runs.
31
+
32
+ ---
33
+
34
+ ## Features
35
+
36
+ | | |
37
+ |-|-|
38
+ | **All image modes** | 1-bit, 8-bit grayscale/palette, 16-bit grayscale, RGB, RGBA |
39
+ | **N-bit embedding** | Store 1–N bits per channel (N limited by channel bit-depth) |
40
+ | **Multiple files** | Embed any number of files in a single carrier image |
41
+ | **Optional filename storage** | Preserve original filenames on extraction, or omit to save header space |
42
+ | **`python -m` support** | Run as `python -m lsb_tool` or via the `lsb-tool` entry point |
43
+ | **Error codes** | Every failure exits with a distinct code — see [ERRORS.md](ERRORS.md) |
44
+
45
+ ---
46
+
47
+ ## Installation
48
+
49
+ ```bash
50
+ pip install lsb-tool
51
+ ```
52
+
53
+ Or install from source:
54
+
55
+ ```bash
56
+ git clone <repo>
57
+ cd lsb-tool
58
+ pip install -e .
59
+ ```
60
+
61
+ ### Requirements
62
+
63
+ - Python 3.8+
64
+ - Pillow ≥ 8.0
65
+ - NumPy ≥ 1.20
66
+
67
+ ---
68
+
69
+ ## Usage
70
+
71
+ The tool can be invoked in two equivalent ways:
72
+
73
+ ```bash
74
+ lsb-tool [options] # entry-point (after pip install)
75
+ python -m lsb_tool [options]
76
+ ```
77
+
78
+ ### Embed
79
+
80
+ ```bash
81
+ lsb-tool -E -i carrier.png -p password -f file1.txt file2.bin
82
+ ```
83
+
84
+ ### Extract
85
+
86
+ ```bash
87
+ lsb-tool -e -i carrier_embedded.png -p password
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Options
93
+
94
+ | Short | Long | Description |
95
+ |-------|------|-------------|
96
+ | `-i` | `--image` | Carrier image path (any PIL-supported format; output is always PNG) |
97
+ | `-p` | `--password` | Password used to derive the pixel-shuffle seed and verification hash |
98
+ | `-f` | `--files` | One or more files to embed (embed mode only) |
99
+ | `-l` | `--level` | Embedding depth — bits per channel (default `1`, max depends on image type) |
100
+ | `-n` | `--max-name-len` | Filename field size in bytes per file, 0–255 (default `0` = filenames not stored) |
101
+ | `-e` | `--extract` | Extract mode |
102
+ | `-E` | `--embed` | Embed mode |
103
+ | `-v` | `--verbose` | Print image, depth, capacity, and file diagnostics after the operation |
104
+
105
+ ---
106
+
107
+ ## Output
108
+
109
+ **Normal mode** — one line per operation:
110
+
111
+ ```
112
+ Embedded 2 file(s) → photo_embedded.png
113
+ Extracted 2 file(s): report.pdf, archive.zip
114
+ ```
115
+
116
+ **Verbose mode** (`-v` / `--verbose`):
117
+
118
+ ```
119
+ image : photo.png (RGB, 1920×1080)
120
+ depth : 2 bits/channel → 6 bits/pixel
121
+ capacity : 4,147,192 bytes
122
+ used : 12,345 bytes (0.3%)
123
+ files : report.pdf (9,000 B), archive.zip (3,345 B)
124
+ hash : 3a7f1c…e482d9
125
+ ```
126
+
127
+ ---
128
+
129
+ ## Embedding depth (`-l` / `--level`)
130
+
131
+ Higher depth stores more bits per channel, multiplying capacity roughly
132
+ proportionally, but making changes more visible to the human eye.
133
+
134
+ | Image mode | Channel bit-depth | Max `-l` value |
135
+ |------------|-------------------|----------------|
136
+ | `1` (binary) | 1 | 1 |
137
+ | `L`, `LA`, `P`, `RGB`, `RGBA` | 8 | 8 |
138
+ | `I;16` (16-bit grayscale) | 16 | 16 |
139
+ | `I` (32-bit grayscale) | 32 | 32 |
140
+
141
+ If you request a depth greater than the image supports, the tool prints a
142
+ warning and clamps to the maximum automatically — no error is raised.
143
+
144
+ ---
145
+
146
+ ## Header structure
147
+
148
+ The payload is split into two regions written into the shuffled pixel stream.
149
+
150
+ ### Region 1 — Preamble
151
+
152
+ Always written at **depth = 1** (LSB only), regardless of the `-l` value.
153
+ Occupies the first `⌈13 / channels⌉` pixels in the shuffled order.
154
+
155
+ | Bits | Field | Size |
156
+ |------|-------|------|
157
+ | 0 – 4 | Embedding depth (value of `-l`) | 5 bits |
158
+ | 5 – 12 | Filename field length (value of `-n`) | 8 bits |
159
+
160
+ The preamble pixel count by image mode:
161
+
162
+ | Image mode | Channels | Preamble pixels |
163
+ |------------|----------|-----------------|
164
+ | `1`, `L`, `P`, `I`, `I;16` | 1 | 13 |
165
+ | `LA` | 2 | 7 |
166
+ | `RGB` | 3 | 5 |
167
+ | `RGBA` | 4 | 4 |
168
+
169
+ ### Region 2 — Main data
170
+
171
+ Written at the chosen depth (`-l`) across all remaining shuffled pixels.
172
+ Layout depends on the `-n` value:
173
+
174
+ #### Without filename storage (`-n 0`, the default)
175
+
176
+ | Offset | Field | Size |
177
+ |--------|-------|------|
178
+ | 0 | Password hash (SHA-256, 64 ASCII hex chars) | 512 bits |
179
+ | 512 | Number of embedded files | 32 bits |
180
+ | 544 | File 0 size in bytes | 32 bits |
181
+ | 576 | File 1 size in bytes | 32 bits |
182
+ | … | File N-1 size in bytes | 32 bits |
183
+ | 544 + 32×N | Raw file data (all files concatenated) | sum(sizes) × 8 bits |
184
+
185
+ **Total header overhead: `512 + 32 + 32×N` bits** (`72 + 4×N` bytes for N files).
186
+
187
+ #### With filename storage (`-n K`, K > 0)
188
+
189
+ | Offset | Field | Size |
190
+ |--------|-------|------|
191
+ | 0 | Password hash (SHA-256, 64 ASCII hex chars) | 512 bits |
192
+ | 512 | Number of embedded files | 32 bits |
193
+ | 544 | File 0 size in bytes | 32 bits |
194
+ | 576 | File 0 filename (UTF-8, zero-padded to K bytes) | K × 8 bits |
195
+ | 576 + K×8 | File 1 size in bytes | 32 bits |
196
+ | … | File 1 filename, File 2 size, … | … |
197
+ | 544 + (32 + K×8)×N | Raw file data (all files concatenated) | sum(sizes) × 8 bits |
198
+
199
+ **Total header overhead: `512 + 32 + (32 + K×8)×N` bits** (`72 + (4 + K)×N` bytes for N files, K-byte name field).
200
+
201
+ #### Overhead comparison
202
+
203
+ | Flags | Header overhead (1 file) | Header overhead (N files) |
204
+ |-------|--------------------------|---------------------------|
205
+ | `-n 0` | 76 bytes | `72 + 4×N` bytes |
206
+ | `-n 16` | 92 bytes | `72 + 20×N` bytes |
207
+ | `-n 32` | 108 bytes | `72 + 36×N` bytes |
208
+ | `-n 64` | 140 bytes | `72 + 68×N` bytes |
209
+ | `-n 255` | 327 bytes | `72 + 259×N` bytes |
210
+
211
+ ---
212
+
213
+ ## Examples
214
+
215
+ ```bash
216
+ # Embed two files, no filename storage (minimal header)
217
+ lsb-tool -E -i photo.png -p hunter2 -f report.pdf archive.zip
218
+
219
+ # Extract (files are named extracted_file_0, extracted_file_1)
220
+ lsb-tool -e -i photo_embedded.png -p hunter2
221
+
222
+ # Embed with filename storage (32-byte name field) and depth 4
223
+ lsb-tool -E -i photo.png -p hunter2 -f secret.txt -l 4 -n 32
224
+
225
+ # Extract — file is restored as "secret.txt"
226
+ lsb-tool -e -i photo_embedded.png -p hunter2
227
+
228
+ # Check capacity and file details after embedding
229
+ lsb-tool -E -i photo.png -p hunter2 -f data.bin -v
230
+
231
+ # Embed multiple files into an RGBA image at depth 2, storing filenames
232
+ lsb-tool -E -i carrier.png -p "correct horse" -f a.txt b.bin c.log -l 2 -n 40
233
+ ```
234
+
235
+ ---
236
+
237
+ ## Quirks and edge cases
238
+
239
+ **Filename truncation** — The filename field is exactly `-n` bytes wide. If a
240
+ filename's UTF-8 encoding is longer than `-n` bytes, it is silently truncated
241
+ to the first `-n` bytes. Truncation mid-character produces an invalid UTF-8
242
+ sequence; the tool falls back to `extracted_file_N` for that file on
243
+ extraction. Use a `-n` value large enough to hold the longest filename you
244
+ expect (e.g. `-n 64` covers most practical names).
245
+
246
+ **Depth clamping** — Requesting `-l` greater than the image's channel
247
+ bit-depth produces a warning on stderr and continues at the clamped value.
248
+ The preamble always stores the actual depth used, so extraction is
249
+ self-consistent.
250
+
251
+ **Key derivation time** — The password hash is computed with `width × height`
252
+ SHA-256 iterations. For large images (e.g. 4K: ~8 million rounds) this can
253
+ take several seconds with no progress output.
254
+
255
+ **Image mode conversion** — If the input image is in an unsupported mode it is
256
+ converted to RGBA before embedding. The conversion is applied only to the
257
+ working copy; the original file on disk is not modified.
258
+
259
+ **Output is always PNG** — The tool saves `<name>_embedded.png` regardless of
260
+ the input format. This is deliberate: lossy formats (JPEG, WebP) alter pixel
261
+ values after saving and destroy the hidden data. Always use the PNG output as
262
+ the carrier for extraction.
263
+
264
+ **Passwords are case-sensitive** — `Hunter2` and `hunter2` produce entirely
265
+ different shuffle orders and hashes.
266
+
267
+ **Per-file size limit** — Each file's size is stored as a 32-bit unsigned
268
+ integer, capping individual files at 4 GiB. Total payload is limited by image
269
+ capacity.
270
+
271
+ ---
272
+
273
+ ## Image format note
274
+
275
+ Always use a lossless format (PNG, BMP, TIFF) for the carrier. Lossy formats
276
+ (JPEG, WebP) alter pixel values after saving and will destroy the hidden data.
277
+ The tool always produces PNG output regardless of the input format.
278
+
279
+ ---
280
+
281
+ ## Running tests
282
+
283
+ ```bash
284
+ pip install -e ".[dev]"
285
+ pytest tests/ -v
286
+ ```
287
+
288
+ ---
289
+
290
+ ## Error codes
291
+
292
+ All errors exit with a specific code and print `[Exx] …` to stderr.
293
+ See [ERRORS.md](ERRORS.md) for the full reference.
294
+
295
+ | Code | Meaning |
296
+ |------|---------|
297
+ | E02 | No mode selected (`-E`/`--embed` or `-e`/`--extract` required) |
298
+ | E03 | Carrier image not found |
299
+ | E04 | A file to embed was not found |
300
+ | E05 | Files too large for this image at the chosen depth |
301
+ | E06 | Wrong password or no embedded data |
302
+ | E07 | Image file is corrupt or unsupported |
303
+ | E08 | Cannot write an output file |
304
+ | E10 | Internal error (bug) |
305
+
306
+ ---
307
+
308
+ ## How it works
309
+
310
+ 1. **Key derivation** — The password and image dimensions are hashed (SHA-256,
311
+ iterated `width × height` times) to produce a 64-character hex digest.
312
+ 2. **Pixel shuffle** — A NumPy PCG64 RNG seeded from the digest shuffles all
313
+ pixel coordinates into a pseudo-random order.
314
+ 3. **Preamble** — The first `⌈13/channels⌉` shuffled pixels store a 13-bit
315
+ header at depth=1: 5 bits for the embedding depth, 8 bits for the
316
+ filename-field length.
317
+ 4. **Main data** — Remaining shuffled pixels carry the payload at the chosen
318
+ depth. Bit `k` maps to: `pixel = k ÷ (channels × depth)`,
319
+ `channel = (k ÷ depth) mod channels`, `bit_pos = k mod depth`.
320
+ 5. **Verification** — The first 512 bits of the main region store the
321
+ password hash as ASCII hex. Extraction fails with E06 if it does not match.
@@ -0,0 +1,299 @@
1
+ # LSB Tool
2
+
3
+ Embed files inside a PNG image or extract previously hidden files using
4
+ **N-bit LSB steganography** secured by a password.
5
+
6
+ Files are scattered across pixels in a password-derived pseudo-random order so
7
+ the hidden data is not readable by standard steganalysis tools that scan
8
+ sequential pixel runs.
9
+
10
+ ---
11
+
12
+ ## Features
13
+
14
+ | | |
15
+ |-|-|
16
+ | **All image modes** | 1-bit, 8-bit grayscale/palette, 16-bit grayscale, RGB, RGBA |
17
+ | **N-bit embedding** | Store 1–N bits per channel (N limited by channel bit-depth) |
18
+ | **Multiple files** | Embed any number of files in a single carrier image |
19
+ | **Optional filename storage** | Preserve original filenames on extraction, or omit to save header space |
20
+ | **`python -m` support** | Run as `python -m lsb_tool` or via the `lsb-tool` entry point |
21
+ | **Error codes** | Every failure exits with a distinct code — see [ERRORS.md](ERRORS.md) |
22
+
23
+ ---
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install lsb-tool
29
+ ```
30
+
31
+ Or install from source:
32
+
33
+ ```bash
34
+ git clone <repo>
35
+ cd lsb-tool
36
+ pip install -e .
37
+ ```
38
+
39
+ ### Requirements
40
+
41
+ - Python 3.8+
42
+ - Pillow ≥ 8.0
43
+ - NumPy ≥ 1.20
44
+
45
+ ---
46
+
47
+ ## Usage
48
+
49
+ The tool can be invoked in two equivalent ways:
50
+
51
+ ```bash
52
+ lsb-tool [options] # entry-point (after pip install)
53
+ python -m lsb_tool [options]
54
+ ```
55
+
56
+ ### Embed
57
+
58
+ ```bash
59
+ lsb-tool -E -i carrier.png -p password -f file1.txt file2.bin
60
+ ```
61
+
62
+ ### Extract
63
+
64
+ ```bash
65
+ lsb-tool -e -i carrier_embedded.png -p password
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Options
71
+
72
+ | Short | Long | Description |
73
+ |-------|------|-------------|
74
+ | `-i` | `--image` | Carrier image path (any PIL-supported format; output is always PNG) |
75
+ | `-p` | `--password` | Password used to derive the pixel-shuffle seed and verification hash |
76
+ | `-f` | `--files` | One or more files to embed (embed mode only) |
77
+ | `-l` | `--level` | Embedding depth — bits per channel (default `1`, max depends on image type) |
78
+ | `-n` | `--max-name-len` | Filename field size in bytes per file, 0–255 (default `0` = filenames not stored) |
79
+ | `-e` | `--extract` | Extract mode |
80
+ | `-E` | `--embed` | Embed mode |
81
+ | `-v` | `--verbose` | Print image, depth, capacity, and file diagnostics after the operation |
82
+
83
+ ---
84
+
85
+ ## Output
86
+
87
+ **Normal mode** — one line per operation:
88
+
89
+ ```
90
+ Embedded 2 file(s) → photo_embedded.png
91
+ Extracted 2 file(s): report.pdf, archive.zip
92
+ ```
93
+
94
+ **Verbose mode** (`-v` / `--verbose`):
95
+
96
+ ```
97
+ image : photo.png (RGB, 1920×1080)
98
+ depth : 2 bits/channel → 6 bits/pixel
99
+ capacity : 4,147,192 bytes
100
+ used : 12,345 bytes (0.3%)
101
+ files : report.pdf (9,000 B), archive.zip (3,345 B)
102
+ hash : 3a7f1c…e482d9
103
+ ```
104
+
105
+ ---
106
+
107
+ ## Embedding depth (`-l` / `--level`)
108
+
109
+ Higher depth stores more bits per channel, multiplying capacity roughly
110
+ proportionally, but making changes more visible to the human eye.
111
+
112
+ | Image mode | Channel bit-depth | Max `-l` value |
113
+ |------------|-------------------|----------------|
114
+ | `1` (binary) | 1 | 1 |
115
+ | `L`, `LA`, `P`, `RGB`, `RGBA` | 8 | 8 |
116
+ | `I;16` (16-bit grayscale) | 16 | 16 |
117
+ | `I` (32-bit grayscale) | 32 | 32 |
118
+
119
+ If you request a depth greater than the image supports, the tool prints a
120
+ warning and clamps to the maximum automatically — no error is raised.
121
+
122
+ ---
123
+
124
+ ## Header structure
125
+
126
+ The payload is split into two regions written into the shuffled pixel stream.
127
+
128
+ ### Region 1 — Preamble
129
+
130
+ Always written at **depth = 1** (LSB only), regardless of the `-l` value.
131
+ Occupies the first `⌈13 / channels⌉` pixels in the shuffled order.
132
+
133
+ | Bits | Field | Size |
134
+ |------|-------|------|
135
+ | 0 – 4 | Embedding depth (value of `-l`) | 5 bits |
136
+ | 5 – 12 | Filename field length (value of `-n`) | 8 bits |
137
+
138
+ The preamble pixel count by image mode:
139
+
140
+ | Image mode | Channels | Preamble pixels |
141
+ |------------|----------|-----------------|
142
+ | `1`, `L`, `P`, `I`, `I;16` | 1 | 13 |
143
+ | `LA` | 2 | 7 |
144
+ | `RGB` | 3 | 5 |
145
+ | `RGBA` | 4 | 4 |
146
+
147
+ ### Region 2 — Main data
148
+
149
+ Written at the chosen depth (`-l`) across all remaining shuffled pixels.
150
+ Layout depends on the `-n` value:
151
+
152
+ #### Without filename storage (`-n 0`, the default)
153
+
154
+ | Offset | Field | Size |
155
+ |--------|-------|------|
156
+ | 0 | Password hash (SHA-256, 64 ASCII hex chars) | 512 bits |
157
+ | 512 | Number of embedded files | 32 bits |
158
+ | 544 | File 0 size in bytes | 32 bits |
159
+ | 576 | File 1 size in bytes | 32 bits |
160
+ | … | File N-1 size in bytes | 32 bits |
161
+ | 544 + 32×N | Raw file data (all files concatenated) | sum(sizes) × 8 bits |
162
+
163
+ **Total header overhead: `512 + 32 + 32×N` bits** (`72 + 4×N` bytes for N files).
164
+
165
+ #### With filename storage (`-n K`, K > 0)
166
+
167
+ | Offset | Field | Size |
168
+ |--------|-------|------|
169
+ | 0 | Password hash (SHA-256, 64 ASCII hex chars) | 512 bits |
170
+ | 512 | Number of embedded files | 32 bits |
171
+ | 544 | File 0 size in bytes | 32 bits |
172
+ | 576 | File 0 filename (UTF-8, zero-padded to K bytes) | K × 8 bits |
173
+ | 576 + K×8 | File 1 size in bytes | 32 bits |
174
+ | … | File 1 filename, File 2 size, … | … |
175
+ | 544 + (32 + K×8)×N | Raw file data (all files concatenated) | sum(sizes) × 8 bits |
176
+
177
+ **Total header overhead: `512 + 32 + (32 + K×8)×N` bits** (`72 + (4 + K)×N` bytes for N files, K-byte name field).
178
+
179
+ #### Overhead comparison
180
+
181
+ | Flags | Header overhead (1 file) | Header overhead (N files) |
182
+ |-------|--------------------------|---------------------------|
183
+ | `-n 0` | 76 bytes | `72 + 4×N` bytes |
184
+ | `-n 16` | 92 bytes | `72 + 20×N` bytes |
185
+ | `-n 32` | 108 bytes | `72 + 36×N` bytes |
186
+ | `-n 64` | 140 bytes | `72 + 68×N` bytes |
187
+ | `-n 255` | 327 bytes | `72 + 259×N` bytes |
188
+
189
+ ---
190
+
191
+ ## Examples
192
+
193
+ ```bash
194
+ # Embed two files, no filename storage (minimal header)
195
+ lsb-tool -E -i photo.png -p hunter2 -f report.pdf archive.zip
196
+
197
+ # Extract (files are named extracted_file_0, extracted_file_1)
198
+ lsb-tool -e -i photo_embedded.png -p hunter2
199
+
200
+ # Embed with filename storage (32-byte name field) and depth 4
201
+ lsb-tool -E -i photo.png -p hunter2 -f secret.txt -l 4 -n 32
202
+
203
+ # Extract — file is restored as "secret.txt"
204
+ lsb-tool -e -i photo_embedded.png -p hunter2
205
+
206
+ # Check capacity and file details after embedding
207
+ lsb-tool -E -i photo.png -p hunter2 -f data.bin -v
208
+
209
+ # Embed multiple files into an RGBA image at depth 2, storing filenames
210
+ lsb-tool -E -i carrier.png -p "correct horse" -f a.txt b.bin c.log -l 2 -n 40
211
+ ```
212
+
213
+ ---
214
+
215
+ ## Quirks and edge cases
216
+
217
+ **Filename truncation** — The filename field is exactly `-n` bytes wide. If a
218
+ filename's UTF-8 encoding is longer than `-n` bytes, it is silently truncated
219
+ to the first `-n` bytes. Truncation mid-character produces an invalid UTF-8
220
+ sequence; the tool falls back to `extracted_file_N` for that file on
221
+ extraction. Use a `-n` value large enough to hold the longest filename you
222
+ expect (e.g. `-n 64` covers most practical names).
223
+
224
+ **Depth clamping** — Requesting `-l` greater than the image's channel
225
+ bit-depth produces a warning on stderr and continues at the clamped value.
226
+ The preamble always stores the actual depth used, so extraction is
227
+ self-consistent.
228
+
229
+ **Key derivation time** — The password hash is computed with `width × height`
230
+ SHA-256 iterations. For large images (e.g. 4K: ~8 million rounds) this can
231
+ take several seconds with no progress output.
232
+
233
+ **Image mode conversion** — If the input image is in an unsupported mode it is
234
+ converted to RGBA before embedding. The conversion is applied only to the
235
+ working copy; the original file on disk is not modified.
236
+
237
+ **Output is always PNG** — The tool saves `<name>_embedded.png` regardless of
238
+ the input format. This is deliberate: lossy formats (JPEG, WebP) alter pixel
239
+ values after saving and destroy the hidden data. Always use the PNG output as
240
+ the carrier for extraction.
241
+
242
+ **Passwords are case-sensitive** — `Hunter2` and `hunter2` produce entirely
243
+ different shuffle orders and hashes.
244
+
245
+ **Per-file size limit** — Each file's size is stored as a 32-bit unsigned
246
+ integer, capping individual files at 4 GiB. Total payload is limited by image
247
+ capacity.
248
+
249
+ ---
250
+
251
+ ## Image format note
252
+
253
+ Always use a lossless format (PNG, BMP, TIFF) for the carrier. Lossy formats
254
+ (JPEG, WebP) alter pixel values after saving and will destroy the hidden data.
255
+ The tool always produces PNG output regardless of the input format.
256
+
257
+ ---
258
+
259
+ ## Running tests
260
+
261
+ ```bash
262
+ pip install -e ".[dev]"
263
+ pytest tests/ -v
264
+ ```
265
+
266
+ ---
267
+
268
+ ## Error codes
269
+
270
+ All errors exit with a specific code and print `[Exx] …` to stderr.
271
+ See [ERRORS.md](ERRORS.md) for the full reference.
272
+
273
+ | Code | Meaning |
274
+ |------|---------|
275
+ | E02 | No mode selected (`-E`/`--embed` or `-e`/`--extract` required) |
276
+ | E03 | Carrier image not found |
277
+ | E04 | A file to embed was not found |
278
+ | E05 | Files too large for this image at the chosen depth |
279
+ | E06 | Wrong password or no embedded data |
280
+ | E07 | Image file is corrupt or unsupported |
281
+ | E08 | Cannot write an output file |
282
+ | E10 | Internal error (bug) |
283
+
284
+ ---
285
+
286
+ ## How it works
287
+
288
+ 1. **Key derivation** — The password and image dimensions are hashed (SHA-256,
289
+ iterated `width × height` times) to produce a 64-character hex digest.
290
+ 2. **Pixel shuffle** — A NumPy PCG64 RNG seeded from the digest shuffles all
291
+ pixel coordinates into a pseudo-random order.
292
+ 3. **Preamble** — The first `⌈13/channels⌉` shuffled pixels store a 13-bit
293
+ header at depth=1: 5 bits for the embedding depth, 8 bits for the
294
+ filename-field length.
295
+ 4. **Main data** — Remaining shuffled pixels carry the payload at the chosen
296
+ depth. Bit `k` maps to: `pixel = k ÷ (channels × depth)`,
297
+ `channel = (k ÷ depth) mod channels`, `bit_pos = k mod depth`.
298
+ 5. **Verification** — The first 512 bits of the main region store the
299
+ password hash as ASCII hex. Extraction fails with E06 if it does not match.
File without changes