fsteg 0.1.0__py3-none-any.whl
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.
- fsteg/__init__.py +3 -0
- fsteg/main.py +318 -0
- fsteg-0.1.0.dist-info/METADATA +512 -0
- fsteg-0.1.0.dist-info/RECORD +7 -0
- fsteg-0.1.0.dist-info/WHEEL +4 -0
- fsteg-0.1.0.dist-info/entry_points.txt +2 -0
- fsteg-0.1.0.dist-info/licenses/LICENSE +21 -0
fsteg/__init__.py
ADDED
fsteg/main.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
FFT Block Steganography — hide text inside images
|
|
4
|
+
==================================================
|
|
5
|
+
Hides or extracts a UTF-8 text message by quantising the magnitudes of
|
|
6
|
+
mid-frequency DFT coefficients inside every non-overlapping 8×8 block of
|
|
7
|
+
the image's luma (Y) channel.
|
|
8
|
+
|
|
9
|
+
Key features
|
|
10
|
+
------------
|
|
11
|
+
* Self-correcting embed: each block is verified through a uint8 round-trip
|
|
12
|
+
so decoded bits always match, even after saving as PNG.
|
|
13
|
+
* CRC-32 integrity check on extraction.
|
|
14
|
+
* 4-byte length prefix + 8-byte sentinel so extraction never needs to
|
|
15
|
+
know the message length in advance.
|
|
16
|
+
* Works on PNG, BMP, TIFF (lossless formats). JPEG is not recommended
|
|
17
|
+
because re-encoding breaks the embedded bits.
|
|
18
|
+
|
|
19
|
+
Usage
|
|
20
|
+
-----
|
|
21
|
+
Embed:
|
|
22
|
+
python fft_steg.py embed cover.png "Your secret message" stego.png
|
|
23
|
+
python fft_steg.py embed cover.png "$(cat secret.txt)" stego.png
|
|
24
|
+
|
|
25
|
+
Extract:
|
|
26
|
+
python fft_steg.py extract stego.png
|
|
27
|
+
|
|
28
|
+
Capacity check (see how many bytes an image can hold):
|
|
29
|
+
python fft_steg.py capacity image.png
|
|
30
|
+
|
|
31
|
+
Dependencies: Pillow, numpy (pip install Pillow numpy)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import sys
|
|
35
|
+
import struct
|
|
36
|
+
import zlib
|
|
37
|
+
import argparse
|
|
38
|
+
import numpy as np
|
|
39
|
+
from PIL import Image
|
|
40
|
+
|
|
41
|
+
if sys.platform.startswith("win") and hasattr(sys.stdout, "reconfigure"):
|
|
42
|
+
try:
|
|
43
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
44
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
45
|
+
except Exception:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
# ── tunables ───────────────────────────────────────────────────────────────────
|
|
49
|
+
STEP = 32 # quantisation step (must stay fixed between embed/extract)
|
|
50
|
+
END_MARKER = b"\x00\xFF\x00\xFF\xDE\xAD\xBE\xEF" # 8-byte payload sentinel
|
|
51
|
+
|
|
52
|
+
# Mid-frequency positions in an unshifted 8×8 DFT
|
|
53
|
+
# DC (0,0) excluded; high-frequency corners avoided for robustness
|
|
54
|
+
MID_FREQ = [
|
|
55
|
+
(1,0),(2,0),(3,0),
|
|
56
|
+
(0,1),(1,1),(2,1),(3,1),
|
|
57
|
+
(0,2),(1,2),(2,2),(3,2),
|
|
58
|
+
(0,3),(1,3),(2,3),
|
|
59
|
+
]
|
|
60
|
+
BITS_PER_BLOCK = len(MID_FREQ) # 14 usable bits per 8×8 block
|
|
61
|
+
|
|
62
|
+
# ── bit / byte helpers ─────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
def _to_bits(data: bytes) -> list[int]:
|
|
65
|
+
bits = []
|
|
66
|
+
for byte in data:
|
|
67
|
+
for shift in range(7, -1, -1):
|
|
68
|
+
bits.append((byte >> shift) & 1)
|
|
69
|
+
return bits
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _to_bytes(bits: list[int]) -> bytes:
|
|
73
|
+
out = bytearray()
|
|
74
|
+
for i in range(0, len(bits) - 7, 8):
|
|
75
|
+
byte = 0
|
|
76
|
+
for b in bits[i:i+8]:
|
|
77
|
+
byte = (byte << 1) | b
|
|
78
|
+
out.append(byte)
|
|
79
|
+
return bytes(out)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _conj(r: int, c: int) -> tuple[int, int]:
|
|
83
|
+
"""Conjugate-symmetric DFT position (ensures real IFFT output)."""
|
|
84
|
+
return (8 - r) % 8, (8 - c) % 8
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _capacity(channel: np.ndarray) -> int:
|
|
88
|
+
H, W = channel.shape
|
|
89
|
+
return (H // 8) * (W // 8) * BITS_PER_BLOCK
|
|
90
|
+
|
|
91
|
+
# ── core block operations ──────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
def _embed_block(block: np.ndarray, bits: list) -> np.ndarray:
|
|
94
|
+
"""
|
|
95
|
+
Embed up to BITS_PER_BLOCK bits into one 8×8 float block.
|
|
96
|
+
|
|
97
|
+
Uses a self-correcting loop: after embedding and doing an IFFT, the block
|
|
98
|
+
is converted to uint8 and back, then its DFT is re-examined. Any
|
|
99
|
+
coefficient whose parity flipped during that round-trip is nudged one
|
|
100
|
+
quantisation level further from the boundary and the check is repeated
|
|
101
|
+
(up to 5 times, which is always sufficient in practice).
|
|
102
|
+
"""
|
|
103
|
+
S = np.fft.fft2(block)
|
|
104
|
+
|
|
105
|
+
for _attempt in range(5):
|
|
106
|
+
# ── embed pass ──
|
|
107
|
+
for idx, (r, c) in enumerate(MID_FREQ):
|
|
108
|
+
bit = bits[idx]
|
|
109
|
+
if bit is None:
|
|
110
|
+
continue
|
|
111
|
+
cr, cc = _conj(r, c)
|
|
112
|
+
mag = abs(S[r, c])
|
|
113
|
+
phase = np.angle(S[r, c])
|
|
114
|
+
q = round(mag / STEP)
|
|
115
|
+
if bit == 1:
|
|
116
|
+
if q % 2 == 0: q += 1
|
|
117
|
+
else:
|
|
118
|
+
if q % 2 == 1: q += 1
|
|
119
|
+
nm = max(q * STEP, STEP)
|
|
120
|
+
S[r, c] = nm * np.exp( 1j * phase)
|
|
121
|
+
S[cr, cc] = nm * np.exp(-1j * phase)
|
|
122
|
+
|
|
123
|
+
# ── uint8 round-trip check ──
|
|
124
|
+
pixels = np.clip(np.fft.ifft2(S).real, 0, 255)
|
|
125
|
+
pixels8 = pixels.astype(np.uint8).astype(float)
|
|
126
|
+
S_rt = np.fft.fft2(pixels8)
|
|
127
|
+
|
|
128
|
+
all_ok = True
|
|
129
|
+
for idx, (r, c) in enumerate(MID_FREQ):
|
|
130
|
+
bit = bits[idx]
|
|
131
|
+
if bit is None:
|
|
132
|
+
continue
|
|
133
|
+
q_rt = round(abs(S_rt[r, c]) / STEP)
|
|
134
|
+
if q_rt % 2 != bit:
|
|
135
|
+
# Parity flipped — nudge this coefficient one step further
|
|
136
|
+
all_ok = False
|
|
137
|
+
cr, cc = _conj(r, c)
|
|
138
|
+
phase = np.angle(S_rt[r, c])
|
|
139
|
+
q_fix = q_rt + 1
|
|
140
|
+
if q_fix % 2 != bit: q_fix += 1
|
|
141
|
+
nm = max(q_fix * STEP, STEP)
|
|
142
|
+
# Update S from the round-tripped version so corrections stack
|
|
143
|
+
S[r, c] = nm * np.exp( 1j * phase)
|
|
144
|
+
S[cr, cc] = nm * np.exp(-1j * phase)
|
|
145
|
+
|
|
146
|
+
if all_ok:
|
|
147
|
+
break
|
|
148
|
+
|
|
149
|
+
return np.fft.ifft2(S).real
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _decode_block(block: np.ndarray) -> list[int]:
|
|
153
|
+
"""Read BITS_PER_BLOCK embedded bits from one 8×8 float block."""
|
|
154
|
+
S = np.fft.fft2(block)
|
|
155
|
+
return [round(abs(S[r, c]) / STEP) % 2 for r, c in MID_FREQ]
|
|
156
|
+
|
|
157
|
+
# ── high-level API ─────────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
def embed(cover_path: str, message: str, output_path: str) -> None:
|
|
160
|
+
"""Embed *message* into *cover_path* and write the stego image to *output_path*."""
|
|
161
|
+
|
|
162
|
+
img = Image.open(cover_path).convert("YCbCr")
|
|
163
|
+
arr = np.array(img, dtype=float) # H × W × 3 (Y, Cb, Cr)
|
|
164
|
+
y_arr = arr[:, :, 0]
|
|
165
|
+
|
|
166
|
+
# ── build payload: CRC32 | length | UTF-8 text | sentinel ──
|
|
167
|
+
msg_bytes = message.encode("utf-8")
|
|
168
|
+
crc = zlib.crc32(msg_bytes) & 0xFFFFFFFF
|
|
169
|
+
payload = struct.pack(">II", crc, len(msg_bytes)) + msg_bytes + END_MARKER
|
|
170
|
+
all_bits = _to_bits(payload)
|
|
171
|
+
|
|
172
|
+
cap = _capacity(y_arr)
|
|
173
|
+
if len(all_bits) > cap:
|
|
174
|
+
max_msg = (cap // 8) - len(END_MARKER) - 8 # 8 bytes = crc + length
|
|
175
|
+
raise ValueError(
|
|
176
|
+
f"Message too long ({len(msg_bytes)} bytes encoded to {len(all_bits)} bits).\n"
|
|
177
|
+
f"This image can hide at most ~{max_msg} bytes of text."
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
H, W = y_arr.shape
|
|
181
|
+
stego_y = y_arr.copy()
|
|
182
|
+
bits_iter = iter(all_bits)
|
|
183
|
+
|
|
184
|
+
for i in range(H // 8):
|
|
185
|
+
for j in range(W // 8):
|
|
186
|
+
# Collect this block's worth of bits (pad with None at the end)
|
|
187
|
+
block_bits = []
|
|
188
|
+
for _ in MID_FREQ:
|
|
189
|
+
try: block_bits.append(next(bits_iter))
|
|
190
|
+
except StopIteration: block_bits.append(None)
|
|
191
|
+
|
|
192
|
+
block = y_arr[i*8:(i+1)*8, j*8:(j+1)*8].copy()
|
|
193
|
+
modified = _embed_block(block, block_bits)
|
|
194
|
+
stego_y[i*8:(i+1)*8, j*8:(j+1)*8] = np.clip(modified, 0, 255)
|
|
195
|
+
|
|
196
|
+
# ── reconstruct and save ──
|
|
197
|
+
arr[:, :, 0] = stego_y
|
|
198
|
+
stego_img = Image.fromarray(arr.clip(0, 255).astype(np.uint8), "YCbCr")
|
|
199
|
+
stego_img.convert("RGB").save(output_path)
|
|
200
|
+
|
|
201
|
+
max_delta = np.abs(stego_y - y_arr).max()
|
|
202
|
+
pct_used = 100 * len(all_bits) / cap
|
|
203
|
+
print(f"[✓] Message embedded successfully → '{output_path}'")
|
|
204
|
+
print(f" Hidden text size : {len(msg_bytes)} bytes")
|
|
205
|
+
print(f" Bits written : {len(all_bits)}")
|
|
206
|
+
print(f" Image capacity used : {pct_used:.1f}% ({len(all_bits)}/{cap} bits)")
|
|
207
|
+
print(f" Max luma pixel shift : {max_delta:.2f} / 255")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def extract(stego_path: str) -> str:
|
|
211
|
+
"""Extract and return the hidden UTF-8 message from *stego_path*."""
|
|
212
|
+
|
|
213
|
+
img = Image.open(stego_path).convert("YCbCr")
|
|
214
|
+
y_arr = np.array(img, dtype=float)[:, :, 0]
|
|
215
|
+
H, W = y_arr.shape
|
|
216
|
+
|
|
217
|
+
all_bits = []
|
|
218
|
+
for i in range(H // 8):
|
|
219
|
+
for j in range(W // 8):
|
|
220
|
+
block = y_arr[i*8:(i+1)*8, j*8:(j+1)*8]
|
|
221
|
+
all_bits.extend(_decode_block(block))
|
|
222
|
+
|
|
223
|
+
raw = _to_bytes(all_bits)
|
|
224
|
+
|
|
225
|
+
# ── locate sentinel ──
|
|
226
|
+
pos = raw.find(END_MARKER)
|
|
227
|
+
if pos < 0:
|
|
228
|
+
raise ValueError(
|
|
229
|
+
"No hidden message found. The image may not contain one, "
|
|
230
|
+
"or it was saved with lossy compression (e.g. JPEG)."
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
if pos < 8:
|
|
234
|
+
raise ValueError("Payload header is truncated — cannot extract message.")
|
|
235
|
+
|
|
236
|
+
# ── parse header: CRC (4 bytes) + length (4 bytes) ──
|
|
237
|
+
stored_crc, msg_len = struct.unpack(">II", raw[:8])
|
|
238
|
+
msg_bytes = raw[8 : 8 + msg_len]
|
|
239
|
+
|
|
240
|
+
if len(msg_bytes) < msg_len:
|
|
241
|
+
raise ValueError(
|
|
242
|
+
f"Payload truncated: expected {msg_len} bytes, recovered {len(msg_bytes)}."
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# ── integrity check ──
|
|
246
|
+
actual_crc = zlib.crc32(msg_bytes) & 0xFFFFFFFF
|
|
247
|
+
if actual_crc != stored_crc:
|
|
248
|
+
raise ValueError(
|
|
249
|
+
f"CRC-32 mismatch (stored {stored_crc:#010x}, computed {actual_crc:#010x}). "
|
|
250
|
+
"The image may have been modified after embedding."
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return msg_bytes.decode("utf-8")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def capacity(image_path: str) -> None:
|
|
257
|
+
"""Print the hiding capacity of an image."""
|
|
258
|
+
img = Image.open(image_path)
|
|
259
|
+
w, h = img.size
|
|
260
|
+
cap = (h // 8) * (w // 8) * BITS_PER_BLOCK
|
|
261
|
+
overhead = len(END_MARKER) + 8 # sentinel + crc + length
|
|
262
|
+
usable = cap // 8 - overhead
|
|
263
|
+
print(f"Image size : {w} × {h} px")
|
|
264
|
+
print(f"8×8 blocks : {(h//8) * (w//8)}")
|
|
265
|
+
print(f"Total capacity: {cap} bits ({cap//8} bytes)")
|
|
266
|
+
print(f"Usable for text: ~{usable} bytes (~{usable} UTF-8 characters)")
|
|
267
|
+
|
|
268
|
+
# ── CLI ────────────────────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
def main():
|
|
271
|
+
parser = argparse.ArgumentParser(
|
|
272
|
+
description="FFT block steganography — hide text inside images.",
|
|
273
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
274
|
+
epilog=__doc__,
|
|
275
|
+
)
|
|
276
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
277
|
+
|
|
278
|
+
em = sub.add_parser("embed", help="Embed a message into a cover image.")
|
|
279
|
+
em.add_argument("cover", help="Input cover image (PNG/BMP/TIFF recommended).")
|
|
280
|
+
em.add_argument("message", help='Text to hide. Wrap in quotes: "Hello world"')
|
|
281
|
+
em.add_argument("output", help="Output stego image (e.g. stego.png).")
|
|
282
|
+
|
|
283
|
+
ex = sub.add_parser("extract", help="Extract a hidden message from a stego image.")
|
|
284
|
+
ex.add_argument("stego", help="Stego image to read from.")
|
|
285
|
+
|
|
286
|
+
cap = sub.add_parser("capacity", help="Show the hiding capacity of an image.")
|
|
287
|
+
cap.add_argument("image", help="Image file to inspect.")
|
|
288
|
+
|
|
289
|
+
args = parser.parse_args()
|
|
290
|
+
|
|
291
|
+
if args.cmd == "embed":
|
|
292
|
+
try:
|
|
293
|
+
embed(args.cover, args.message, args.output)
|
|
294
|
+
except Exception as exc:
|
|
295
|
+
print(f"[✗] Embed failed: {exc}", file=sys.stderr)
|
|
296
|
+
sys.exit(1)
|
|
297
|
+
|
|
298
|
+
elif args.cmd == "extract":
|
|
299
|
+
try:
|
|
300
|
+
msg = extract(args.stego)
|
|
301
|
+
print("[✓] Hidden message extracted:")
|
|
302
|
+
print()
|
|
303
|
+
print(msg)
|
|
304
|
+
except Exception as exc:
|
|
305
|
+
print(f"[✗] Extract failed: {exc}", file=sys.stderr)
|
|
306
|
+
sys.exit(1)
|
|
307
|
+
|
|
308
|
+
elif args.cmd == "capacity":
|
|
309
|
+
try:
|
|
310
|
+
capacity(args.image)
|
|
311
|
+
except Exception as exc:
|
|
312
|
+
print(f"[✗] {exc}", file=sys.stderr)
|
|
313
|
+
sys.exit(1)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
if __name__ == "__main__":
|
|
317
|
+
main()
|
|
318
|
+
|
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fsteg
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: High-fidelity FFT block-based frequency domain steganography CLI & library
|
|
5
|
+
Project-URL: Homepage, https://github.com/Kishan-Agarwal-28/fsteg
|
|
6
|
+
Project-URL: Repository, https://github.com/Kishan-Agarwal-28/fsteg.git
|
|
7
|
+
Project-URL: Issues, https://github.com/Kishan-Agarwal-28/fsteg/issues
|
|
8
|
+
Author-email: Kishan Agarwal <kishanagarwal028@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: block,cli,dft,fft,frequency-domain,image,security,steganography
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Education
|
|
16
|
+
Classifier: Intended Audience :: Information Technology
|
|
17
|
+
Classifier: Intended Audience :: Science/Research
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Natural Language :: English
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Classifier: Programming Language :: Python :: 3
|
|
22
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
27
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
28
|
+
Classifier: Topic :: Security
|
|
29
|
+
Classifier: Topic :: Security :: Cryptography
|
|
30
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
31
|
+
Classifier: Typing :: Typed
|
|
32
|
+
Requires-Python: >=3.11
|
|
33
|
+
Requires-Dist: numpy>=2.4.6
|
|
34
|
+
Requires-Dist: pillow>=12.3.0
|
|
35
|
+
Provides-Extra: test
|
|
36
|
+
Requires-Dist: pytest>=8.0.0; extra == 'test'
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# 🛰️ fsteg — FFT Block-Based Frequency Domain Steganography
|
|
40
|
+
|
|
41
|
+
[](https://python.org)
|
|
42
|
+
[](https://en.wikipedia.org/wiki/Discrete_Fourier_transform)
|
|
43
|
+
[](https://en.wikipedia.org/wiki/Cyclic_redundancy_check)
|
|
44
|
+
[](https://github.com/astral-sh/uv)
|
|
45
|
+
[](LICENSE)
|
|
46
|
+
|
|
47
|
+
A high-fidelity, frequency-domain steganography engine in Python. **`fsteg`** embeds arbitrary UTF-8 text messages inside digital images by quantising the magnitude spectrum of mid-frequency Discrete Fourier Transform (DFT) coefficients across non-overlapping $8 \times 8$ blocks of the image's luma ($Y$) channel.
|
|
48
|
+
|
|
49
|
+
Unlike naive spatial-domain steganography (such as LSB substitution) which can be trivially detected via statistical histograms or chi-squared attacks, `fsteg` distributes the secret signal across orthogonal 2D sinusoidal basis functions. It features an adaptive **self-correcting round-trip verification engine** that resolves spatial discretization and clipping errors, ensuring 100% bit recovery upon extraction.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 📑 Table of Contents
|
|
54
|
+
|
|
55
|
+
- [Motivation & Comparison](#-motivation--comparison)
|
|
56
|
+
- [Key Features](#-key-features)
|
|
57
|
+
- [System Architecture & Theory](#-system-architecture--theory)
|
|
58
|
+
- [1. YCbCr Luma Channel Isolation](#1-ycbcr-luma-channel-isolation)
|
|
59
|
+
- [2. 8×8 Block Tiling & 2D Discrete Fourier Transform](#2-88-block-tiling--2d-discrete-fourier-transform)
|
|
60
|
+
- [3. Mid-Frequency Coefficient Selection](#3-mid-frequency-coefficient-selection)
|
|
61
|
+
- [4. Hermitian (Conjugate) Symmetry Enforcement](#4-hermitian-conjugate-symmetry-enforcement)
|
|
62
|
+
- [5. Quantization Index Modulation (QIM)](#5-quantization-index-modulation-qim)
|
|
63
|
+
- [6. The Self-Correcting uint8 Round-Trip Loop](#6-the-self-correcting-uint8-round-trip-loop)
|
|
64
|
+
- [Protocol & Binary Framing](#-protocol--binary-framing)
|
|
65
|
+
- [Payload Capacity & Sizing Guide](#-payload-capacity--sizing-guide)
|
|
66
|
+
- [Installation & Setup](#-installation--setup)
|
|
67
|
+
- [CLI Reference & Usage](#-cli-reference--usage)
|
|
68
|
+
- [Check Capacity](#1-check-image-capacity)
|
|
69
|
+
- [Embed Message](#2-embed-a-message)
|
|
70
|
+
- [Extract Message](#3-extract-a-hidden-message)
|
|
71
|
+
- [Programmatic Python API](#-programmatic-python-api)
|
|
72
|
+
- [Lossless vs. Lossy Carrier Formats](#-lossless-vs-lossy-carrier-formats)
|
|
73
|
+
- [Security & Operational Best Practices](#-security--operational-best-practices)
|
|
74
|
+
- [Troubleshooting & FAQs](#-troubleshooting--faqs)
|
|
75
|
+
- [Project Layout](#-project-layout)
|
|
76
|
+
- [License](#-license)
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 💡 Motivation & Comparison
|
|
81
|
+
|
|
82
|
+
| Characteristic | Spatial LSB Substitution | Frequency-Domain (`fsteg` FFT) |
|
|
83
|
+
| :--- | :--- | :--- |
|
|
84
|
+
| **Embedding Domain** | Spatial pixel intensity bitplanes | 2D Fourier magnitude spectrum |
|
|
85
|
+
| **Perceptual Invisibility** | High in high-noise regions; poor in smooth gradients | Outstanding across all image regions |
|
|
86
|
+
| **Statistical Steganalysis** | Vulnerable to Chi-Square, RS analysis, and Sample Pair Analysis | Resilient to spatial histogram and pixel-difference steganalysis |
|
|
87
|
+
| **Energy Distribution** | Confined to individual pixels | Dispersed over entire $8 \times 8$ spatial block |
|
|
88
|
+
| **Bit-Discretization Handling** | Trivial (direct integer manipulation) | **Self-correcting iterative feedback loop** |
|
|
89
|
+
| **Integrity Assurance** | Often none (raw bit insertion) | Built-in **CRC-32 checksum** with length header & sentinel |
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## ✨ Key Features
|
|
94
|
+
|
|
95
|
+
- **Adaptive Self-Correction**: When floating-point Inverse FFT values are clipped to $[0, 255]$ and discretized to `uint8`, quantization noise can invert frequency coefficient parities. `fsteg` simulates this round-trip on each block up to 5 times, iteratively nudging coefficients away from decision boundaries until convergence is achieved.
|
|
96
|
+
- **Hermitian Conjugate Symmetry Preservation**: Automatically mirrors spectral modifications to conjugate coordinates $(8-r, 8-c)$, guaranteeing that the inverse transform remains strictly real-valued without imaginary artifacts.
|
|
97
|
+
- **Mid-Frequency Band Allocation**: Exactly 14 bits embedded per $8 \times 8$ block. Low-frequency DC $(0,0)$ is preserved to avoid perceptual shifts in luminance, and extreme high-frequency corners are avoided for stability.
|
|
98
|
+
- **Blind Extraction**: Embeds a 4-byte big-endian message length prefix and an 8-byte hexadecimal end marker (`0x00FF00FFDEADBEEF`), eliminating the need for the receiver to know payload length beforehand.
|
|
99
|
+
- **Tamper Detection via CRC-32**: Automatically computes and verifies an IEEE 802.3 32-bit CRC. Extraction aborts with an informative diagnostic error if the payload was modified or corrupted.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 🔬 System Architecture & Theory
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
[ Cover Image ] ──────► Convert to YCbCr ──────► Extract Y Channel (Luma)
|
|
107
|
+
│
|
|
108
|
+
Partition into 8×8 Blocks
|
|
109
|
+
│
|
|
110
|
+
[ Secret Message ] ▼
|
|
111
|
+
│ ┌───────────────────────────────────┐
|
|
112
|
+
Compute CRC-32 + Pack Length Prefix │ For each 8×8 block: │
|
|
113
|
+
│ │ 1. Compute 2D FFT: S = FFT2(B) │
|
|
114
|
+
▼ │ 2. Quantize 14 mid-freq coefs │
|
|
115
|
+
[ Payload Stream + 8-Byte Sentinel ] ─►│ 3. Enforce conjugate symmetry │
|
|
116
|
+
│ 4. Run self-correcting IFFT loop │
|
|
117
|
+
└───────────────────────────────────┘
|
|
118
|
+
│
|
|
119
|
+
Reassemble Stego Y Channel
|
|
120
|
+
│
|
|
121
|
+
[ Stego Image ] ◄────── Convert to RGB ◄──────── Merge Y with original Cb, Cr
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 1. YCbCr Luma Channel Isolation
|
|
125
|
+
The human visual system (HVS) possesses significantly greater sensitivity to variations in luminance than to chromaticity (color difference). `fsteg` transforms the input cover image from RGB to YCbCr:
|
|
126
|
+
|
|
127
|
+
$$Y = 0.299\,R + 0.587\,G + 0.114\,B$$
|
|
128
|
+
|
|
129
|
+
The embedding logic is applied **exclusively to the $Y$ (luma) channel**, while $Cb$ and $Cr$ remain unchanged, minimizing perceptible color cast.
|
|
130
|
+
|
|
131
|
+
### 2. 8×8 Block Tiling & 2D Discrete Fourier Transform
|
|
132
|
+
The luminance plane $Y$ is tiled into non-overlapping blocks $B_{i, j}$ of dimension $8 \times 8$ pixels ($0 \le m, n \le 7$). The 2D Discrete Fourier Transform converts each spatial block into its 2D spectral components:
|
|
133
|
+
|
|
134
|
+
$$S[u, v] = \sum_{m=0}^{7} \sum_{n=0}^{7} B[m, n] \cdot e^{-j 2\pi \left( \frac{um}{8} + \frac{vn}{8} \right)}$$
|
|
135
|
+
|
|
136
|
+
Each complex coefficient $S[u, v]$ possesses a magnitude $M = |S[u, v]|$ and phase $\phi = \arg(S[u, v])$.
|
|
137
|
+
|
|
138
|
+
### 3. Mid-Frequency Coefficient Selection
|
|
139
|
+
Each $8 \times 8$ block holds exactly **14 usable bits**. The coordinates selected in the unshifted DFT matrix are:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
v -> 0 1 2 3 4 5 6 7
|
|
143
|
+
u +------------------------------------------------------------
|
|
144
|
+
0 | DC bit3 bit7 bit11 . . . .
|
|
145
|
+
1 | bit0 bit4 bit8 bit12 . . . .
|
|
146
|
+
2 | bit1 bit5 bit9 bit13 . . . .
|
|
147
|
+
3 | bit2 bit6 bit10 . . . . .
|
|
148
|
+
4 | . . . . . . . .
|
|
149
|
+
5 | . . . . . . . .
|
|
150
|
+
6 | . . . . . . . .
|
|
151
|
+
7 | . . . . . . . .
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
- **DC component $(0, 0)$ is omitted**: Modifying DC alters the average block brightness, leading to blocky artifacts.
|
|
155
|
+
- **High frequencies are omitted**: High frequencies carry little energy and are vulnerable to mild spatial perturbations.
|
|
156
|
+
- **Selected coordinates**: `(1,0), (2,0), (3,0), (0,1), (1,1), (2,1), (3,1), (0,2), (1,2), (2,2), (3,2), (0,3), (1,3), (2,3)`.
|
|
157
|
+
|
|
158
|
+
### 4. Hermitian (Conjugate) Symmetry Enforcement
|
|
159
|
+
Because spatial pixel values are real numbers, their Fourier transform must satisfy Hermitian symmetry:
|
|
160
|
+
|
|
161
|
+
$$S[(8 - u) \pmod 8, (8 - v) \pmod 8] = S^*[u, v]$$
|
|
162
|
+
|
|
163
|
+
Whenever the magnitude at $(u, v)$ is altered, `fsteg` calculates the conjugate coordinate $(u^*, v^*) = ((8-u)\%8, (8-v)\%8)$ and assigns:
|
|
164
|
+
|
|
165
|
+
$$S[u, v] = M_{\text{new}} \cdot e^{j \phi}, \qquad S[u^*, v^*] = M_{\text{new}} \cdot e^{-j \phi}$$
|
|
166
|
+
|
|
167
|
+
This prevents the Inverse FFT from producing non-zero imaginary residuals.
|
|
168
|
+
|
|
169
|
+
### 5. Quantization Index Modulation (QIM)
|
|
170
|
+
A uniform quantizer with step size $\Delta = 32$ is applied to coefficient magnitudes:
|
|
171
|
+
|
|
172
|
+
$$q = \text{round}\left(\frac{|S[u, v]|}{\Delta}\right)$$
|
|
173
|
+
|
|
174
|
+
Data bits are encoded into the **parity** of the quantization integer $q$:
|
|
175
|
+
- **Bit 1**: $q$ must be **odd** ($q \pmod 2 = 1$). If even, $q \leftarrow q + 1$.
|
|
176
|
+
- **Bit 0**: $q$ must be **even** ($q \pmod 2 = 0$). If odd, $q \leftarrow q + 1$.
|
|
177
|
+
|
|
178
|
+
The modulated magnitude is computed as $M' = \max(q \cdot \Delta, \Delta)$, preserving original phase $\phi$.
|
|
179
|
+
|
|
180
|
+
### 6. The Self-Correcting uint8 Round-Trip Loop
|
|
181
|
+
A common point of failure in frequency-domain steganography is the conversion back to the spatial integer domain:
|
|
182
|
+
|
|
183
|
+
$$\text{pixels} = \text{clip}\left(\text{Re}(\text{IFFT2}(S)), 0, 255\right) \xrightarrow{\text{round}} \text{uint8}$$
|
|
184
|
+
|
|
185
|
+
Quantization to 8-bit integers and edge clipping introduces broadband spatial noise $e[m, n]$. When taking $\text{FFT2}(\text{pixels}_{\text{uint8}})$, the reconstructed magnitude $M_{\text{rt}}$ may shift across the decision boundary:
|
|
186
|
+
|
|
187
|
+
$$\text{round}\left(\frac{M_{\text{rt}}}{\Delta}\right) \pmod 2 \neq \text{embedded bit}$$
|
|
188
|
+
|
|
189
|
+
To solve this, `fsteg` implements an **iterative self-correction feedback loop** in `_embed_block()`:
|
|
190
|
+
1. Synthesizes spatial block via `ifft2` and converts to `uint8`.
|
|
191
|
+
2. Computes `fft2` on the quantized `uint8` block to check the recovered parity.
|
|
192
|
+
3. For any coefficient where parity flipped, nudges the quantization level $q_{\text{fix}}$ further away from the decision boundary.
|
|
193
|
+
4. Updates the spectral matrix and repeats (converges in 1–2 iterations, guaranteed within 5 passes).
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## 📦 Protocol & Binary Framing
|
|
198
|
+
|
|
199
|
+
The binary payload is structured with strict network big-endian serialization:
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
0 1 2 3
|
|
203
|
+
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
204
|
+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
205
|
+
| CRC-32 (4 Bytes) |
|
|
206
|
+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
207
|
+
| Payload Length L (4 Bytes) |
|
|
208
|
+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
209
|
+
| |
|
|
210
|
+
+ Payload Data (L Bytes) +
|
|
211
|
+
| |
|
|
212
|
+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
213
|
+
| |
|
|
214
|
+
+ Sentinel (8 Bytes: 0x00FF00FFDEADBEEF) +
|
|
215
|
+
| |
|
|
216
|
+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
- **CRC-32** (4 bytes, `uint32_t`, big-endian): Verifies payload authenticity.
|
|
220
|
+
- **Length $L$** (4 bytes, `uint32_t`, big-endian): Byte length of the UTF-8 text string.
|
|
221
|
+
- **Payload** ($L$ bytes): Raw UTF-8 encoded text.
|
|
222
|
+
- **Sentinel** (8 bytes): `\x00\xFF\x00\xFF\xDE\xAD\xBE\xEF` used to demarcate end of payload during streaming extraction.
|
|
223
|
+
- **Fixed Framing Overhead**: $4 + 4 + 8 = 16\text{ bytes}$ ($128\text{ bits}$).
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## 📊 Payload Capacity & Sizing Guide
|
|
228
|
+
|
|
229
|
+
The maximum data that can be embedded in an image depends strictly on its pixel dimensions:
|
|
230
|
+
|
|
231
|
+
$$\text{Capacity}_{\text{raw}} = \left\lfloor\frac{H}{8}\right\rfloor \times \left\lfloor\frac{W}{8}\right\rfloor \times 14\text{ bits}$$
|
|
232
|
+
|
|
233
|
+
$$\text{Capacity}_{\text{usable}} \approx \left\lfloor\frac{\text{Capacity}_{\text{raw}}}{8}\right\rfloor - 16\text{ bytes}$$
|
|
234
|
+
|
|
235
|
+
### Reference Table
|
|
236
|
+
|
|
237
|
+
| Resolution | Dimensions ($W \times H$) | $8 \times 8$ Blocks | Raw Capacity (Bits) | Usable Capacity (Bytes) | Typical Payload Fit |
|
|
238
|
+
| :--- | :--- | :--- | :--- | :--- | :--- |
|
|
239
|
+
| **Small Avatar** | $256 \times 256$ | $1,024$ | $14,336$ | **$1,776$ B** | Small paragraphs, private keys |
|
|
240
|
+
| **Standard** | $512 \times 512$ | $4,096$ | $57,344$ | **$7,152$ B** | Multi-page text, code snippets |
|
|
241
|
+
| **HD 720p** | $1280 \times 720$ | $14,400$ | $201,600$ | **$25,184$ B** | Short articles, scripts |
|
|
242
|
+
| **Full HD 1080p** | $1920 \times 1080$ | $32,400$ | $453,600$ | **$56,684$ B** | Complete document / chapter |
|
|
243
|
+
| **CTF Sample** | $2500 \times 1343$ | $52,104$ | $729,456$ | **$91,166$ B** | Comprehensive archive, book chapter |
|
|
244
|
+
| **4K UHD** | $3840 \times 2160$ | $129,600$ | $1,814,400$ | **$226,784$ B** | Medium-length novella |
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## 🚀 Installation & Setup
|
|
251
|
+
|
|
252
|
+
### ⚡ One-Line Standalone Install (No Python Required)
|
|
253
|
+
|
|
254
|
+
#### Linux & macOS:
|
|
255
|
+
```bash
|
|
256
|
+
curl -fsSL https://raw.githubusercontent.com/Kishan-Agarwal-28/fsteg/main/scripts/install.sh | bash
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
#### Windows (PowerShell):
|
|
260
|
+
```powershell
|
|
261
|
+
irm https://raw.githubusercontent.com/Kishan-Agarwal-28/fsteg/main/scripts/install.ps1 | iex
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
### 📦 System Package Managers
|
|
267
|
+
|
|
268
|
+
#### Python Package Index (PyPI):
|
|
269
|
+
```bash
|
|
270
|
+
pip install fsteg
|
|
271
|
+
# or via uv:
|
|
272
|
+
uv tool install fsteg
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
#### Homebrew (macOS & Linux):
|
|
276
|
+
```bash
|
|
277
|
+
brew tap Kishan-Agarwal-28/tap
|
|
278
|
+
brew install fsteg
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
#### Chocolatey (Windows):
|
|
282
|
+
```powershell
|
|
283
|
+
choco install fsteg
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
#### Scoop (Windows):
|
|
287
|
+
```powershell
|
|
288
|
+
scoop install https://github.com/Kishan-Agarwal-28/fsteg/releases/latest/download/fsteg.json
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
#### Windows Package Manager (WinGet):
|
|
292
|
+
```powershell
|
|
293
|
+
winget install fsteg
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
#### Debian / Ubuntu (APT):
|
|
297
|
+
```bash
|
|
298
|
+
# Download latest .deb from GitHub Releases
|
|
299
|
+
curl -LO https://github.com/Kishan-Agarwal-28/fsteg/releases/latest/download/fsteg_amd64.deb
|
|
300
|
+
sudo dpkg -i fsteg_amd64.deb
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
#### Arch Linux (AUR / Pacman):
|
|
304
|
+
```bash
|
|
305
|
+
# Using makepkg from release PKGBUILD:
|
|
306
|
+
curl -LO https://github.com/Kishan-Agarwal-28/fsteg/releases/latest/download/PKGBUILD
|
|
307
|
+
makepkg -si
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
### 🛠️ Developer Setup (from source)
|
|
313
|
+
|
|
314
|
+
#### Using `uv`:
|
|
315
|
+
```bash
|
|
316
|
+
git clone https://github.com/Kishan-Agarwal-28/fsteg.git
|
|
317
|
+
cd fsteg
|
|
318
|
+
uv sync
|
|
319
|
+
uv run fsteg --help
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
#### Using `pip` and `venv`:
|
|
323
|
+
```bash
|
|
324
|
+
git clone https://github.com/Kishan-Agarwal-28/fsteg.git
|
|
325
|
+
cd fsteg
|
|
326
|
+
python -m venv .venv
|
|
327
|
+
source .venv/bin/activate # On Windows: .venv\Scripts\Activate.ps1
|
|
328
|
+
pip install -e .
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## 💻 CLI Reference & Usage
|
|
334
|
+
|
|
335
|
+
`main.py` provides a clean command-line interface with three primary subcommands:
|
|
336
|
+
|
|
337
|
+
```
|
|
338
|
+
usage: main.py [-h] {embed,extract,capacity} ...
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### 1. Check Image Capacity
|
|
342
|
+
Determine the exact number of bytes a carrier image can accommodate before attempting to embed:
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
# Usage: python main.py capacity <image_path>
|
|
346
|
+
python main.py capacity cover.png
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
**Example Output:**
|
|
350
|
+
```text
|
|
351
|
+
Image size : 1920 × 1080 px
|
|
352
|
+
8×8 blocks : 32400
|
|
353
|
+
Total capacity: 453600 bits (56700 bytes)
|
|
354
|
+
Usable for text: ~56684 bytes (~56684 UTF-8 characters)
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
### 2. Embed a Message
|
|
360
|
+
Embed a string or file content into a cover image and write the resulting stego image.
|
|
361
|
+
|
|
362
|
+
#### Single-line string:
|
|
363
|
+
```bash
|
|
364
|
+
# Usage: python main.py embed <cover_image> "<message>" <output_image>
|
|
365
|
+
python main.py embed cover.png "Project Titan: Launch window confirmed for 0400 UTC." stego.png
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
#### Multi-line text or file contents:
|
|
369
|
+
```bash
|
|
370
|
+
# Linux / macOS (Bash):
|
|
371
|
+
python main.py embed cover.png "$(cat classified_brief.txt)" stego.png
|
|
372
|
+
|
|
373
|
+
# Windows (PowerShell):
|
|
374
|
+
python main.py embed cover.png (Get-Content -Raw classified_brief.txt) stego.png
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
**Example Output:**
|
|
378
|
+
```text
|
|
379
|
+
[✓] Message embedded successfully → 'stego.png'
|
|
380
|
+
Hidden text size : 48 bytes
|
|
381
|
+
Bits written : 512
|
|
382
|
+
Image capacity used : 0.1% (512/453600 bits)
|
|
383
|
+
Max luma pixel shift : 2.84 / 255
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
### 3. Extract a Hidden Message
|
|
389
|
+
Recover and verify the concealed payload from a stego image:
|
|
390
|
+
|
|
391
|
+
```bash
|
|
392
|
+
# Usage: python main.py extract <stego_image>
|
|
393
|
+
python main.py extract stego.png
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
**Example Output:**
|
|
397
|
+
```text
|
|
398
|
+
[✓] Hidden message extracted:
|
|
399
|
+
|
|
400
|
+
Project Titan: Launch window confirmed for 0400 UTC.
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## 🐍 Programmatic Python API
|
|
406
|
+
|
|
407
|
+
You can also import `fsteg` as a module directly inside your Python projects or automated scripts:
|
|
408
|
+
|
|
409
|
+
```python
|
|
410
|
+
from main import embed, extract, capacity
|
|
411
|
+
|
|
412
|
+
# 1. Inspect image hiding headroom
|
|
413
|
+
capacity("carrier.png")
|
|
414
|
+
|
|
415
|
+
# 2. Embed secret data
|
|
416
|
+
secret_message = "Confidential coordinates: 37.7749° N, 122.4194° W"
|
|
417
|
+
embed(
|
|
418
|
+
cover_path="carrier.png",
|
|
419
|
+
message=secret_message,
|
|
420
|
+
output_path="carrier_stego.png"
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
# 3. Extract and verify data
|
|
424
|
+
recovered = extract("carrier_stego.png")
|
|
425
|
+
print("Recovered Message:", recovered)
|
|
426
|
+
assert recovered == secret_message
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## 🖼️ Lossless vs. Lossy Carrier Formats
|
|
432
|
+
|
|
433
|
+
> [!IMPORTANT]
|
|
434
|
+
> Always use **lossless image formats** such as **PNG**, **BMP**, or **TIFF** for `fsteg`.
|
|
435
|
+
|
|
436
|
+
### Why JPEG Re-Encoding Fails
|
|
437
|
+
Standard JPEG compression operates by partitioning images into $8 \times 8$ blocks, computing the Discrete Cosine Transform (DCT), and dividing by a lossy quantization table:
|
|
438
|
+
|
|
439
|
+
$$C_{\text{quantized}}[u, v] = \text{round}\left(\frac{C_{\text{DCT}}[u, v]}{Q[u, v]}\right)$$
|
|
440
|
+
|
|
441
|
+
This step discards subtle high- and mid-frequency fluctuations to minimize file size. This lossy quantization corrupts the precise DFT magnitude parities established by `fsteg`, resulting in CRC mismatch or an undetectable sentinel.
|
|
442
|
+
|
|
443
|
+
*(Note: If a JPEG carrier is saved at uncompressed/maximum 100% quality settings, the data may occasionally survive, but PNG/BMP/TIFF are strongly recommended for guaranteed fidelity.)*
|
|
444
|
+
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
## 🛡️ Security & Operational Best Practices
|
|
448
|
+
|
|
449
|
+
1. **Steganography vs. Cryptography**:
|
|
450
|
+
- Steganography hides the *existence* of communication.
|
|
451
|
+
- Cryptography protects the *confidentiality* of the content.
|
|
452
|
+
- **Recommendation**: Always encrypt your message prior to embedding (e.g., using AES-GCM or ChaCha20-Poly1305). If intercepted, an adversary inspecting spectral coefficients will extract only ciphertext.
|
|
453
|
+
2. **Visual Fidelity**:
|
|
454
|
+
- `STEP = 32` yields an average pixel shift $\Delta_{\text{pixel}} \approx 1\text{ to }3$ intensity levels out of $255$. This remains completely imperceptible to human inspection.
|
|
455
|
+
3. **Carrier Selection**:
|
|
456
|
+
- Select cover images with rich natural textures (landscapes, urban photography, textured surfaces) rather than artificial flat color backgrounds. Natural texture energy in the frequency spectrum provides superior camouflage.
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## 🔧 Troubleshooting & FAQs
|
|
461
|
+
|
|
462
|
+
### Q: UnicodeEncodeError: `'charmap' codec can't encode character '\u2713'` (Windows PowerShell / CMD)
|
|
463
|
+
**Cause:** Windows command terminals often default to legacy code pages (e.g., `cp1252`).
|
|
464
|
+
**Fix:** Force UTF-8 encoding in Python before running:
|
|
465
|
+
```powershell
|
|
466
|
+
$env:PYTHONIOENCODING="utf-8"
|
|
467
|
+
python main.py extract stego.png
|
|
468
|
+
```
|
|
469
|
+
Or switch console code page: `chcp 65001`.
|
|
470
|
+
|
|
471
|
+
---
|
|
472
|
+
|
|
473
|
+
### Q: `ValueError: Message too long (...)`
|
|
474
|
+
**Cause:** The message byte size exceeds the image's capacity.
|
|
475
|
+
**Fix:** Run `python main.py capacity <image>` to check the limits. Use a higher-resolution cover image or compress the payload (e.g., `gzip`) before embedding.
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
### Q: `ValueError: No hidden message found`
|
|
480
|
+
**Cause:** The 8-byte sentinel marker was not found in the decoded bitstream.
|
|
481
|
+
**Common reasons:**
|
|
482
|
+
- The image was converted or re-saved using lossy compression (JPEG, WebP).
|
|
483
|
+
- The image was cropped, resized, rotated, or color-adjusted.
|
|
484
|
+
- The image does not contain an `fsteg` payload.
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
### Q: `ValueError: CRC-32 mismatch (stored 0x..., computed 0x...)`
|
|
489
|
+
**Cause:** The sentinel was discovered, but one or more bits in the payload were flipped.
|
|
490
|
+
**Common reasons:**
|
|
491
|
+
- Minor spatial alterations, noise injection, or compression artifacts occurred after embedding.
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## 📂 Project Layout
|
|
496
|
+
|
|
497
|
+
```text
|
|
498
|
+
fft-steg/
|
|
499
|
+
├── .gitignore # Standard Python / uv gitignore rules
|
|
500
|
+
├── .python-version # Locked Python interpreter version (3.11+)
|
|
501
|
+
├── pyproject.toml # PEP 518/621 project metadata & dependencies
|
|
502
|
+
├── uv.lock # Deterministic dependency lockfile
|
|
503
|
+
├── main.py # Core steganography engine & CLI entrypoint
|
|
504
|
+
├── stego.jpg # Sample carrier image
|
|
505
|
+
└── README.md # Project documentation
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
## 📜 License
|
|
511
|
+
|
|
512
|
+
Distributed under the **MIT License**. See `LICENSE` for more information.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
fsteg/__init__.py,sha256=VB_xCtKcAxFrhhde9zHQTt1PAea0PB0aYbXt8QtFQ7Q,101
|
|
2
|
+
fsteg/main.py,sha256=EMGY6ZDCInWjBhM11IvpmXaZ-fH2XGJkArzE1kG--v8,11822
|
|
3
|
+
fsteg-0.1.0.dist-info/METADATA,sha256=f6vEsT7EzToSxUjUzoXLmSKo8h_XSS9beKQrIJRrIwM,22823
|
|
4
|
+
fsteg-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
5
|
+
fsteg-0.1.0.dist-info/entry_points.txt,sha256=-A1oFIo3698Az7EUaK37CTXYZduLdLDjTeFis-XWVME,42
|
|
6
|
+
fsteg-0.1.0.dist-info/licenses/LICENSE,sha256=tfAKsp3J2SrqdQi2RtTgVd3dn6cMJwm1ZB4qZlZtRzQ,1071
|
|
7
|
+
fsteg-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kishan Agarwal
|
|
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.
|