shadowforge-rs
Quantum-resistant steganography toolkit for journalists, whistleblowers, and dissidents operating against nation-state adversaries.
Pre-audit software. shadowforge-rs has not yet undergone a formal cryptographic or security audit. Use it as a supplementary layer alongside established tools (Signal, Tor, Tails) — never as a sole protection mechanism.
shadowforge-rs is a ground-up Rust reimplementation of shadowforge (Go), designed for the journalist-vs-nation-state threat model.
What It Does
shadowforge-rs hides encrypted payloads inside ordinary-looking cover media (images, audio, PDFs, text) using steganographic techniques that resist automated detection. It then layers post-quantum cryptography, forward error correction, and operational security countermeasures on top.
Key Capabilities
| Capability | Description |
|---|---|
| 10 steganographic techniques | LSB image, DCT JPEG, palette, audio (LSB/phase/echo), zero-width text, PDF content-stream, PDF metadata, corpus selection |
| Post-quantum cryptography | ML-KEM-1024 (key encapsulation), ML-DSA-87 (signatures) — pure Rust, no liboqs |
| Reed-Solomon error correction | K-of-N shard splitting with HMAC integrity verification |
| Deniable steganography | Dual-payload embedding — reveal a decoy under compulsion |
| Dead drop mode | Platform-aware cover generation for public posting (no direct file transfer) |
| Time-lock puzzles | Rivest sequential-squaring payloads that can’t be opened early |
| Stylometric scrubbing | Normalise writing patterns to resist authorship attribution |
| Amnesiac mode | Zero disk writes — entire pipeline runs through std::io::pipe() |
| Canary shards | Tripwire detection for compromised distribution channels |
| Geographic distribution | Jurisdiction-threshold manifests requiring shards from multiple countries |
| Forensic watermarks | Unique recipient fingerprints to trace leaks |
| Panic wipe | Emergency 3-pass secure deletion of key material |
Design Principles
- Threat-first: Every feature maps to a specific adversary capability.
- Zero panics: No
.unwrap(),.expect(), or unchecked indexing in production code — including tests. - Pure domain: The domain layer contains zero I/O. All external interaction goes through port traits.
- Unicode safe: All text operations use grapheme clusters. Arabic, Thai, Devanagari, and emoji ZWJ sequences work correctly.
- Post-quantum only: No RSA, no ECDSA, no X25519. ML-KEM and ML-DSA exclusively.
Installation
Prerequisites
- Rust 1.94.1 or later (edition 2024)
- pdfium shared library (optional, for PDF rasterisation)
From Source
# Clone the repository
git clone https://github.com/greysquirr3l/shadowforge-rs.git
cd shadowforge-rs
# Build in release mode (all default features enabled)
cargo build --release
# The binary is at target/release/shadowforge
Cargo Features
shadowforge-rs uses optional features to control which capabilities are compiled in. This allows you to reduce the attack surface and build dependencies by disabling features you don’t need.
Available Features
| Feature | Default | Purpose |
|---|---|---|
pdf | ✅ | PDF embedding/extraction and page rasterisation (requires pdfium) |
corpus | ✅ | Corpus-based steganography (zero-modification cover selection) |
adaptive | ✅ | Adaptive embedding (STC-inspired steganalysis evasion) |
simd | ❌ | SIMD-accelerated operations (for performance-critical deployments) |
Building with Specific Features
# Disable all optional features
cargo build --release --no-default-features
# Disable only PDF support
cargo build --release --no-default-features --features corpus,adaptive
# Disable only adaptive embedding
cargo build --release --no-default-features --features pdf,corpus
Verify Installation
shadowforge version
PDF Support (Optional)
PDF page rasterisation requires the pdfium shared library. Without it, PDF content-stream and metadata steganography still work, but the render-to-PNG pipeline is unavailable.
The build process will auto-detect pdfium if:
- Set via the
PDFIUM_DYNAMIC_LIB_PATHenvironment variable - Found in a standard system library path:
- macOS:
/opt/homebrew/lib(Homebrew),/opt/local/lib(MacPorts),/usr/local/lib,/usr/lib - Linux:
/usr/local/lib,/usr/lib,/usr/lib/x86_64-linux-gnu,/usr/lib/aarch64-linux-gnu - Windows:
C:\Program Files\pdfium\lib,C:\Program Files (x86)\pdfium\lib
- macOS:
If pdfium is not found, the build will emit a warning. (Note: this warning only appears when the pdf feature is enabled; builds without it proceed silently.)
macOS (Apple Silicon)
curl -L https://github.com/bblanchon/pdfium-binaries/releases/latest/download/pdfium-mac-arm64.tgz | tar xz
export PDFIUM_DYNAMIC_LIB_PATH="$(pwd)/lib"
macOS (Intel)
curl -L https://github.com/bblanchon/pdfium-binaries/releases/latest/download/pdfium-mac-x64.tgz | tar xz
export PDFIUM_DYNAMIC_LIB_PATH="$(pwd)/lib"
Linux (x86_64)
curl -L https://github.com/bblanchon/pdfium-binaries/releases/latest/download/pdfium-linux-x64.tgz | tar xz
export PDFIUM_DYNAMIC_LIB_PATH="$(pwd)/lib"
Shell Completions
See Shell Completions for setting up tab completion in your shell.
Quick Start
This guide walks through the most common workflow: generating keys, embedding a payload into a cover image, and extracting it.
1. Generate Keys
# Generate an ML-KEM-1024 key pair for encryption
shadowforge keygen --algorithm kyber1024 --output ./keys
# Generate an ML-DSA-87 key pair for signing
shadowforge keygen --algorithm dilithium3 --output ./keys-sign
This creates public.key and secret.key inside each output directory.
2. Embed a Payload
# Hide a file inside a PNG cover image using LSB steganography
shadowforge embed \
--input secret-document.txt \
--cover photo.png \
--output stego-photo.png \
--technique lsb
The output file looks identical to the original photo but carries the hidden payload.
3. Extract a Payload
# Recover the hidden file
shadowforge extract \
--input stego-photo.png \
--output recovered-document.txt \
--technique lsb
4. Analyse Cover Capacity
Before embedding, check how much data a cover can hold:
shadowforge analyse \
--input photo.png \
--technique lsb \
--format json
Available Techniques
| Technique | Cover Type | Best For |
|---|---|---|
lsb | PNG, BMP | Large payloads in lossless images |
dct | JPEG | Lossy images (survives recompression) |
palette | GIF, indexed PNG | Small payloads in palette images |
lsb-audio | WAV | Audio covers |
phase | WAV | Phase-encoded audio (more robust) |
echo | WAV | Echo-hidden audio |
zero-width | Plain text | Text covers (zero-width Unicode) |
pdf-content | PDF content-stream LSB | |
pdf-metadata | PDF metadata fields | |
corpus | Image corpus | Zero-modification cover selection |
Next Steps
- Distributed embedding for splitting payloads across multiple covers
- Threat model to understand what shadowforge protects against
- Operational security for real-world usage procedures
Shell Completions
shadowforge can generate tab-completion scripts for all major shells.
Usage
shadowforge completions <SHELL>
Where <SHELL> is one of: bash, zsh, fish, elvish, powershell.
You can also write directly to a file:
shadowforge completions bash --output ~/.local/share/bash-completion/completions/shadowforge
Bash
# Add to ~/.bashrc or ~/.bash_profile
eval "$(shadowforge completions bash)"
Or install persistently:
shadowforge completions bash > ~/.local/share/bash-completion/completions/shadowforge
Zsh
# Add to ~/.zshrc (before compinit)
eval "$(shadowforge completions zsh)"
Or install to your completions directory:
shadowforge completions zsh > ~/.zfunc/_shadowforge
# Ensure ~/.zfunc is in your fpath:
# fpath=(~/.zfunc $fpath)
Fish
shadowforge completions fish > ~/.config/fish/completions/shadowforge.fish
Elvish
eval (shadowforge completions elvish | slurp)
PowerShell
shadowforge completions powershell | Out-String | Invoke-Expression
# Or add to your $PROFILE:
shadowforge completions powershell >> $PROFILE
CLI Overview
shadowforge provides a single binary with subcommands grouped by function.
shadowforge <COMMAND>
Core Commands
| Command | Description |
|---|---|
version | Print version and git SHA |
keygen | Generate a post-quantum key pair |
embed | Embed a payload into a cover medium |
extract | Extract a hidden payload from a stego cover |
embed-distributed | Split a payload across multiple covers with Reed-Solomon coding |
extract-distributed | Reconstruct a payload from distributed stego covers |
analyse | Estimate cover capacity and detectability |
archive | Pack/unpack archive bundles (ZIP, TAR, TAR.GZ) |
completions | Generate shell completion scripts |
Countermeasure Commands
| Command | Threat Addressed |
|---|---|
scrub | Stylometric source identification |
dead-drop | Traffic analysis / sender-recipient linking |
time-lock | Time-sensitive source protection |
watermark | Internal leak attribution detection |
corpus | Statistical steganalysis signature |
Global Options
-h, --help Print help
-V, --version Print version
Steganographic Techniques
Most commands accept a --technique flag. Available techniques:
| Value | Cover Type | Description |
|---|---|---|
lsb | PNG, BMP | Least-significant-bit substitution |
dct | JPEG | DCT coefficient modulation |
palette | GIF, indexed PNG | Palette index substitution |
lsb-audio | WAV | Audio LSB substitution |
phase | WAV | Phase encoding |
echo | WAV | Echo hiding |
zero-width | Plain text | Zero-width Unicode characters |
pdf-stream | Content-stream LSB | |
pdf-meta | Metadata field embedding | |
corpus | Image set | Zero-modification cover selection |
Embedding Profiles
The embed and embed-distributed commands support --profile:
| Profile | Behaviour |
|---|---|
standard | Default — no detectability constraint |
adaptive | STC-inspired optimisation to bound detectability |
survivable | Survives platform recompression (requires --platform) |
keygen
Generate a post-quantum key pair.
Usage
shadowforge keygen --algorithm <ALGORITHM> --output <DIR>
Options
| Option | Required | Description |
|---|---|---|
--algorithm | Yes | kyber1024 (ML-KEM-1024) or dilithium3 (ML-DSA-87) |
--output | Yes | Output directory for key files |
Output
Creates two files in the output directory:
public.key— the public key (safe to share)secret.key— the secret key (protect with your life)
Examples
# Generate encryption keys
shadowforge keygen --algorithm kyber1024 --output ./enc-keys
# Generate signing keys
shadowforge keygen --algorithm dilithium3 --output ./sign-keys
Security Notes
- Secret keys are zeroed from memory on drop (
ZeroizeOnDrop). - Store secret keys on encrypted storage. Consider amnesiac mode for key generation on sensitive systems.
- ML-KEM-1024 provides NIST Level 5 security against quantum adversaries.
- ML-DSA-87 provides NIST Level 5 signature security.
embed
Embed a payload into a cover medium.
Usage
shadowforge embed --input <FILE> --cover <FILE> --output <FILE> --technique <TECHNIQUE> [OPTIONS]
Options
| Option | Required | Description |
|---|---|---|
--input | Yes | Path to the payload file |
--cover | No | Cover medium (omit for amnesiac mode) |
--output | Yes | Output path for the stego file |
--technique | Yes | Steganographic technique (see overview) |
--profile | No | standard (default), adaptive, or survivable |
--platform | No | Target platform (required when profile = survivable) |
--amnesia | No | Read cover from stdin, write stego to stdout |
--scrub-style | No | Scrub text payload before embedding |
--deniable | No | Enable dual-payload deniable embedding |
--decoy-payload | No | Decoy payload path (with --deniable) |
--decoy-key | No | Decoy key path (with --deniable) |
--key | No | Primary key path (with --deniable) |
Examples
# Basic LSB embedding
shadowforge embed \
--input secret.txt --cover photo.png --output stego.png --technique lsb
# Adaptive embedding (bounded detectability)
shadowforge embed \
--input secret.txt --cover photo.png --output stego.png \
--technique lsb --profile adaptive
# Compression-survivable for Instagram
shadowforge embed \
--input secret.txt --cover photo.jpg --output stego.jpg \
--technique dct --profile survivable --platform instagram
# Deniable embedding (dual payloads)
shadowforge embed \
--input real-secret.txt --cover photo.png --output stego.png \
--technique lsb --deniable \
--key ./primary.key --decoy-payload decoy.txt --decoy-key ./decoy.key
# Amnesiac mode (zero disk writes)
cat cover.png | shadowforge embed \
--input secret.txt --output /dev/stdout --technique lsb --amnesia > stego.png
# Embed with stylometric scrubbing
shadowforge embed \
--input article-draft.txt --cover photo.png --output stego.png \
--technique lsb --scrub-style
extract
Extract a hidden payload from a stego cover.
Usage
shadowforge extract --input <FILE> --output <FILE> --technique <TECHNIQUE> [OPTIONS]
Options
| Option | Required | Description |
|---|---|---|
--input | Yes | Path to the stego file |
--output | Yes | Output path for the extracted payload |
--technique | Yes | Steganographic technique |
--key | No | Key path for deniable extraction |
--amnesia | No | Read from stdin, write to stdout |
Examples
# Basic extraction
shadowforge extract \
--input stego.png --output recovered.txt --technique lsb
# Extract with deniable key (real payload)
shadowforge extract \
--input stego.png --output recovered.txt \
--technique lsb --key ./primary.key
# Extract decoy payload (under compulsion)
shadowforge extract \
--input stego.png --output decoy.txt \
--technique lsb --key ./decoy.key
# Amnesiac extraction
cat stego.png | shadowforge extract \
--output /dev/stdout --technique lsb --amnesia > recovered.txt
embed-distributed
Split a payload into Reed-Solomon coded shards and embed each into a separate cover.
Usage
shadowforge embed-distributed \
--input <FILE> --covers <GLOB> --output-archive <FILE> \
--technique <TECHNIQUE> [OPTIONS]
Options
| Option | Required | Default | Description |
|---|---|---|---|
--input | Yes | Payload file | |
--covers | Yes | Glob pattern for cover files | |
--output-archive | Yes | Output archive path | |
--technique | Yes | Steganographic technique | |
--data-shards | No | 3 | Number of data shards |
--parity-shards | No | 2 | Number of parity shards |
--profile | No | standard | Embedding profile |
--platform | No | Target platform (for survivable) | |
--canary | No | Inject a canary shard for tamper detection | |
--geo-manifest | No | Geographic distribution manifest (TOML) |
Examples
# Distribute across 5 covers (3 data + 2 parity)
shadowforge embed-distributed \
--input secret.txt \
--covers "covers/*.png" \
--output-archive distributed.zip \
--technique lsb
# With canary shard
shadowforge embed-distributed \
--input secret.txt \
--covers "covers/*.png" \
--output-archive distributed.zip \
--technique lsb --canary
# Geographic distribution
shadowforge embed-distributed \
--input secret.txt \
--covers "covers/*.png" \
--output-archive distributed.zip \
--technique lsb \
--geo-manifest manifest.toml
How It Works
- The payload is split into
data-shardspieces. parity-shardsadditional pieces are generated via Reed-Solomon coding.- Each shard is embedded into a separate cover file.
- All stego covers are packed into the output archive.
Recovery requires any data-shards of the total shards. With the default 3+2 configuration, any 3 of 5 shards reconstruct the original payload.
extract-distributed
Reconstruct a payload from distributed stego covers.
Usage
shadowforge extract-distributed \
--input-archive <FILE> --output <FILE> --technique <TECHNIQUE> [OPTIONS]
Options
| Option | Required | Default | Description |
|---|---|---|---|
--input-archive | Yes | Input archive or directory path | |
--output | Yes | Output path for the recovered payload | |
--technique | Yes | Steganographic technique | |
--data-shards | No | 3 | Number of data shards in the original split |
--parity-shards | No | 2 | Number of parity shards in the original split |
Examples
# Reconstruct from archive
shadowforge extract-distributed \
--input-archive distributed.zip \
--output recovered.txt \
--technique lsb
# Custom shard counts
shadowforge extract-distributed \
--input-archive distributed.zip \
--output recovered.txt \
--technique lsb \
--data-shards 5 --parity-shards 3
Fault Tolerance
If some shards are missing or corrupted, Reed-Solomon coding can reconstruct the original payload as long as at least data-shards valid shards remain. With 3+2 defaults, losing any 2 shards is tolerable.
analyse
Estimate cover capacity and detectability for a given technique.
Usage
shadowforge analyse --cover <FILE> --technique <TECHNIQUE> [--json]
Options
| Option | Required | Description |
|---|---|---|
--cover | Yes | Path to the cover file |
--technique | Yes | Steganographic technique |
--json | No | Output as JSON instead of a table |
Examples
# Human-readable output
shadowforge analyse --cover photo.png --technique lsb
# JSON output (for scripting)
shadowforge analyse --cover photo.png --technique lsb --json
Output
The analysis reports:
- Capacity: Maximum payload size in bytes
- Technique: The steganographic method used
- Chi-square score: Detectability metric (lower is less detectable)
Countermeasure Commands
These commands address specific nation-state adversary capabilities. Each maps to a threat in the threat model.
| Command | Threat | Description |
|---|---|---|
scrub | Stylometric identification | Normalise writing patterns |
dead-drop | Traffic analysis | Platform-aware public posting |
time-lock | Time-sensitive sources | Delayed-reveal payloads |
watermark | Insider attribution | Recipient fingerprinting |
corpus | Statistical steganalysis | Zero-modification cover selection |
These capabilities are also available as flags on the core embed command:
--scrub-style— inline stylometric scrubbing--deniable— dual-payload deniable embedding--amnesia— zero disk writes--profile adaptive— bounded detectability--profile survivable --platform <P>— compression survival
scrub
Scrub text of stylometric fingerprints to resist authorship attribution.
Usage
shadowforge scrub --input <FILE> --output <FILE> [OPTIONS]
Options
| Option | Required | Default | Description |
|---|---|---|---|
--input | Yes | Input text file | |
--output | Yes | Output file | |
--avg-sentence-len | No | 15 | Target average sentence length |
--vocab-size | No | 1000 | Target vocabulary size |
Examples
shadowforge scrub --input article.txt --output scrubbed.txt
# Custom parameters for more aggressive normalisation
shadowforge scrub --input article.txt --output scrubbed.txt \
--avg-sentence-len 12 --vocab-size 800
How It Works
The scrubber normalises your writing against a frequency table, adjusting sentence length distribution and vocabulary complexity to match a generic baseline. This makes stylometric analysis (e.g. Writeprints, JStylo) less effective at identifying the author.
Limitation: Stylometric scrubbing is statistical and partial. A determined adversary with a large writing sample may still find residual patterns. Use this alongside other countermeasures, not as a sole defence.
dead-drop
Encode a payload for posting on a public platform without direct file transfer.
Usage
shadowforge dead-drop \
--cover <FILE> --input <FILE> --platform <PLATFORM> --output <FILE> [OPTIONS]
Options
| Option | Required | Description |
|---|---|---|
--cover | Yes | Cover image path |
--input | Yes | Payload file path |
--platform | Yes | Target platform |
--output | Yes | Output stego file |
--technique | No | Steganographic technique (defaults to platform-appropriate) |
Supported Platforms
| Platform | Recompression |
|---|---|
instagram | Aggressive JPEG |
twitter | JPEG |
whatsapp | JPEG |
telegram | Moderate JPEG |
imgur | JPEG |
Examples
# Prepare a dead-drop image for Twitter
shadowforge dead-drop \
--cover photo.jpg --input secret.txt \
--platform twitter --output post.jpg
How It Works
- The payload is embedded using a technique that survives the target platform’s recompression.
- The output image is posted publicly (e.g. uploaded to Twitter).
- The recipient downloads the image and extracts using
shadowforge extract.
This eliminates direct sender-recipient communication channels, defeating traffic analysis.
time-lock
Create time-lock puzzle payloads that cannot be opened before a specified duration.
Usage
shadowforge time-lock <COMMAND>
Subcommands
lock
shadowforge time-lock lock \
--input <FILE> --output <FILE> --duration <SECONDS>
unlock
shadowforge time-lock unlock \
--input <FILE> --output <FILE>
Examples
# Lock a payload for 24 hours (86400 seconds)
shadowforge time-lock lock \
--input secret.txt --output locked.bin --duration 86400
# Unlock (requires sequential computation)
shadowforge time-lock unlock \
--input locked.bin --output recovered.txt
How It Works
Uses Rivest’s iterated squaring scheme: the payload is encrypted with a key that can only be derived by performing a sequential chain of modular squarings. No amount of parallelism can speed this up.
Important: Time-lock puzzles provide a practical delay, not a cryptographic guarantee. Hardware advances may reduce the effective delay. Do not rely on this as the sole time-based protection.
watermark
Embed and detect forensic watermarks to trace the origin of leaked copies.
Usage
shadowforge watermark <COMMAND>
Subcommands
embed
Embed a unique recipient fingerprint into a cover.
shadowforge watermark embed \
--cover <FILE> --recipient <ID> --output <FILE>
detect
Check a file for watermark fingerprints.
shadowforge watermark detect \
--input <FILE> --tags <FILE>
Examples
# Watermark a document for a specific recipient
shadowforge watermark embed \
--cover report.png --recipient "recipient-uuid" --output report-wm.png
# Check if a leaked copy contains a known watermark
shadowforge watermark detect \
--input leaked.png --tags known-recipients.json
How It Works
Each recipient gets a unique permutation pattern embedded via LSB. If a copy is leaked, watermark detect matches it against known recipient tags to identify the source of the leak.
corpus
Corpus steganography — zero-modification cover selection via ANN search.
Usage
shadowforge corpus <COMMAND>
Subcommands
index
Build an index over a local image corpus.
shadowforge corpus index --dir <DIR> --output <FILE>
select
Select the best-matching cover from the corpus for a given payload.
shadowforge corpus select \
--index <FILE> --payload <FILE> --output <FILE>
Examples
# Build a corpus index from a photo library
shadowforge corpus index --dir ~/Photos --output corpus.idx
# Find the cover whose existing LSB pattern best matches the payload
shadowforge corpus select \
--index corpus.idx --payload secret.txt --output best-cover.png
How It Works
Instead of modifying a cover to embed data, corpus steganography searches a large collection of images to find one whose existing statistical properties naturally encode (or closely match) the payload bits.
This produces zero modifications to the selected cover, making steganalysis detection theoretically impossible — the cover has never been altered.
Requirement: This technique requires a sufficiently large corpus of cover images to find good matches. Effectiveness scales with corpus size.
Threat Model Overview
shadowforge-rs is designed for the journalist-vs-nation-state threat model. The primary adversary has:
- Infrastructure-scale automated steganalysis (Aletheia, StegExpose)
- Legal authority to compel decryption
- Traffic analysis capabilities across ISPs and platforms
- Endpoint access (device seizure, forensic imaging)
- Stylometric analysis capabilities
- Jurisdictional legal pressure across borders
Threat-to-Countermeasure Map
| # | Threat | Countermeasure | Command / Flag |
|---|---|---|---|
| 1 | Automated steganalysis | Adaptive embedding, cover profile matching, compression survival, corpus selection | --profile adaptive, --profile survivable, corpus select |
| 2 | Compelled decryption | Deniable embedding, panic wipe, time-lock | --deniable, panic, time-lock lock |
| 3 | Traffic analysis | Dead drop mode, platform-aware embedding | dead-drop |
| 4 | Endpoint compromise | Amnesiac mode, ZeroizeOnDrop | --amnesia |
| 5 | Legal/jurisdictional pressure | Geographic threshold distribution, canary shards | --geo-manifest, --canary |
| 6 | Stylometric identification | StyloScrubber | scrub, --scrub-style |
| 7 | Internal leak attribution | Forensic watermarker | watermark embed/detect |
What shadowforge Does NOT Protect Against
See Residual Risks for limitations and scenarios where shadowforge is insufficient.
Automated Steganalysis
Threat
Nation-state adversaries deploy automated steganalysis tools (Aletheia, StegExpose, ensemble classifiers) at infrastructure scale to detect steganographic modifications in images transiting networks and platforms.
Countermeasures
Adaptive Embedding
The AdaptiveEmbedder uses STC-inspired permutation optimisation to minimise the statistical footprint of embedding. It bounds a detectability budget, choosing which cover bits to modify to minimise distortion.
shadowforge embed --input secret.txt --cover photo.png --output stego.png \
--technique lsb --profile adaptive
Cover Profile Matching
The CoverProfileMatcher analyses camera model fingerprints and noise patterns in the cover image. Embedding parameters are adjusted to preserve these natural statistical signatures.
Compression-Survivable Embedding
Social media platforms aggressively recompress uploaded images. The CompressionSurvivableEmbedder ensures payloads survive this recompression for specific target platforms.
shadowforge embed --input secret.txt --cover photo.jpg --output stego.jpg \
--technique dct --profile survivable --platform twitter
Corpus Steganography
The most powerful countermeasure: instead of modifying a cover at all, find one whose existing bit patterns naturally encode the payload. Zero modifications means zero statistical evidence.
shadowforge corpus select --index corpus.idx --payload secret.txt --output cover.png
Residual Risk
Targeted, manual analysis by a skilled steganalyst examining a specific file remains difficult to completely defeat. Adaptive embedding raises the bar significantly but does not guarantee undetectability against all possible classifiers.
Compelled Decryption
Threat
Border agents or authorities demand decryption of seized devices and storage media. Refusal may result in legal penalties or worse.
Countermeasures
Deniable Steganography
The DeniableEmbedder creates dual-payload stego files. Under compulsion, reveal the decoy key — it extracts an innocuous decoy payload. The real payload remains hidden and requires the primary key.
shadowforge embed --input real-secret.txt --cover photo.png --output stego.png \
--technique lsb --deniable \
--key primary.key --decoy-payload shopping-list.txt --decoy-key decoy.key
Under compulsion: hand over decoy.key. The adversary gets the shopping list. They cannot prove a second payload exists.
Panic Wipe
Emergency secure deletion of all key material. Three-pass overwrite, exits silently with code 0.
This command is hidden from --help output to avoid drawing attention.
Time-Lock Puzzles
Encrypt the payload such that it can’t be opened until a sequential computation completes. Even seizing the device doesn’t help — the key doesn’t exist yet.
shadowforge time-lock lock --input secret.txt --output locked.bin --duration 86400
Residual Risk
Deniability fails if the adversary obtains both keys. Time-lock puzzles provide practical delay, not a cryptographic hard guarantee (hardware improvements may shorten the delay).
Traffic Analysis
Threat
Adversaries monitor network traffic to identify sender-recipient communication patterns. Even encrypted channels reveal who is talking to whom.
Countermeasure: Dead Drop Mode
Dead drop mode eliminates direct communication between sender and recipient:
- The sender embeds the payload into an image using platform-aware encoding.
- The sender posts the image publicly (social media, image hosting).
- The recipient downloads the public image and extracts the payload.
No direct network connection between sender and recipient ever occurs.
shadowforge dead-drop \
--cover photo.jpg --input secret.txt \
--platform twitter --output post.jpg
Residual Risk
The adversary could correlate posting timestamps with known source activity. Use Tor/VPN for the upload. The recipient’s download is indistinguishable from normal browsing.
Endpoint Compromise
Threat
The adversary seizes the device and performs forensic imaging — including swap, temp files, and memory dumps.
Countermeasures
Amnesiac Mode
The entire embed/extract pipeline runs through std::io::pipe() with zero disk writes. No temporary files, no swap entries.
cat cover.png | shadowforge embed \
--input secret.txt --output /dev/stdout --technique lsb --amnesia > stego.png
ZeroizeOnDrop
All structs holding key material or plaintext payloads implement ZeroizeOnDrop. When they go out of scope, memory is securely overwritten before deallocation — not left for forensic recovery.
Constant-Time Comparisons
All cryptographic comparisons use subtle::ConstantTimeEq to prevent timing side channels that could leak key material.
Residual Risk
Cold-boot attacks against RAM and hardware-level memory forensics remain outside the software mitigation scope. Use full-disk encryption and power off devices when not in active use.
Legal & Jurisdictional Pressure
Threat
Adversaries use legal instruments across jurisdictions to compel disclosure or seize material. A single-jurisdiction seizure could compromise the entire payload.
Countermeasures
Geographic Threshold Distribution
Split the payload into shards distributed across multiple jurisdictions. Require shards from a minimum number of distinct jurisdictions to reconstruct.
shadowforge embed-distributed \
--input secret.txt --covers "covers/*.png" \
--output-archive distributed.zip \
--technique lsb --geo-manifest jurisdictions.toml
No single jurisdiction’s legal authority can compel disclosure of enough shards.
Canary Shards
Inject tripwire shards into the distribution. If a compromised shard is accessed, the canary triggers, alerting the distribution network.
shadowforge embed-distributed \
--input secret.txt --covers "covers/*.png" \
--output-archive distributed.zip \
--technique lsb --canary
Residual Risk
Coordination between multiple jurisdictions (e.g. MLAT treaties) could theoretically overcome threshold distribution. The minimum-jurisdictions setting should account for known bilateral agreements.
Stylometric Identification
Threat
Adversaries use stylometric analysis (Writeprints, JStylo) to identify the author of anonymous text by analysing sentence length, vocabulary, punctuation patterns, and other linguistic features.
Countermeasure: StyloScrubber
The StyloScrubber normalises text against a generic frequency table:
- Adjusts average sentence length to a configurable target
- Constrains vocabulary to a target size
- Smooths punctuation distribution
shadowforge scrub --input article.txt --output scrubbed.txt \
--avg-sentence-len 12 --vocab-size 800
Or inline during embedding:
shadowforge embed --input article.txt --cover photo.png --output stego.png \
--technique lsb --scrub-style
Residual Risk
Stylometric scrubbing is statistical and partial. A determined adversary with a large corpus of the author’s known writing may still identify residual patterns. This is a risk-reduction measure, not a guarantee of anonymity.
Residual Risks
No tool provides absolute security. shadowforge raises the cost and complexity of adversary actions but has known limitations:
Pre-Audit Status
shadowforge-rs has not undergone a formal security audit. Undiscovered vulnerabilities may exist in the cryptographic implementation, steganographic algorithms, or memory handling.
Specific Limitations
-
Time-lock puzzles provide practical delay, not cryptographic guarantees. Hardware advances (especially ASICs) could reduce effective delay times.
-
Deniable steganography is defeated if the adversary obtains both the primary and decoy keys. Key management discipline is essential.
-
Stylometric scrubbing is statistical and partial. Authors with highly distinctive styles may retain identifiable residual patterns.
-
Corpus steganography effectiveness scales with corpus size. A small corpus may not contain a sufficiently close match.
-
Amnesiac mode protects against disk forensics but not against cold-boot attacks or hardware memory forensics.
-
Compression-survivable embedding is calibrated to current platform recompression settings. Platform changes may break payload recovery.
-
Side channels beyond software scope (power analysis, electromagnetic emanation, acoustic cryptanalysis) are not addressed.
Complementary Tools
shadowforge should be used alongside, not instead of:
- Signal — end-to-end encrypted messaging
- Tor / Tails — network anonymity
- VeraCrypt — full-disk/volume encryption
- Qubes OS — compartmentalised computing
Responsible Disclosure
If you discover a vulnerability, see the Security Policy for reporting instructions.
Operational Security Overview
This section introduces the operational security principles behind shadowforge. Detailed scenario-based playbooks (border crossings, dead drops, geographic distribution, time-lock workflows, and zero-trace procedures) are available in the source repository under docs/src/opsec/ but are intentionally excluded from the published site.
Why? Algorithmic details and CLI references follow Kerckhoffs’s principle — publishing them does not weaken the system. Operational playbooks, however, describe human behaviour patterns that an adversary could use to fingerprint shadowforge users. Keeping them repo-only means they are accessible to anyone who clones the source, but not indexed or browsable by casual reconnaissance.
Prerequisite: Familiarise yourself with the threat model before planning any operation. Understanding which threats apply to your situation determines which countermeasures to deploy.
Available Playbooks (repo only)
| Playbook | File | Primary Threats |
|---|---|---|
| Crossing a Border | docs/src/opsec/border-crossing.md | Compelled decryption, device seizure |
| Dead Drop via Public Platform | docs/src/opsec/dead-drop.md | Traffic analysis, surveillance |
| Geographic Distribution | docs/src/opsec/geographic.md | Jurisdictional pressure |
| Time-Lock Source Protection | docs/src/opsec/time-lock.md | Time-sensitive compromise |
| Zero-Trace Operation | docs/src/opsec/zero-trace.md | Endpoint forensics |
To read these, clone the repository and open the files directly.
General Principles
-
Layer your defences. No single countermeasure is sufficient. Combine steganographic concealment with encrypted channels (Signal), network anonymity (Tor), and operational discipline.
-
Test your procedures. Before using shadowforge in a high-stakes situation, practice the full workflow (embed → transfer → extract) in a safe environment.
-
Verify key material. Always verify public keys through an out-of-band channel before trusting them.
-
Destroy after use. Use
zeroizediscipline — or better, amnesiac mode — to ensure key material doesn’t persist.
Design Philosophy
shadowforge-rs uses a collapsed hexagonal (ports-and-adapters) architecture with DDD-lite bounded contexts.
Why Hexagonal?
The core domain logic — cryptographic operations, steganographic encoding, Reed-Solomon correction — must be pure and testable without I/O. Hexagonal architecture enforces this by pushing all external interactions behind port traits.
┌─────────────────────────────────────────────┐
│ interface/ │
│ (CLI, future API) │
├─────────────────────────────────────────────┤
│ application/ │
│ (orchestration services) │
├──────────────┬──────────────────────────────┤
│ domain/ │ adapters/ │
│ (pure logic │ (I/O, FFI, filesystem) │
│ + ports) │ │
└──────────────┴──────────────────────────────┘
Dependencies always point inward:
- adapters/ imports port traits from domain/ and implements them.
- application/ accepts port trait references and orchestrates domain logic.
- interface/ constructs concrete adapters, wires them into application services, and handles user interaction.
- domain/ imports nothing from the outer layers. It defines the vocabulary, the rules, and the contracts.
Why “Collapsed”?
A full hexagonal architecture separates each bounded context into its own crate with explicit port modules. shadowforge-rs collapses this into a single crate with module boundaries instead of crate boundaries. The workspace structure (crates/) is intentional: future companion crates (e.g. shadowforge-web, shadowforge-api) add as new members without restructuring the core.
Why DDD-lite?
Each bounded context (crypto, stego, correction, etc.) owns its own logic and interacts with others only through the shared type vocabulary in domain/types.rs and the port traits in domain/ports.rs. There are no full aggregate roots or repository patterns — the “lite” means we take the context-boundary discipline without the ceremony.
Bounded Contexts
All bounded contexts live under crates/shadowforge/src/domain/. Each context owns its logic and types, interacting with the rest of the system through the shared type vocabulary (types.rs) and port traits (ports.rs).
Core Contexts
| Context | Module | Responsibility |
|---|---|---|
| Crypto | crypto/ | ML-KEM-1024 encapsulation, ML-DSA-87 signing, AES-256-GCM encryption, Argon2id key derivation |
| Correction | correction/ | Reed-Solomon error correction, K-of-N shard splitting and recovery |
| Stego | stego/ | 10 steganographic techniques: LSB (image), DCT (JPEG), palette, phase/echo/spread (audio), zero-width text, PDF content-stream LSB, PDF metadata, corpus selection |
| Media | media/ | Image and audio format helpers (PNG, BMP, JPEG, GIF, WAV) |
pdf/ | PDF domain logic: embed/extract, page-render pipeline, content-stream LSB, metadata watermarking | |
| Distribution | distribution/ | Distribution patterns: 1:1, 1:N, N:1, N:M matrix |
| Reconstruction | reconstruction/ | K-of-N shard reassembly with manifest verification |
| Archive | archive/ | ZIP, TAR, TAR.GZ multi-carrier bundle support |
| Analysis | analysis/ | Capacity estimation and chi-square detectability scoring |
Nation-State Countermeasure Contexts
| Context | Module | Threat Addressed |
|---|---|---|
| Adaptive | adaptive/ | Automated steganalysis (STC-inspired optimisation, cover profile matching, compression-survivable embedding) |
| Deniable | deniable/ | Compelled decryption (dual-payload with plausible decoy) |
| Canary | canary/ | Distribution compromise (canary shard tripwires) |
| Dead Drop | deadrop/ | Traffic analysis (platform-aware cover generation for public posting) |
| Time-Lock | timelock/ | Time-sensitive source protection (Rivest sequential squaring) |
| Scrubber | scrubber/ | Stylometric identification (frequency-table normalisation) |
| Corpus | corpus/ | Statistical stego signatures (zero-modification cover selection via ANN) |
| Opsec | opsec/ | Endpoint compromise (amnesiac mode, panic wipe, geographic manifests, forensic watermarks) |
Inter-Context Communication
Contexts do not import each other directly. Communication flows through:
- Shared types in
domain/types.rs—Payload,Shard,CoverMedia,EncryptedPayload, etc. - Port traits in
domain/ports.rs—Encryptor,Signer,SymmetricCipher,ErrorCorrector,MediaLoader, etc. - Application services in
application/— orchestrate multiple contexts by accepting port trait references.
This keeps each context independently testable and replaceable.
Cryptographic Design
Principles
- Post-quantum by default. All asymmetric operations use NIST PQC finalists (ML-KEM, ML-DSA). No classical RSA or ECDH.
- Defense in depth. Asymmetric encapsulation wraps a symmetric key; the payload itself is encrypted with AES-256-GCM.
- Constant-time everything. Secret comparisons use
subtle::ConstantTimeEq. No==on key bytes. - Zeroize on drop. Every struct holding key material or plaintext derives
ZeroizeOnDrop. - No secrets in logs. Audit every
tracing::call near key material.
Algorithms
| Purpose | Algorithm | Crate | Notes |
|---|---|---|---|
| Key encapsulation | ML-KEM-1024 | ml-kem | NIST FIPS 203 finalist |
| Digital signatures | ML-DSA-87 | ml-dsa | NIST FIPS 204 finalist |
| Symmetric encryption | AES-256-GCM | aes-gcm | 256-bit key, 96-bit nonce, authenticated |
| Key derivation | Argon2id | argon2 | Memory-hard KDF for passphrase-based keys |
| Shard integrity | HMAC-SHA256 | hmac + sha2 | Per-shard MAC for tamper detection |
Key Lifecycle
keygen
├─ ML-KEM-1024 keypair → (encapsulation_key, decapsulation_key)
└─ ML-DSA-87 keypair → (signing_key, verifying_key)
embed
├─ ML-KEM encapsulate(ek) → (ciphertext, shared_secret)
├─ Argon2id(shared_secret, salt) → symmetric_key
├─ AES-256-GCM encrypt(symmetric_key, nonce, payload) → encrypted_payload
└─ ML-DSA sign(sk, encrypted_payload) → signature
extract
├─ ML-DSA verify(vk, encrypted_payload, signature)
├─ ML-KEM decapsulate(dk, ciphertext) → shared_secret
├─ Argon2id(shared_secret, salt) → symmetric_key
└─ AES-256-GCM decrypt(symmetric_key, nonce, encrypted_payload) → payload
CryptoBundle Pattern
Application services never construct concrete crypto types. Instead, they receive a CryptoBundle containing trait references:
#![allow(unused)]
fn main() {
pub struct CryptoBundle<'a> {
pub encryptor: &'a dyn Encryptor,
pub signer: &'a dyn Signer,
pub cipher: &'a dyn SymmetricCipher,
}
}
The CLI layer constructs concrete adapters and assembles the bundle. This keeps domain/ and application/ free of I/O and concrete crypto dependencies.
Security Policy
Reporting Vulnerabilities
If you discover a security vulnerability in shadowforge-rs, do not open a public issue. Instead:
- Email the maintainers with a detailed description.
- Include steps to reproduce the issue if possible.
- Allow reasonable time for a fix before public disclosure.
We follow coordinated disclosure and will credit reporters unless they prefer anonymity.
Supported Versions
Only the latest release on the main branch receives security patches. Pin your dependency to a specific version and update regularly.
Threat Model Scope
shadowforge-rs is designed to resist:
- Automated steganalysis at infrastructure scale
- Compelled decryption at border crossings
- Traffic analysis and sender-recipient correlation
- Endpoint compromise and memory forensics
- Jurisdictional legal pressure across multiple countries
- Stylometric source identification
For the full threat model, see the Threat Model section.
Cryptographic Choices
- Post-quantum only. ML-KEM-1024 and ML-DSA-87 (NIST PQC finalists). No classical algorithms.
- No custom cryptography. All primitives come from audited Rust crates (
ml-kem,ml-dsa,aes-gcm,argon2). - Constant-time comparisons via
subtle::ConstantTimeEqon all secret material. - Secure zeroing via
zeroizeandZeroizeOnDropon all key material and plaintext buffers.
Supply Chain
cargo-denyenforces banned crate policies (deny.toml).- No
native-tls,openssl-sys,pqcrypto, oroqsdependencies. - Dependabot alerts are monitored. Yanked crates are blocked.
Unsafe Code
#![forbid(unsafe_code)]is set at the crate root.- The only exception is
adapters/pdf.rsfor pdfium-render FFI bindings. - Any
unsafe externblock requires a safety comment and explicit review.
Development Setup
Prerequisites
- Rust 1.94.1 (stable). The project pins this in
rust-toolchain.toml. - cargo-deny for supply-chain checks:
cargo install cargo-deny - mdbook for documentation:
cargo install mdbook
Clone and Build
git clone https://github.com/greysquirr3l/shadowforge-rs.git
cd shadowforge-rs
cargo build --workspace
Verify
cargo test --workspace
cargo clippy --workspace -- -D warnings
cargo deny check
Optional: PDFium
PDF rasterisation requires the PDFium shared library. On macOS:
brew install pdfium
On Linux, download the prebuilt binary from pdfium-binaries and place it on LD_LIBRARY_PATH.
Editor Setup
The project includes .vscode/settings.json with rust-analyzer configuration. VS Code with the rust-analyzer extension will pick up lints and formatting automatically.
Testing Standards
Running Tests
cargo test --workspace
All tests must also pass single-threaded (no test-global mutable state):
cargo test --workspace -- --test-threads=1
Coverage Targets
| Module | Minimum Coverage |
|---|---|
domain/crypto/ | 90% |
| Overall | 85% |
Coverage is enforced in CI via cargo-tarpaulin.
Requirements
- Every public function in
domain/andapplication/must have at least one test. - All tests that touch key material must call
zeroize()on temporaries before assertions. - CLI integration tests use the
assert_cmdcrate. - Unicode tests must include Arabic, Thai, Devanagari, and emoji ZWJ inputs.
Writing Tests
Tests live alongside the code they test, in #[cfg(test)] modules:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_then_decode_round_trips() {
// ...
}
}
}
.unwrap() and .expect() are permitted inside #[cfg(test)] blocks but nowhere else.
What to Test
- Round-trip correctness. Encode then decode; encrypt then decrypt; split then reassemble.
- Edge cases. Empty input, single-byte input, maximum capacity.
- Error paths. Invalid keys, corrupted shards, truncated ciphertext.
- Unicode safety. Multi-byte grapheme clusters, bidirectional text, ZWJ emoji sequences.
- Zeroing. Confirm that key material is zeroed after use.
Code Style
Formatting
The project uses rustfmt with settings in rustfmt.toml. Format before committing:
cargo fmt --all
Lints
Clippy is configured at maximum strictness in Cargo.toml:
clippy::pedantic— enabledclippy::expect_used— denied (no.expect()outside tests)clippy::unwrap_used— denied (no.unwrap()outside tests)clippy::indexing_slicing— denied (use.get()with explicit handling)
Run the full lint check:
cargo clippy --workspace --all-targets -- -D warnings
Lint Suppression
Use #[expect(lint)] instead of #[allow(lint)]. The expect attribute warns if the suppressed lint stops firing, preventing stale suppressions from accumulating.
#![allow(unused)]
fn main() {
#[expect(clippy::cast_possible_truncation)]
fn small_index(n: usize) -> u8 {
n as u8
}
}
Error Handling
- All error types use
thiserror. - No
anyhowindomain/oradapters/. - Propagate errors with
?. Match orif letwhen recovery is needed.
Commits
Use conventional commits:
feat: add dead-drop platform support
fix: handle empty cover image gracefully
refactor: extract shard validation into helper
test: add ZWJ emoji round-trip tests
docs: document time-lock calibration
Standard Library Preferences
Prefer modern standard library features over third-party crates:
| Prefer | Over |
|---|---|
std::sync::LazyLock | lazy_static!, once_cell |
Vec::extract_if | manual filter/retain loops |
<[T]>::array_windows | manual index arithmetic |
str::floor_char_boundary | unchecked byte slicing |
strict_add / strict_sub / strict_mul | checked_add().unwrap() |