phasm-core 0.2.2

Pure-Rust steganography engine — hide encrypted messages in JPEG photos
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
# phasm-core

[![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue.svg)](LICENSE)
[![Rust](https://img.shields.io/badge/rust-1.75%2B-orange.svg)](https://www.rust-lang.org)
[![Platforms](https://img.shields.io/badge/platforms-iOS%20%7C%20Android%20%7C%20Web%20%7C%20CLI-green.svg)](https://phasm.app)

Pure-Rust steganography engine for hiding encrypted text messages in JPEG photos.

This is the core library behind [Phasm](https://phasm.app) — available on [iOS](https://apps.apple.com/app/phasm-steganography/id6759446274), Android, and the [web](https://phasm.app).

## Two Embedding Modes

### Ghost (Stealth)

Optimizes for **undetectability**. Uses the [J-UNIWARD](https://phasm.app/blog/uerd-vs-juniwird-detection-benchmarks) cost function to assign distortion costs per DCT coefficient, then embeds via [Syndrome-Trellis Codes (STC)](https://phasm.app/blog/syndrome-trellis-codes-practical-guide) to minimize total distortion. The result resists state-of-the-art steganalysis detectors (SRNet, XedroudjNet) at typical embedding rates.

Use Ghost when the stego image will be stored or transmitted without recompression — the embedding does not survive JPEG re-encoding.

Ghost mode also supports **file attachments** (Brotli-compressed, multi-file).

#### Shadow Messages (Plausible Deniability)

Ghost mode supports **[shadow messages](https://phasm.app/blog/shadow-messages-plausible-deniability-steganography)** — multiple messages hidden in a single image, each with a different passphrase. If coerced into revealing a passphrase, you can give the shadow passphrase instead of the primary one. The adversary decodes a decoy message and has no way to detect whether additional messages exist.

Shadows use **Y-channel direct LSB embedding** with Reed-Solomon error correction. Key design properties:

- **Cost-pool position selection** — shadow positions are drawn from the cheapest UNIWARD-cost regions via two-tier filtering (top-N% cost pool + keyed ChaCha20 permutation), ensuring modifications land in textured areas for maximum stealth
- **∞-cost protection** — when the primary message uses dynamic w ≥ 2, shadow positions receive `f32::INFINITY` cost in the STC Viterbi trellis, routing the primary encoder around them with **BER ≈ 0%**
- **Headerless brute-force decode** — no magic bytes or agreed-upon parameters. A "first-block peek" decodes just the first RS block for each (fraction, parity) combination to read `plaintext_len` and derive the exact frame data length — **~30 RS block decodes** instead of scanning thousands of FDL values. A small-FDL fallback handles tiny messages (single partial RS block). AES-256-GCM-SIV authentication is the only validator
- **Stego-cost verification** — after STC embedding, the encoder re-runs UNIWARD on the stego image to verify shadow BER. If verification fails, an **escalation cascade** automatically increases RS parity (4 → 8 → 16 → … → 128) until the shadow survives or capacity is exhausted

`smart_decode` automatically tries shadow decode as a fallback after primary Ghost decode.

#### SI-UNIWARD (Deep Cover)

When the encoder has access to the original uncompressed pixels (e.g. PNG, HEIC, or RAW input converted to JPEG), **SI-UNIWARD** (Side-Informed UNIWARD) exploits JPEG quantization rounding errors to dramatically reduce embedding costs. Coefficients that were "close to the boundary" between two quantization levels can be flipped at near-zero cost, and the modification direction is chosen to move *toward* the pre-quantization value rather than the default nsF5 direction.

The result: **~43% higher capacity** at the same detection risk, or equivalently the same capacity with significantly lower distortion. The decoder is completely unchanged — `ghost_decode` works identically on standard and SI-UNIWARD stego images.

Use `ghost_encode_si` / `ghost_capacity_si` when raw pixels are available alongside the JPEG cover.

### Armor (Robust)

Optimizes for **survivability**. Uses Spread Transform Dither Modulation (STDM) to embed bits into stable low-frequency DCT coefficients, protected by [adaptive Reed-Solomon ECC](https://phasm.app/blog/surviving-jpeg-recompression) with [soft-decision decoding via log-likelihood ratios](https://phasm.app/blog/soft-majority-voting-llr-concatenated-codes). A [DFT magnitude template](https://phasm.app/blog/dft-template-geometric-resilience-steganography) provides geometric synchronization against rotation, scaling, and cropping.

Armor is the **default mode** on all platforms.

#### Fortress Sub-Mode

For short messages, Armor automatically activates **Fortress** — a [BA-QIM (Block-Average Quantization Index Modulation)](https://phasm.app/blog/watson-perceptual-masking-qim-steganography) scheme that embeds one bit per 8x8 block into the block-average brightness. Fortress exploits [three invariants of JPEG recompression](https://phasm.app/blog/jpeg-recompression-invariants) — block averages, brightness ordering, and coefficient signs — that persist even through aggressive re-encoding.

Fortress survives WhatsApp recompression (QF ~62, resolution resize to 1600px). See [how 15 messaging platforms process your photos](https://phasm.app/blog/how-15-platforms-process-your-photos) for a comprehensive platform analysis.

[Watson perceptual masking](https://phasm.app/blog/watson-perceptual-masking-qim-steganography) adapts the embedding strength per-block based on local texture energy, keeping modifications invisible in textured regions while protecting smooth areas.

### Cover Image Optimizer

`optimize_cover` preprocesses raw pixels before JPEG compression to improve embedding quality and capacity. Each mode has a different pipeline:

- **Ghost**: Texture-adaptive 4-stage pipeline (noise injection, micro-contrast, unsharp mask, smooth-region dithering). Per-pixel 5×5 variance map adapts strength to existing texture — avoids degrading pre-optimized images.
- **Armor**: Light pipeline (block-boundary smoothing, DC stabilization) for STDM robustness.
- **Fortress**: Minimal (block-boundary smoothing only) to stabilize DC averages for BA-QIM.

**"Do no harm" guarantee**: if optimization reduces average gradient energy (a proxy for stego capacity), the original pixels are returned unchanged. Modifications are imperceptible (PSNR > 44 dB, SSIM > 0.993).

```rust
use phasm_core::{optimize_cover, OptimizerConfig, OptimizerMode};

let optimized = optimize_cover(&raw_pixels_rgb, width, height, &OptimizerConfig {
    strength: 0.85,
    seed: [0u8; 32], // ChaCha20 seed for deterministic noise
    mode: OptimizerMode::Ghost,
});
```

### Decode Auto-Detection

`smart_decode` tries all modes automatically — Ghost primary → Ghost shadow → Armor → Fortress — no mode selector needed on the decode side.

### Which Mode Should I Use?

| | Ghost | Armor | Fortress |
|---|---|---|---|
| **Goal** | Undetectable | Survives recompression | Survives WhatsApp |
| **Algorithm** | J-UNIWARD + STC | STDM + Reed-Solomon | BA-QIM + Watson masking |
| **Best for** | Direct sharing, AirDrop, email, "send as file" | Social media, cloud storage, cross-platform | WhatsApp, messaging apps with aggressive compression |
| **Capacity** | High (~1 KB per 12 MP) | Medium | Low (short messages only) |
| **Resists detection** | Yes (steganalysis-resistant) | Moderate | Moderate |
| **Resists recompression** | No | Yes (>95% at mild QF drop) | Yes (survives QF ~62) |
| **Plausible deniability** | Yes (shadow messages) | No | No |
| **File attachments** | Yes (up to 2 MB) | No | No |
| **Activation** | `--mode ghost` | Default | Auto (short messages in Armor) |

**Rule of thumb:** If the image stays intact → Ghost. If it gets recompressed → Armor. If it goes through WhatsApp → Fortress auto-activates.

## Quick Start

```rust
use phasm_core::{ghost_encode, ghost_decode};

let cover = std::fs::read("photo.jpg").unwrap();
let stego = ghost_encode(&cover, "secret message", "passphrase").unwrap();
let decoded = ghost_decode(&stego, "passphrase").unwrap();
assert_eq!(decoded.text, "secret message");
```

```rust
use phasm_core::{armor_encode, armor_decode};

let cover = std::fs::read("photo.jpg").unwrap();
let stego = armor_encode(&cover, "secret message", "passphrase").unwrap();
let (decoded, quality) = armor_decode(&stego, "passphrase").unwrap();
assert_eq!(decoded.text, "secret message");
println!("Integrity: {:.0}%", quality.integrity_percent);
```

```rust
use phasm_core::{ghost_encode_si, ghost_decode};

// SI-UNIWARD: when you have original uncompressed pixels + the JPEG cover
let raw_pixels_rgb = /* RGB pixel data from PNG/HEIC/RAW */;
let cover = std::fs::read("photo.jpg").unwrap();
let stego = ghost_encode_si(
    &cover, raw_pixels_rgb, width, height, "secret message", "passphrase"
).unwrap();
// Decode is identical — no special decoder needed
let decoded = ghost_decode(&stego, "passphrase").unwrap();
assert_eq!(decoded.text, "secret message");
```

```rust
use phasm_core::{ghost_encode_with_shadows, ghost_decode, ghost_shadow_decode, ShadowLayer};

let cover = std::fs::read("photo.jpg").unwrap();
let shadows = vec![
    ShadowLayer {
        message: "decoy message".into(),
        passphrase: "decoy_pass".into(),
        files: vec![],
    },
];
let stego = ghost_encode_with_shadows(
    &cover, "real secret", &[], "real_pass", &shadows, None
).unwrap();

// Primary message — with the real passphrase
let primary = ghost_decode(&stego, "real_pass").unwrap();
assert_eq!(primary.text, "real secret");

// Shadow message — with the decoy passphrase
let shadow = ghost_shadow_decode(&stego, "decoy_pass").unwrap();
assert_eq!(shadow.text, "decoy message");
```

## API

```rust
// Ghost mode (stealth)
ghost_encode(jpeg_bytes, message, passphrase) -> Result<Vec<u8>>
ghost_encode_with_quality(jpeg_bytes, message, passphrase) -> Result<(Vec<u8>, EncodeQuality)>
ghost_decode(jpeg_bytes, passphrase) -> Result<PayloadData>
ghost_encode_with_files(jpeg_bytes, message, files, passphrase) -> Result<Vec<u8>>
ghost_encode_with_files_quality(jpeg_bytes, message, files, passphrase) -> Result<(Vec<u8>, EncodeQuality)>
ghost_capacity(jpeg_image) -> Result<usize>

// Ghost shadow messages (plausible deniability)
ghost_encode_with_shadows(jpeg_bytes, message, files, passphrase, shadows, si) -> Result<Vec<u8>>
ghost_encode_with_shadows_quality(jpeg_bytes, message, files, passphrase, shadows, si) -> Result<(Vec<u8>, EncodeQuality)>
ghost_encode_si_with_shadows(jpeg_bytes, raw_rgb, width, height, message, files, passphrase, shadows) -> Result<Vec<u8>>
ghost_encode_si_with_shadows_quality(jpeg_bytes, raw_rgb, width, height, message, files, passphrase, shadows) -> Result<(Vec<u8>, EncodeQuality)>
ghost_shadow_decode(stego_bytes, passphrase) -> Result<PayloadData>
shadow_capacity(jpeg_bytes) -> Result<usize>
estimate_shadow_capacity(jpeg_image) -> Result<usize>
ghost_capacity_with_shadows(jpeg_bytes, shadows) -> Result<usize>

// Ghost SI-UNIWARD (stealth + side-informed, ~43% more capacity)
ghost_encode_si(jpeg_bytes, raw_rgb, width, height, message, passphrase) -> Result<Vec<u8>>
ghost_encode_si_with_quality(jpeg_bytes, raw_rgb, width, height, message, passphrase) -> Result<(Vec<u8>, EncodeQuality)>
ghost_encode_si_with_files(jpeg_bytes, raw_rgb, width, height, message, files, passphrase) -> Result<Vec<u8>>
ghost_encode_si_with_files_quality(jpeg_bytes, raw_rgb, width, height, message, files, passphrase) -> Result<(Vec<u8>, EncodeQuality)>
ghost_capacity_si(jpeg_image) -> Result<usize>

// Armor mode (robust)
armor_encode(jpeg_bytes, message, passphrase) -> Result<Vec<u8>>
armor_encode_with_quality(jpeg_bytes, message, passphrase) -> Result<(Vec<u8>, EncodeQuality)>
armor_decode(jpeg_bytes, passphrase) -> Result<(PayloadData, DecodeQuality)>
armor_capacity(jpeg_image) -> Result<usize>

// Unified decode (auto-detects mode)
smart_decode(jpeg_bytes, passphrase) -> Result<(PayloadData, DecodeQuality)>

// Cover image optimizer (preprocessing before JPEG compression)
optimize_cover(pixels_rgb, width, height, config) -> Vec<u8>

// Capacity estimation with Brotli compression
compressed_payload_size(text, files) -> usize

// Image dimension validation
validate_encode_dimensions(width, height) -> Result<()>

// Real-time progress tracking
progress::init(total)      // reset and set total steps
progress::advance()        // increment step (capped at total)
progress::finish()         // mark complete (step = total)
progress::get() -> (u32, u32) // read (step, total)
progress::cancel()         // request cancellation
progress::check_cancelled() -> Result<()> // returns Err(Cancelled) if set
```

### Types

- **`PayloadData`** — decoded message text + optional file attachments
- **`FileEntry`** — file attachment (filename + content bytes)
- **`ShadowLayer`** — shadow message for plausible deniability (message + passphrase + optional files)
- **`EncodeQuality`** — encode-time score (0-100), contextual hint key, mode (stealth for Ghost, robustness for Armor)
- **`DecodeQuality`** — signal integrity percentage, RS error count/capacity, fortress flag
- **`ArmorCapacityInfo`** — capacity breakdown by encoding tier (Phase 1/2/3, Fortress)
- **`OptimizerConfig`** — optimizer settings (strength, seed, mode)
- **`OptimizerMode`** — pipeline variant (Ghost, Armor, Fortress)

The SI-UNIWARD functions (`ghost_encode_si`, `ghost_encode_si_with_files`) accept additional parameters for the raw uncompressed pixels (`raw_rgb: &[u8]`, `pixel_width: u32`, `pixel_height: u32`). The decoder does not need side information — `ghost_decode` and `smart_decode` work for both standard and SI-UNIWARD encoded images.

## Video steganography (in development)

Early implementation of H.264 video steganography lives in this crate.
`stego/video/h264_pipeline.rs` embeds encrypted messages in CAVLC-coded
Baseline H.264 streams across four domains (trailing-one signs,
level-suffix magnitude LSBs, level-suffix sign LSBs, motion-vector
difference LSBs). The pure-Rust H.264 encoder and its CABAC entropy
coder are clean-room — no ffmpeg/x264/OpenH264 code derivation; numeric
tables are transcribed from ITU-T H.264 or cited to published academic
sources.

### Source-only distribution (patent-pool posture)

The encoder is gated behind the `h264-encoder` Cargo feature, which is
**OFF by default**. Pre-built binaries published to GitHub Releases (the
`phasm` CLI installer at the bottom of this README) do **NOT** bundle
the H.264 encoder — they ship JPEG steganography only. Consumers who
want CLI video stego must rebuild from source themselves:

```bash
cargo install phasm-cli --features h264-encoder
```

This posture accommodates [Via LA's AVC patent-pool](https://www.via-la.com/licensing-2/avc-h-264/)
licensing for distributed H.264 encoder binaries. Source-only
distribution is permitted royalty-free; binary distribution above the
free-tier (100,000 units/year as of 2026) is not. Mobile-app builds
include the encoder and are subject to the patent-pool terms applicable
to those distribution channels — not to source consumers of this
crate.

The `core/cli/Cargo.toml` and `core/Cargo.toml` make this gating
explicit; do not enable `h264-encoder` for any binary you intend to
publish through GitHub Releases or other free-tier-blind channels
without first reviewing the Via LA AVC license terms.

Not yet shipped in any phasm CLI or mobile-app build at production
quality. The API and default feature set will change. Published here
for transparency and review; production use is not recommended at this
stage.

## Cargo Features

| Feature | Description |
|---------|-------------|
| `parallel` | Enables [Rayon](https://github.com/rayon-rs/rayon) parallelism for J-UNIWARD cost computation, STC embedding, and Armor decode sweeps. Recommended for native builds. |
| `wasm` | WASM bridge support via `wasm-bindgen` + `js-sys`. |

## CLI

A command-line interface is available in the `cli/` directory.

### Install

```bash
# macOS / Linux
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/cgaffga/phasmcore/releases/latest/download/phasm-cli-installer.sh | sh

# Windows PowerShell
powershell -c "irm https://github.com/cgaffga/phasmcore/releases/latest/download/phasm-cli-installer.ps1 | iex"

# Or build from source
cargo install --path cli
```

### Usage

```bash

# Encode (Armor mode, default)
phasm encode photo.jpg -m "secret message" -p "passphrase"

# Encode (Ghost mode)
phasm encode photo.jpg --mode ghost -m "secret" -p "pass" -o secret.jpg

# PNG input → auto Deep Cover (SI-UNIWARD)
phasm encode photo.png --mode ghost -m "hello" -p "pass"

# Shadow messages (plausible deniability)
phasm encode photo.jpg --mode ghost -m "real" -p "main" --m2 "decoy" --p2 "shadow"

# File attachments
phasm encode photo.jpg --mode ghost -m "see attached" -p "pass" --attach plans.pdf

# Decode (auto-detects mode)
phasm decode stego.jpg -p "passphrase"

# Extract file attachments
phasm decode stego.jpg -p "pass" --extract ./output/

# Show capacity
phasm capacity photo.jpg

# Pipe message from stdin
echo "hello" | phasm encode photo.jpg -p "pass"

# Machine-readable output
phasm encode photo.jpg -m "hello" -p "pass" --json
phasm decode stego.jpg -p "pass" --quiet
```

See `phasm encode --help`, `phasm decode --help`, `phasm capacity --help` for all options.

## Building & Testing

```bash
cargo test -p phasm-core                    # default (single-threaded)
cargo test -p phasm-core --features parallel # with Rayon parallelism
```

### Examples

```bash
# Encode and decode a message
cargo run -p phasm-core --example test_encode -- photo.jpg "Hello" "passphrase"
cargo run -p phasm-core --example test_encode -- --decode stego.jpg "passphrase"

# Timing benchmark
cargo run -p phasm-core --example test_timing -- stego.jpg

# Quick decode test
cargo run -p phasm-core --example test_link -- stego.jpg
```

## Architecture

### Design Principles

- **Zero C FFI** — pure Rust from JPEG parsing to AES encryption, compiles to native and WASM
- **Deterministic** — identical output across x86, ARM, and WASM using a [pure-IEEE 754 polynomial math module](https://phasm.app/blog/deterministic-cross-platform-math-wasm) (no `f64::sin`/`cos` — they compile to non-deterministic `Math.*` in WASM)
- **Memory-efficient** — strip-based wavelet computation, compact positions (8 bytes each), 1-bit packed Viterbi back pointers (32× reduction), segmented checkpoint for large images (O(√n) memory), `i8`-quantized SI-UNIWARD rounding errors (8× smaller than `f64`), strip-by-strip luma block computation, strategic `drop()` calls before JPEG output. Supports **200 MP** images under ~1 GB peak memory.
- **Short messages** — optimized for text payloads under 1 KB
- **Stego output is raw** — the JPEG bytes after encoding must be saved/shared without re-encoding

### JPEG Codec

The [`jpeg`](src/jpeg/) module is a [from-scratch JPEG coefficient codec](https://phasm.app/blog/pure-rust-jpeg-coefficient-codec) — zero dependencies beyond `std`. It parses JPEG files into DCT coefficient grids, allows modification, and writes them back with **byte-for-byte round-trip fidelity**. Supports both baseline (SOF0) and progressive (SOF2) JPEG input, always outputs baseline.

Original Huffman tables are preserved from the cover image (with fallback to rebuild if needed).

### Cryptography

All payloads are encrypted before embedding:

- **Key derivation**: Argon2id (RFC 9106) — two tiers:
  - *Structural key* (deterministic from passphrase + fixed salt): drives coefficient permutation and STC matrix generation
  - *Encryption key* (passphrase + random salt): AES-256-GCM-SIV (RFC 8452) with nonce-misuse resistance
- **PRNG**: ChaCha20 for all key-derived randomness (permutations, spreading vectors, template peaks)
- **Payload compression**: Brotli (RFC 7932) for compact payloads; flags byte indicates compression

### Ghost Pipeline

1. Parse JPEG into DCT coefficients
2. Derive structural key (Argon2id, overlapped with step 3 on multi-core) → permutation seed + STC seed
3. Compute [J-UNIWARD costs](https://phasm.app/blog/uerd-vs-juniwird-detection-benchmarks) **strip-by-strip** (Daubechies-8 wavelet, 3 subbands, parallel row+column filtering) — positions collected inline, no full CostMap materialized
4. *(SI-UNIWARD only)* Modulate costs inline using quantized rounding errors (`i8`, <2% precision loss): `cost *= (1 - 2|error|)`. Luma blocks computed in strips of 50 block-rows to minimize peak memory.
5. Permute coefficient order (Fisher-Yates with ChaCha20)
6. Encrypt payload (AES-256-GCM-SIV) and frame (length + CRC)
7. **Short STC with dynamic w**: embed only the actual `m` message bits (not zero-padded to `m_max`). The encoder picks `w = min(⌊n/m⌋, 10)` — for small messages this means **2,500× fewer coefficient modifications** compared to fixed `w`. The decoder brute-forces all `w` values 1–10 in parallel (`rayon::find_map_first`); CRC32 in the frame format provides instant validation (~0 false positive rate at 2⁻³²).
8. *(Shadow mode)* Assign `f32::INFINITY` cost to shadow positions → Viterbi routes around them
9. Embed via [STC](https://phasm.app/blog/syndrome-trellis-codes-practical-guide) (h=7) minimizing weighted distortion; SI-UNIWARD uses informed modification direction (toward pre-quantization value)
10. Write modified coefficients back to JPEG (with progress reporting per MCU row)

#### STC Viterbi Optimizer

The STC encoder uses a Viterbi-style dynamic programming algorithm to find the minimum-cost modification sequence across a trellis with 2^h = 128 states. Two key memory optimizations make it practical for large images (32MP+ photos, 48MP camera sensors):

**1-bit packed back pointers** — The standard Viterbi stores one predecessor state (u32) per trellis state per cover step. Since there are exactly 2 candidate predecessors per target state (stego_bit = 0 or 1), only 1 bit is needed. One `u128` (16 bytes) stores all 128 states' decisions per step, replacing a `Vec<u32>` of 128 entries (512 bytes). **32× memory reduction.**

The forward pass iterates over *target states* instead of source states:

```rust
for s in 0..num_states {
    let cost_0 = prev_cost[s]       + rho_0;  // stego=0, pred=s
    let cost_1 = prev_cost[s ^ col] + rho_1;  // stego=1, pred=s^col
    if cost_1 < cost_0 {
        curr_cost[s] = cost_1;
        packed_bp |= 1u128 << s;  // 1 bit records the choice
    } else {
        curr_cost[s] = cost_0;
    }
}
```

Traceback reconstructs predecessors from the single bit:

```rust
let bit = ((back_ptr[j] >> s) & 1) as u8;
if bit == 1 { s ^= col; }  // undo XOR to get predecessor
```

At message-block boundaries (shift steps), the pre-shift state is fully determined from the known message bit — no storage needed:

```rust
s = ((s << 1) | message_bit) & (num_states - 1);  // invertible shift
```

**Segmented checkpoint Viterbi** — For images with >1M usable positions, even 16 bytes/step can exceed mobile memory (e.g., 100M positions × 16 bytes = 1.6 GB). The segmented approach reduces memory to O(√n):

- **Phase A (forward scan)**: Run the full Viterbi without storing back pointers. Save cost array checkpoints every K = ⌈√m⌉ message blocks (~1 KB each). Total checkpoint memory: ~1.5 MB for maximum payload.
- **Phase B (segment recomputation)**: Process segments in reverse order. For each segment, re-run the forward pass from its checkpoint storing back pointers for that segment only, then traceback and free. Peak memory: ~200 KB per segment.

The dispatcher auto-selects the optimal path:

```rust
const SEGMENTED_THRESHOLD: usize = 1_000_000;

if n_used <= SEGMENTED_THRESHOLD {
    stc_embed_inline(...)   // single pass, all back_ptr in memory
} else {
    stc_embed_segmented(...) // checkpoint/recompute, O(√n) memory
}
```

Both paths produce **bit-for-bit identical output** (verified by equivalence tests). The segmented path trades 2× compute time for O(√n) memory — typically ~3 MB total for any image size.

| Image | n_used | Inline memory | Segmented memory |
|-------|--------|--------------|-----------------|
| 12 MP phone | 2M | 32 MB | 3 MB |
| 32 MB PNG | 5M | 80 MB | 3 MB |
| 48 MP camera | 10M | 160 MB | 3 MB |
| 100 MP medium format | 30M | 480 MB | 3 MB |
| 200 MP flagship | 59M | 944 MB | 3 MB |

#### Strip-Based UNIWARD & Compact Positions

The J-UNIWARD cost function requires wavelet decomposition of the entire decompressed image — three directional subbands (LH, HL, HH) via Daubechies-8 filters. For a 200 MP image, storing the full pixel array + 3 wavelet subbands would need ~3.2 GB.

The **strip-based streaming** approach eliminates this:

1. Process the image in horizontal strips of 50 block rows (~400 pixels)
2. Each strip decompresses its pixel rows (with ±22px padding for the 16-tap wavelet filter boundary), computes row/column-filtered wavelet subbands, and evaluates per-coefficient costs
3. Embeddable positions are collected inline into a compact `Vec<CoeffPos>` — no full CostMap is ever materialized
4. Strip memory is freed before the next strip begins

The `CoeffPos` type uses **compact representation**: `u32` flat index + `f32` cost = 8 bytes per position (down from 16 bytes with `usize` + `f64`). The STC accepts `f32` costs directly, promoting to `f64` only for internal Viterbi accumulation.

| Image | DctGrid | Positions | Strip buffers | **Total peak** |
|-------|---------|-----------|---------------|---------------|
| 12 MP phone | 24 MB | 48 MB | 12 MB | **84 MB** |
| 48 MP camera | 96 MB | 190 MB | 30 MB | **316 MB** |
| 100 MP medium format | 200 MB | 400 MB | 50 MB | **650 MB** |
| 200 MP flagship | 400 MB | 800 MB | 170 MB | **~1 GB** |

Capacity estimation is instantaneous: it counts non-zero AC coefficients directly from the DctGrid (no wavelet computation needed), since J-UNIWARD assigns finite costs to all non-zero coefficients.

### Armor Pipeline

1. Parse JPEG, derive structural keys
2. Compute stability map (select low-frequency AC coefficients)
3. Encrypt and frame payload with adaptive RS parity
4. Embed via STDM with spreading vectors (ChaCha20-derived)
5. For short messages: activate Fortress ([BA-QIM](https://phasm.app/blog/watson-perceptual-masking-qim-steganography) on block averages with [Watson masking](https://phasm.app/blog/watson-perceptual-masking-qim-steganography))
6. Embed [DFT magnitude template](https://phasm.app/blog/dft-template-geometric-resilience-steganography) for geometric resilience
7. Decode: three-phase parallel sweep with [soft-decision concatenated codes](https://phasm.app/blog/soft-majority-voting-llr-concatenated-codes)

### Progress Reporting

Both encode and decode pipelines report real-time progress via global atomics (`progress::advance()`). This enables responsive UI feedback on all platforms:

- **Ghost encode**: 177 steps — 5 (parse) + 100 (UNIWARD sub-steps) + 50 (STC Viterbi sub-steps) + 2 (permute + key derivation) + 20 (JPEG write MCU rows)
- **Ghost encode with shadows**: 277 steps — adds 100 (verification UNIWARD pass) for stego-cost check
- **Ghost decode**: 107 steps — 5 (parse) + 100 (UNIWARD) + 2 (STC extraction + decrypt). Dynamic w brute-force runs in parallel.
- **Armor encode**: 6 steps (STDM path) or 3 steps (Fortress path, auto-adjusted at runtime)
- **Armor decode**: dynamic total based on candidate count (~50 steps). Phase 1 delta sweep parallelized across ~21 candidates.

Cancellation is cooperative: `progress::cancel()` sets a flag checked at natural loop boundaries via `progress::check_cancelled()`.

### FFT

In-house Cooley-Tukey + Bluestein FFT implementation using deterministic twiddle factors (`det_sincos()`). No external FFT crate — guarantees [bit-identical results across all platforms](https://phasm.app/blog/deterministic-cross-platform-math-wasm).

## Project Structure

```
src/
  lib.rs            Public API re-exports
  det_math.rs       Deterministic math (sin/cos/atan2/hypot/exp/powi)
  jpeg/
    mod.rs          JpegImage: parse, modify, serialize
    bitio.rs        Bit-level reader/writer with JPEG byte stuffing
    dct.rs          DCT coefficient grids and quantization tables
    error.rs        JPEG parsing errors
    frame.rs        SOF frame info (dimensions, components, subsampling)
    huffman.rs      Huffman coding tables (two-level decode, encode)
    marker.rs       JPEG marker iterator
    pixels.rs       IDCT/DCT for pixel-domain operations (forward DCT, luma extraction)
    scan.rs         Entropy-coded scan reader/writer
    tables.rs       DQT/DHT table parsing
    zigzag.rs       Zigzag scan order mapping
  stego/
    mod.rs          Ghost/Armor encode/decode entry points
    pipeline.rs     Ghost mode pipeline (J-UNIWARD + STC)
    crypto.rs       AES-256-GCM-SIV + Argon2id key derivation
    frame.rs        Payload framing (length, CRC, mode byte, salt, nonce)
    payload.rs      Payload serialization (Brotli, file attachments)
    permute.rs      Fisher-Yates coefficient permutation
    side_info.rs    SI-UNIWARD: rounding errors, cost modulation, direction selection
    shadow.rs       Shadow messages (Y-channel direct LSB + RS ECC, plausible deniability)
    optimizer.rs    Cover image optimizer (texture-adaptive preprocessing, do-no-harm)
    quality.rs      Encode quality scoring (stealth for Ghost, robustness for Armor)
    capacity.rs     Ghost capacity estimation
    progress.rs     Real-time progress tracking (atomics + WASM callback)
    error.rs        StegoError enum
    cost/
      mod.rs        Cost function trait
      uniward.rs    J-UNIWARD (Daubechies-8 wavelet, 3 subbands)
      uerd.rs       UERD cost function (legacy)
    stc/
      mod.rs        Syndrome-Trellis Codes
      embed.rs      STC Viterbi embedding (1-bit packed + segmented checkpoint)
      extract.rs    STC extraction (syndrome computation)
      hhat.rs       H-hat submatrix generation (ChaCha20-derived)
    armor/
      mod.rs        Armor mode re-exports
      pipeline.rs   Armor encode/decode (STDM + Fortress + DFT template)
      embedding.rs  STDM embed/extract with adaptive delta
      selection.rs  Coefficient stability map
      spreading.rs  ChaCha20-derived spreading vectors
      ecc.rs        Reed-Solomon GF(2^8) encoder/decoder
      repetition.rs Repetition coding with soft majority voting
      capacity.rs   Armor capacity estimation
      fortress.rs   BA-QIM block-average embedding + Watson masking
      template.rs   DFT magnitude template (geometric resilience)
      dft_payload.rs DFT ring-based payload embedding
      fft2d.rs      2D FFT (Cooley-Tukey + Bluestein)
      resample.rs   Bilinear resampling for geometric correction
cli/                Command-line interface (phasm binary)
test-vectors/       Synthetic JPEG test images
tests/              Integration tests (round-trip, cross-platform, geometry)
examples/           CLI tools (encode/decode, timing, diagnostics)
```

## Research & Publications

The algorithms in phasm-core are documented in detail on the [Phasm blog](https://phasm.app/blog):

### Steganography Fundamentals
- [Adaptive Steganography at Low Embedding Rates: UERD vs J-UNIWARD Detection Benchmarks](https://phasm.app/blog/uerd-vs-juniwird-detection-benchmarks)
- [Syndrome-Trellis Codes for Practical JPEG Steganography](https://phasm.app/blog/syndrome-trellis-codes-practical-guide)
- [Surviving JPEG Recompression: A Quantitative Analysis of DCT-Domain Robust Steganography](https://phasm.app/blog/surviving-jpeg-recompression)

### Robustness & Error Correction
- [Three Invariants of JPEG Recompression: Block Averages, Brightness Ordering, and Coefficient Signs](https://phasm.app/blog/jpeg-recompression-invariants)
- [Soft Decoding for Steganography: How LLRs and Concatenated Codes Turn 19% BER Into Zero Errors](https://phasm.app/blog/soft-majority-voting-llr-concatenated-codes)
- [Perceptual Masking Meets QIM: Adaptive Embedding Strength for Invisible Robust Steganography](https://phasm.app/blog/watson-perceptual-masking-qim-steganography)

### Geometric Resilience
- [Fourier-Domain Template Embedding: How Phasm Survives Rotation, Scaling, and Cropping](https://phasm.app/blog/dft-template-geometric-resilience-steganography)

### Plausible Deniability
- [Shadow Messages: How Phasm Hides Multiple Secrets in a Single Photo](https://phasm.app/blog/shadow-messages-plausible-deniability-steganography)

### Implementation
- [Building a Pure-Rust JPEG Coefficient Codec](https://phasm.app/blog/pure-rust-jpeg-coefficient-codec)
- [When f64::sin() Breaks Your Crypto: Building Deterministic Math for WASM Steganography](https://phasm.app/blog/deterministic-cross-platform-math-wasm)
- [From 480 MB to 3 MB: Fitting Viterbi Steganography on a Phone](https://phasm.app/blog/segmented-viterbi-memory-efficient-stc)

### Platform Analysis
- [How 15 Messaging Platforms Process Your Photos](https://phasm.app/blog/how-15-platforms-process-your-photos)

## How Does Phasm Compare?

| Feature | Phasm | OpenStego | Steghide | F5 |
|---------|-------|-----------|----------|----|
| **Status** | Active (2025–present) | Unmaintained | Unmaintained (2003) | Unmaintained |
| **Language** | Rust (native + WASM) | Java | C++ | Java |
| **Encryption** | AES-256-GCM-SIV + Argon2id | DES | Blowfish / AES | None |
| **Embedding** | J-UNIWARD + STC (h=7) | LSB | LSB-adjacent | DCT histogram shift |
| **Steganalysis resistance** | High (resists SRNet, XedroudjNet) | Low | Low | Moderate |
| **Survives recompression** | Yes (Armor / Fortress) | No | No | No |
| **Survives WhatsApp** | Yes (Fortress, short messages) | No | No | No |
| **Plausible deniability** | Yes (shadow messages) | No | No | No |
| **Side-informed mode** | Yes (SI-UNIWARD, +43% capacity) | No | No | No |
| **File attachments** | Yes (Ghost, up to 2 MB) | No | Yes | No |
| **Geometric resilience** | Yes (DFT template) | No | No | No |
| **Platforms** | iOS, Android, Web (WASM), CLI | Desktop (Java) | Linux / Windows CLI | Java |
| **Web (no install)** | Yes (WebAssembly) | No | No | No |
| **Image format** | JPEG (any input auto-converted) | PNG, BMP | JPEG, BMP, WAV, AU | JPEG |
| **Open source** | GPL-3.0 | GPL-2.0 | GPL-2.0 | MIT |

## FAQ

**Does Phasm work with WhatsApp?**
Yes. Fortress sub-mode (auto-activates for short messages in Armor mode) survives WhatsApp's default recompression pipeline. For longer messages, send the image as a document/file to bypass compression entirely.

**How detectable is Ghost mode?**
At typical embedding rates (0.4 bpnzAC), even deep-learning steganalysis detectors like SRNet perform near random chance against J-UNIWARD-embedded images. SI-UNIWARD (auto-activated for non-JPEG inputs) further reduces detection risk.

**What image sizes are supported?**
Minimum 200×200 pixels, maximum 8192×8192 / 16 megapixels. Oversized images are automatically downsampled. The engine handles images up to 200 MP internally with ~3 MB peak memory for STC embedding.

**Is the output JPEG or PNG?**
Always JPEG. Input can be any common format (JPEG, PNG, WebP, HEIC) — non-JPEG inputs are auto-converted and enable SI-UNIWARD Deep Cover in Ghost mode.

**Can I hide files, not just text?**
Yes, in Ghost mode. File attachments up to 2 MB per file are supported alongside text messages. Files are Brotli-compressed before embedding.

**What happens if I lose my passphrase?**
The message cannot be recovered. Phasm uses AES-256-GCM-SIV with Argon2id key derivation — no backdoor, no recovery mechanism. Only the correct passphrase can decrypt the message.

**Can someone prove a message exists without the passphrase?**
Ghost mode is designed to resist this. Shadow messages add plausible deniability — you can reveal a decoy passphrase that decodes a harmless message, with no way to prove additional messages exist.

**Does Phasm work offline?**
Yes. All processing happens on-device. The web version runs entirely in WebAssembly — no server communication, no account required.

## License

GPL-3.0-only. See [LICENSE](LICENSE).

Third-party dependency licenses are listed in [THIRD_PARTY_LICENSES](THIRD_PARTY_LICENSES).