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.
- lsb_tool-2.1.0/PKG-INFO +321 -0
- lsb_tool-2.1.0/README.md +299 -0
- lsb_tool-2.1.0/lsb_tool/__init__.py +0 -0
- lsb_tool-2.1.0/lsb_tool/__main__.py +103 -0
- lsb_tool-2.1.0/lsb_tool/util/__init__.py +0 -0
- lsb_tool-2.1.0/lsb_tool/util/container.py +196 -0
- lsb_tool-2.1.0/lsb_tool/util/errors.py +35 -0
- lsb_tool-2.1.0/lsb_tool/util/security.py +61 -0
- lsb_tool-2.1.0/lsb_tool/util/utils.py +372 -0
- lsb_tool-2.1.0/lsb_tool.egg-info/PKG-INFO +321 -0
- lsb_tool-2.1.0/lsb_tool.egg-info/SOURCES.txt +16 -0
- lsb_tool-2.1.0/lsb_tool.egg-info/dependency_links.txt +1 -0
- lsb_tool-2.1.0/lsb_tool.egg-info/entry_points.txt +2 -0
- lsb_tool-2.1.0/lsb_tool.egg-info/requires.txt +5 -0
- lsb_tool-2.1.0/lsb_tool.egg-info/top_level.txt +1 -0
- lsb_tool-2.1.0/pyproject.toml +42 -0
- lsb_tool-2.1.0/setup.cfg +4 -0
- lsb_tool-2.1.0/tests/test_lsb.py +604 -0
lsb_tool-2.1.0/PKG-INFO
ADDED
|
@@ -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.
|
lsb_tool-2.1.0/README.md
ADDED
|
@@ -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
|