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 ADDED
@@ -0,0 +1,3 @@
1
+ from .main import capacity, embed, extract, main
2
+
3
+ __all__ = ["main", "embed", "extract", "capacity"]
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
+ [![Python Version](https://img.shields.io/badge/Python-3.11%2B-blue.svg?logo=python&logoColor=white)](https://python.org)
42
+ [![Fast Fourier Transform](https://img.shields.io/badge/Algorithm-2D--FFT%20%2F%20DFT-orange.svg)](https://en.wikipedia.org/wiki/Discrete_Fourier_transform)
43
+ [![Integrity Validation](https://img.shields.io/badge/Integrity-CRC--32-green.svg)](https://en.wikipedia.org/wiki/Cyclic_redundancy_check)
44
+ [![Package Manager](https://img.shields.io/badge/uv-compatible-purple.svg?logo=astral)](https://github.com/astral-sh/uv)
45
+ [![License](https://img.shields.io/badge/License-MIT-lightgrey.svg)](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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ fsteg = fsteg.main:main
@@ -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.