Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

CapabilityDescription
10 steganographic techniquesLSB image, DCT JPEG, palette, audio (LSB/phase/echo), zero-width text, PDF content-stream, PDF metadata, corpus selection
Post-quantum cryptographyML-KEM-1024 (key encapsulation), ML-DSA-87 (signatures) — pure Rust, no liboqs
Reed-Solomon error correctionK-of-N shard splitting with HMAC integrity verification
Deniable steganographyDual-payload embedding — reveal a decoy under compulsion
Dead drop modePlatform-aware cover generation for public posting (no direct file transfer)
Time-lock puzzlesRivest sequential-squaring payloads that can’t be opened early
Stylometric scrubbingNormalise writing patterns to resist authorship attribution
Amnesiac modeZero disk writes — entire pipeline runs through std::io::pipe()
Canary shardsTripwire detection for compromised distribution channels
Geographic distributionJurisdiction-threshold manifests requiring shards from multiple countries
Forensic watermarksUnique recipient fingerprints to trace leaks
Panic wipeEmergency 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

FeatureDefaultPurpose
pdfPDF embedding/extraction and page rasterisation (requires pdfium)
corpusCorpus-based steganography (zero-modification cover selection)
adaptiveAdaptive embedding (STC-inspired steganalysis evasion)
simdSIMD-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_PATH environment 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

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

TechniqueCover TypeBest For
lsbPNG, BMPLarge payloads in lossless images
dctJPEGLossy images (survives recompression)
paletteGIF, indexed PNGSmall payloads in palette images
lsb-audioWAVAudio covers
phaseWAVPhase-encoded audio (more robust)
echoWAVEcho-hidden audio
zero-widthPlain textText covers (zero-width Unicode)
pdf-contentPDFPDF content-stream LSB
pdf-metadataPDFPDF metadata fields
corpusImage corpusZero-modification cover selection

Next Steps

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

CommandDescription
versionPrint version and git SHA
keygenGenerate a post-quantum key pair
embedEmbed a payload into a cover medium
extractExtract a hidden payload from a stego cover
embed-distributedSplit a payload across multiple covers with Reed-Solomon coding
extract-distributedReconstruct a payload from distributed stego covers
analyseEstimate cover capacity and detectability
archivePack/unpack archive bundles (ZIP, TAR, TAR.GZ)
completionsGenerate shell completion scripts

Countermeasure Commands

CommandThreat Addressed
scrubStylometric source identification
dead-dropTraffic analysis / sender-recipient linking
time-lockTime-sensitive source protection
watermarkInternal leak attribution detection
corpusStatistical steganalysis signature

Global Options

-h, --help     Print help
-V, --version  Print version

Steganographic Techniques

Most commands accept a --technique flag. Available techniques:

ValueCover TypeDescription
lsbPNG, BMPLeast-significant-bit substitution
dctJPEGDCT coefficient modulation
paletteGIF, indexed PNGPalette index substitution
lsb-audioWAVAudio LSB substitution
phaseWAVPhase encoding
echoWAVEcho hiding
zero-widthPlain textZero-width Unicode characters
pdf-streamPDFContent-stream LSB
pdf-metaPDFMetadata field embedding
corpusImage setZero-modification cover selection

Embedding Profiles

The embed and embed-distributed commands support --profile:

ProfileBehaviour
standardDefault — no detectability constraint
adaptiveSTC-inspired optimisation to bound detectability
survivableSurvives platform recompression (requires --platform)

keygen

Generate a post-quantum key pair.

Usage

shadowforge keygen --algorithm <ALGORITHM> --output <DIR>

Options

OptionRequiredDescription
--algorithmYeskyber1024 (ML-KEM-1024) or dilithium3 (ML-DSA-87)
--outputYesOutput 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

OptionRequiredDescription
--inputYesPath to the payload file
--coverNoCover medium (omit for amnesiac mode)
--outputYesOutput path for the stego file
--techniqueYesSteganographic technique (see overview)
--profileNostandard (default), adaptive, or survivable
--platformNoTarget platform (required when profile = survivable)
--amnesiaNoRead cover from stdin, write stego to stdout
--scrub-styleNoScrub text payload before embedding
--deniableNoEnable dual-payload deniable embedding
--decoy-payloadNoDecoy payload path (with --deniable)
--decoy-keyNoDecoy key path (with --deniable)
--keyNoPrimary 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

OptionRequiredDescription
--inputYesPath to the stego file
--outputYesOutput path for the extracted payload
--techniqueYesSteganographic technique
--keyNoKey path for deniable extraction
--amnesiaNoRead 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

OptionRequiredDefaultDescription
--inputYesPayload file
--coversYesGlob pattern for cover files
--output-archiveYesOutput archive path
--techniqueYesSteganographic technique
--data-shardsNo3Number of data shards
--parity-shardsNo2Number of parity shards
--profileNostandardEmbedding profile
--platformNoTarget platform (for survivable)
--canaryNoInject a canary shard for tamper detection
--geo-manifestNoGeographic 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

  1. The payload is split into data-shards pieces.
  2. parity-shards additional pieces are generated via Reed-Solomon coding.
  3. Each shard is embedded into a separate cover file.
  4. 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

OptionRequiredDefaultDescription
--input-archiveYesInput archive or directory path
--outputYesOutput path for the recovered payload
--techniqueYesSteganographic technique
--data-shardsNo3Number of data shards in the original split
--parity-shardsNo2Number 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

OptionRequiredDescription
--coverYesPath to the cover file
--techniqueYesSteganographic technique
--jsonNoOutput 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.

CommandThreatDescription
scrubStylometric identificationNormalise writing patterns
dead-dropTraffic analysisPlatform-aware public posting
time-lockTime-sensitive sourcesDelayed-reveal payloads
watermarkInsider attributionRecipient fingerprinting
corpusStatistical steganalysisZero-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

OptionRequiredDefaultDescription
--inputYesInput text file
--outputYesOutput file
--avg-sentence-lenNo15Target average sentence length
--vocab-sizeNo1000Target 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

OptionRequiredDescription
--coverYesCover image path
--inputYesPayload file path
--platformYesTarget platform
--outputYesOutput stego file
--techniqueNoSteganographic technique (defaults to platform-appropriate)

Supported Platforms

PlatformRecompression
instagramAggressive JPEG
twitterJPEG
whatsappJPEG
telegramModerate JPEG
imgurJPEG

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

  1. The payload is embedded using a technique that survives the target platform’s recompression.
  2. The output image is posted publicly (e.g. uploaded to Twitter).
  3. 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

#ThreatCountermeasureCommand / Flag
1Automated steganalysisAdaptive embedding, cover profile matching, compression survival, corpus selection--profile adaptive, --profile survivable, corpus select
2Compelled decryptionDeniable embedding, panic wipe, time-lock--deniable, panic, time-lock lock
3Traffic analysisDead drop mode, platform-aware embeddingdead-drop
4Endpoint compromiseAmnesiac mode, ZeroizeOnDrop--amnesia
5Legal/jurisdictional pressureGeographic threshold distribution, canary shards--geo-manifest, --canary
6Stylometric identificationStyloScrubberscrub, --scrub-style
7Internal leak attributionForensic watermarkerwatermark 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:

  1. The sender embeds the payload into an image using platform-aware encoding.
  2. The sender posts the image publicly (social media, image hosting).
  3. 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

  1. Time-lock puzzles provide practical delay, not cryptographic guarantees. Hardware advances (especially ASICs) could reduce effective delay times.

  2. Deniable steganography is defeated if the adversary obtains both the primary and decoy keys. Key management discipline is essential.

  3. Stylometric scrubbing is statistical and partial. Authors with highly distinctive styles may retain identifiable residual patterns.

  4. Corpus steganography effectiveness scales with corpus size. A small corpus may not contain a sufficiently close match.

  5. Amnesiac mode protects against disk forensics but not against cold-boot attacks or hardware memory forensics.

  6. Compression-survivable embedding is calibrated to current platform recompression settings. Platform changes may break payload recovery.

  7. 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)

PlaybookFilePrimary Threats
Crossing a Borderdocs/src/opsec/border-crossing.mdCompelled decryption, device seizure
Dead Drop via Public Platformdocs/src/opsec/dead-drop.mdTraffic analysis, surveillance
Geographic Distributiondocs/src/opsec/geographic.mdJurisdictional pressure
Time-Lock Source Protectiondocs/src/opsec/time-lock.mdTime-sensitive compromise
Zero-Trace Operationdocs/src/opsec/zero-trace.mdEndpoint forensics

To read these, clone the repository and open the files directly.

General Principles

  1. Layer your defences. No single countermeasure is sufficient. Combine steganographic concealment with encrypted channels (Signal), network anonymity (Tor), and operational discipline.

  2. Test your procedures. Before using shadowforge in a high-stakes situation, practice the full workflow (embed → transfer → extract) in a safe environment.

  3. Verify key material. Always verify public keys through an out-of-band channel before trusting them.

  4. Destroy after use. Use zeroize discipline — 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

ContextModuleResponsibility
Cryptocrypto/ML-KEM-1024 encapsulation, ML-DSA-87 signing, AES-256-GCM encryption, Argon2id key derivation
Correctioncorrection/Reed-Solomon error correction, K-of-N shard splitting and recovery
Stegostego/10 steganographic techniques: LSB (image), DCT (JPEG), palette, phase/echo/spread (audio), zero-width text, PDF content-stream LSB, PDF metadata, corpus selection
Mediamedia/Image and audio format helpers (PNG, BMP, JPEG, GIF, WAV)
PDFpdf/PDF domain logic: embed/extract, page-render pipeline, content-stream LSB, metadata watermarking
Distributiondistribution/Distribution patterns: 1:1, 1:N, N:1, N:M matrix
Reconstructionreconstruction/K-of-N shard reassembly with manifest verification
Archivearchive/ZIP, TAR, TAR.GZ multi-carrier bundle support
Analysisanalysis/Capacity estimation and chi-square detectability scoring

Nation-State Countermeasure Contexts

ContextModuleThreat Addressed
Adaptiveadaptive/Automated steganalysis (STC-inspired optimisation, cover profile matching, compression-survivable embedding)
Deniabledeniable/Compelled decryption (dual-payload with plausible decoy)
Canarycanary/Distribution compromise (canary shard tripwires)
Dead Dropdeadrop/Traffic analysis (platform-aware cover generation for public posting)
Time-Locktimelock/Time-sensitive source protection (Rivest sequential squaring)
Scrubberscrubber/Stylometric identification (frequency-table normalisation)
Corpuscorpus/Statistical stego signatures (zero-modification cover selection via ANN)
Opsecopsec/Endpoint compromise (amnesiac mode, panic wipe, geographic manifests, forensic watermarks)

Inter-Context Communication

Contexts do not import each other directly. Communication flows through:

  1. Shared types in domain/types.rsPayload, Shard, CoverMedia, EncryptedPayload, etc.
  2. Port traits in domain/ports.rsEncryptor, Signer, SymmetricCipher, ErrorCorrector, MediaLoader, etc.
  3. Application services in application/ — orchestrate multiple contexts by accepting port trait references.

This keeps each context independently testable and replaceable.

Cryptographic Design

Principles

  1. Post-quantum by default. All asymmetric operations use NIST PQC finalists (ML-KEM, ML-DSA). No classical RSA or ECDH.
  2. Defense in depth. Asymmetric encapsulation wraps a symmetric key; the payload itself is encrypted with AES-256-GCM.
  3. Constant-time everything. Secret comparisons use subtle::ConstantTimeEq. No == on key bytes.
  4. Zeroize on drop. Every struct holding key material or plaintext derives ZeroizeOnDrop.
  5. No secrets in logs. Audit every tracing:: call near key material.

Algorithms

PurposeAlgorithmCrateNotes
Key encapsulationML-KEM-1024ml-kemNIST FIPS 203 finalist
Digital signaturesML-DSA-87ml-dsaNIST FIPS 204 finalist
Symmetric encryptionAES-256-GCMaes-gcm256-bit key, 96-bit nonce, authenticated
Key derivationArgon2idargon2Memory-hard KDF for passphrase-based keys
Shard integrityHMAC-SHA256hmac + sha2Per-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:

  1. Email the maintainers with a detailed description.
  2. Include steps to reproduce the issue if possible.
  3. 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::ConstantTimeEq on all secret material.
  • Secure zeroing via zeroize and ZeroizeOnDrop on all key material and plaintext buffers.

Supply Chain

  • cargo-deny enforces banned crate policies (deny.toml).
  • No native-tls, openssl-sys, pqcrypto, or oqs dependencies.
  • 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.rs for pdfium-render FFI bindings.
  • Any unsafe extern block 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

ModuleMinimum Coverage
domain/crypto/90%
Overall85%

Coverage is enforced in CI via cargo-tarpaulin.

Requirements

  • Every public function in domain/ and application/ 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_cmd crate.
  • 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 — enabled
  • clippy::expect_useddenied (no .expect() outside tests)
  • clippy::unwrap_useddenied (no .unwrap() outside tests)
  • clippy::indexing_slicingdenied (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 anyhow in domain/ or adapters/.
  • Propagate errors with ?. Match or if let when 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:

PreferOver
std::sync::LazyLocklazy_static!, once_cell
Vec::extract_ifmanual filter/retain loops
<[T]>::array_windowsmanual index arithmetic
str::floor_char_boundaryunchecked byte slicing
strict_add / strict_sub / strict_mulchecked_add().unwrap()