Skip to content

Repository files navigation

SNOW2 ❄️

A modern Rust tribute to Matthew Kwan's SNOW steganography tool.

Try the live demo →

SNOW2 is a clean, modern reimplementation inspired by the original SNOW program by Matthew Kwan — a classic late-1990s tool that demonstrated how encrypted messages could be hidden inside ordinary-looking text using whitespace.

This project preserves the elegance and educational value of the original while upgrading the cryptography, safety, and engineering for today.


Philosophy

The original SNOW was brilliant because it taught two powerful concepts quickly:

  1. Encryption protects meaning.
  2. Steganography hides existence.

SNOW2 keeps that same spirit:

  • Small
  • Understandable
  • Educational
  • Respectful of the original design

But cryptographically modern.


What SNOW2 Does

SNOW2 hides an encrypted payload inside a text "carrier" using whitespace-based steganography.

You provide:

  • A message or file
  • A password
  • Optional second secret (pepper / "Signal Key")

SNOW2 produces:

  • A modified text file that reads as ordinary text
  • A recoverable encrypted payload, invisible when the text is read normally
  • A payload that, once decoded from the channel, is indistinguishable from random bytes (v4 hardened pipeline)

Extraction requires the correct secrets. Tampering causes authenticated failure.


Modern Cryptography

SNOW2 upgrades the crypto model completely.

AEAD Encryption

  • XChaCha20-Poly1305
    • Large nonce space
    • Misuse-resistant
    • Authenticated encryption (confidentiality + integrity)

Key Derivation (Domain-Separated)

  • Argon2id → master secret, then HKDF-SHA256 expansion with domain labels
    • Memory-hard, GPU-resistant
    • Tunable parameters (--kdf-mib, --kdf-iters, --kdf-par)
    • Domain labels: snow2/aead-key, snow2/pepper-binding
    • Pepper bound via HKDF salt — no concatenation ambiguity

Extraction-Side KDF Bounds

  • Untrusted container headers are validated before any expensive KDF work
  • Hard limits reject absurd memory/time cost values from hostile containers
  • Bounds: max 512 MiB memory, max 64 iterations, max 16 parallelism, min 8 MiB memory
  • Both embedding and extraction validate KDF params — you can't create un-extractable containers

Hardened Profile

  • KdfParams::hardened() — 256 MiB / t=4 / p=1 for high-value secrets
  • EmbedSecurityOptions::hardened() — hardened KDF + mandatory pepper

Optional Pepper ("Signal Key")

  • A second secret never stored in the carrier
  • Cryptographically bound via HKDF domain separation (not concatenation)
  • If missing or incorrect → decryption fails
  • Not required, but strongly encouraged

Pepper-required policy

SNOW2 can require a pepper for decryption.

If enabled at embed time, the container is marked pepper_required=true (authenticated in the header).
Extraction will fail if the pepper is not provided — even if the password is correct.

This is useful when you want "password + something else you know" by policy.

Secure Memory

  • SecureVec: mlock'd, dual-guard-paged, zeroize-on-drop memory buffers (native targets)
    • Leading and trailing guard pages (PROT_NONE) catch underflows and overflows
    • MADV_DONTDUMP (Linux) excludes secure buffers from core dumps
    • Optional enable_paranoid_memory() calls mlockall to pin all process pages
  • Intermediate plaintext from AEAD decryption is zeroized after copy to SecureVec
  • Decompression temporaries are explicitly zeroized before drop
  • Passwords zeroized from memory after use
  • On WASM targets, falls back to zeroize-on-drop Vec wrappers (no mlock available)

Bitstream Integrity

  • V4 containers: Outer AEAD (XChaCha20-Poly1305 with Argon2-derived key) provides cryptographic integrity over the entire embedded bitstream — no CRC framing needed
  • Legacy containers (v1/v3): CRC-32 framing catches carrier corruption (whitespace stripping, copy-paste mangling) before the container parser or AEAD sees it

Why Two AEAD Layers

V4 encrypts twice, with two keys HKDF-expanded from the same Argon2id master secret:

Layer Key material Covers
Outer password only (snow2/outer-key) the entire embedded bitstream, padding included
Inner password + pepper (snow2/aead-key) the container plaintext

The outer key deliberately omits the pepper. That is what lets extraction decrypt the outer layer with the password alone, read the authenticated pepper_required flag, and report "this container requires a pepper" rather than a generic authentication failure (src/crypto.rs:216).

The cost is explicit: the password alone is enough to learn that a v4 container is present and whether it demands a pepper. The pepper protects the payload, not the payload's existence. Do not treat --pepper-required as a mechanism for hiding that a second secret is in play.

Payload Indistinguishability (V4 Hardened Pipeline)

V4 hardens what the payload looks like to an analyst who has already decoded the channel. It does not hide the channel itself — see Detectability.

  1. Random carrier padding — every non-empty line in the carrier gets steganographic content (real data OR random padding), eliminating the statistical boundary between "message lines" and "clean lines"
  2. Outer AEAD encryption — the entire embedded bitstream is encrypted with an Argon2-derived key + XChaCha20-Poly1305, so the decoded bytes are indistinguishable from uniform random noise
  3. Constant-size containers — payloads are padded to fixed-size buckets (multiples of 64 bytes), masking the actual message length
  4. Plaintext compression — deflate compression before encryption reduces the data footprint
  5. Stripped wire format — no magic bytes or header length on the wire (saves 9 bytes, removes ASCII signatures)
  6. Packed binary header — 49-byte binary header with log2-encoded KDF params (was 266-byte JSON)

What is measured, and where

web_demo/stress_test.mjs decodes the zero-width channel, reassembles the recovered bytes, and checks them for uniformity over 20 embed/extract rounds:

Measurement Result
Unique byte values in the decoded bitstream 256 / 256
Chi-squared vs. uniform over 256 buckets ≈ 255 (test threshold < 350; expected value for a uniform source is 255)
Carrier line coverage 100% of non-empty lines carry channel content

Read those numbers precisely:

  • The chi-squared figure is computed after the channel has been located and decoded. It confirms the outer AEAD produces uniform output — which is what any correct AEAD must do. It says nothing about how hard the channel is to find.
  • The 100% coverage figure removes the message/padding boundary inside the channel. It is simultaneously the strongest signal that a channel is present at all: ordinary text contains no zero-width characters, and a v4 carrier contains them on every non-empty line.
  • Nothing in tests/ measures detectability of the channel, and no claim of undetectability is made here.

Steganography Modes

SNOW2 supports multiple embedding strategies.

Recommendation: For reliable day-to-day use, prefer websafe-zw mode or work with file-based carriers that won't be processed by editors, mail clients, or CI tools. Reserve classic-trailing for educational purposes, controlled environments, or when faithfulness to the original SNOW is desired.

classic-trailing

  • Trailing spaces and tabs encode bits at the end of each line
  • Direct homage to original SNOW
  • Most elegant
  • Most fragile — trailing whitespace is silently stripped by most text editors, Git hooks, linters, formatters, email systems, and CI pipelines. If the carrier passes through any of these, the payload is destroyed.

websafe-zw (recommended)

  • Zero-width Unicode character embedding (U+200B, U+200C)
  • 8 bits per line — each carrier line can hold a full byte, dramatically reducing carrier size
  • Survives copy/paste, most editors, and many messaging apps
  • Better suited for browser/WASM usage
  • Some platforms may strip zero-width characters (Unicode normalization, clipboard sanitizers)

Important Limitations

Whitespace steganography is inherently fragile, and the channel is not hard to find. V4 hardens the payload (see Payload Indistinguishability); it does not make a modified carrier look unmodified.

Detectability

  • A grep for U+200B / U+200C, a cat -A, or any Unicode-aware linter finds the channel immediately. Neither mode resists an analyst who is looking for it.
  • V4's 100% line coverage makes this easier, not harder. Ordinary text has no zero-width characters and no trailing whitespace; a v4 carrier has them on every non-empty line. That uniformity is itself the fingerprint.
  • What V4 buys is that once the channel is found, the bits give nothing up: no magic bytes, no length field, no message/padding boundary, no plaintext.
  • Plainly: casual readers see ordinary text; a motivated analyst who looks for the channel finds the channel.

Trailing whitespace (classic-trailing mode)

  • Most text editors have "trim trailing whitespace on save" enabled by default — VS Code, JetBrains, Vim, and others will silently destroy the payload on save
  • Git can be configured to strip trailing whitespace (core.whitespace)
  • Linters, formatters, CI tools, and email systems routinely normalize whitespace
  • This is the most fragile mode, but also the most faithful to the original SNOW
  • For reliability, use websafe-zw mode or exchange carriers as untouched files

Zero-width characters (websafe-zw mode)

  • More tolerant of copy/paste than trailing whitespace
  • Survives in many browsers, rich-text editors, and messaging apps
  • Can still be stripped by: Unicode normalization (NFKC/NFKD), some messaging platforms, clipboard sanitizers, or HTML rendering
  • Not "bulletproof" — just "more resilient"

General

  • If the carrier changes in any way, extraction will fail — this is by design (integrity is mandatory via AEAD)
  • File modification metadata (timestamps, sizes) may reveal that a file has been altered
  • A forensic analyst comparing the original carrier to the modified carrier can detect embedding

Use the scan command to check a carrier before embedding. It reports:

  • Stego capacity (raw bits and estimated payload max after container overhead)
  • Line ending format (LF vs CRLF vs mixed)
  • Tab character warnings (editor auto-expansion risk)
  • Existing trailing whitespace or zero-width characters
  • Consecutive marker run detection (signs of existing embedded data)
  • CRLF normalization risk warnings
  • Per-mode fragility notes

Quick Start (CLI)

1) Build

cargo build --release
./target/release/snow2 --help

2) Generate a carrier (recommended for demos)

classic-trailing embeds 1 bit per non-empty line; websafe-zw embeds 8 bits per line (a full byte), so it needs far fewer lines.

A helper script is included:

chmod +x scripts/make_carrier.sh
./scripts/make_carrier.sh 6000 carrier.txt

3) Embed a message

snow2 embed \
  --mode websafe-zw \
  --carrier carrier.txt \
  --out out.txt \
  --message "hello snow2" \
  --password "pw"

Tip: websafe-zw is recommended for most use cases — it survives copy/paste and editor saves far better than classic-trailing. Use classic-trailing when you want exact fidelity to the original SNOW behaviour.

4) Extract

snow2 extract \
  --mode websafe-zw \
  --carrier out.txt \
  --out recovered.bin \
  --password "pw"

cat recovered.bin

5) Scan a carrier

Inspect a carrier file for capacity, existing embedded data, or corruption risks:

snow2 scan --carrier carrier.txt

6) Best-effort file shredding

Overwrite and remove a file (see caveats below):

snow2 shred --path sensitive.txt --passes 3

Note: File shredding is best-effort. On SSDs with wear-leveling, CoW filesystems (btrfs, ZFS), or journaling filesystems, overwritten data may persist in spare blocks, snapshots, or journals. For high-assurance deletion, use full-disk encryption and destroy the key.

Quick-Reference Cheat Sheet

# Embed (websafe-zw, recommended)
snow2 embed -m websafe-zw -c carrier.txt -o out.txt --message "secret" --password "pw"

# Embed from file
snow2 embed -m websafe-zw -c carrier.txt -o out.txt --input secret.bin --password "pw"

# Embed with pepper + policy
snow2 embed -m websafe-zw -c carrier.txt -o out.txt --message "secret" \
  --password "pw" --pepper "signal" --pepper-required

# Extract
snow2 extract -m websafe-zw -c out.txt -o recovered.bin --password "pw"

# Extract with pepper
snow2 extract -m websafe-zw -c out.txt -o recovered.bin --password "pw" --pepper "signal"

# Scan carrier for capacity & risks
snow2 scan --carrier carrier.txt

# Classic-trailing mode
snow2 embed -m classic-trailing -c carrier.txt -o out.txt --message "secret" --password "pw"
snow2 extract -m classic-trailing -c out.txt -o recovered.bin --password "pw"

Security Options (CLI)

Pepper / Signal Key

Use a second secret:

snow2 embed \
  --mode classic-trailing \
  --carrier carrier.txt \
  --out out.txt \
  --message "hello" \
  --password "pw" \
  --pepper "signal key"

Extract must include the same pepper:

snow2 extract \
  --mode classic-trailing \
  --carrier out.txt \
  --out recovered.bin \
  --password "pw" \
  --pepper "signal key"

Require pepper by policy

This forces "password + pepper" for anyone who tries to decrypt later:

snow2 embed \
  --mode classic-trailing \
  --carrier carrier.txt \
  --out out.txt \
  --message "hello" \
  --password "pw" \
  --pepper "signal key" \
  --pepper-required

If someone tries to extract without the pepper, it will fail.

Tune Argon2id (KDF)

Defaults are reasonable for interactive use, but you can harden:

Flag Default Description
--kdf-mib 64 Memory cost in MiB (min 8, max 512)
--kdf-iters 3 Iterations / time cost (min 1, max 64)
--kdf-par 1 Parallelism (min 1, max 16)

Example:

snow2 embed \
  --mode classic-trailing \
  --carrier carrier.txt \
  --out out.txt \
  --message "hello" \
  --password "pw" \
  --pepper "signal key" \
  --pepper-required \
  --kdf-mib 128 \
  --kdf-iters 4 \
  --kdf-par 1

Post-Quantum Cryptography (Optional)

SNOW2 supports hybrid post-quantum encryption via the optional pqc feature flag.

When enabled, containers use a Version 2 format with:

  • Kyber1024 — lattice-based key encapsulation, via pqcrypto-kyber 0.8
  • Dilithium5 — lattice-based digital signatures, via pqcrypto-dilithium 0.5
  • Hybrid encryption — Kyber KEM shared secret → HKDF → XChaCha20-Poly1305
  • Authenticated containers — every PQC container is signed with Dilithium5
  • Encrypted key storage — secret keys can be encrypted at rest with password-derived AEAD
    • Versioned format (SNOW2EK\0 magic + version byte) for future evolution
    • Version byte authenticated as AEAD AAD to prevent downgrade attacks
  • Hardened file permissions — secret key files written with 0o600 (Unix) via atomic rename

PQC mode does not use passwords. Instead, you generate a keypair and use key files.

PQC carriers do not get the v4 hardening. The v4 outer AEAD is keyed by Argon2id over the password, and PQC mode has no password, so PQC containers are embedded with the pre-v4 bitstream: CRC-32 framing, no constant-size bucket padding, and no random fill of unused lines. Two consequences follow — the payload length is observable from how many lines carry markers, and only those lines carry content, so the message/padding boundary that v4 removes is visible again. Keying the outer layer from the Kyber shared secret would fix this, but that is a new container version rather than a patch. Until then, treat PQC mode as protecting confidentiality against a future quantum adversary, not as the stronger steganographic channel.

These are the NIST round-3 parameter sets, not the final standards. pqcrypto-kyber and pqcrypto-dilithium wrap PQClean's Kyber and Dilithium, which predate FIPS 203 (ML-KEM) and FIPS 204 (ML-DSA) and differ from them in key derivation and hashing. SNOW2 v2 containers are therefore not interoperable with ML-KEM / ML-DSA implementations, and should not be described as NIST-standardized. Migrating would mean moving to pqcrypto-mlkem / pqcrypto-mldsa and minting a new container version.

Build with PQC support

cargo build --release --features pqc

Generate a PQC keypair

snow2 pqc-keygen --pk-out key.pk --sk-out key.sk
# With encrypted secret key:
snow2 pqc-keygen --pk-out key.pk --sk-out key.sk --sk-password "my password"

Embed with PQC

snow2 embed \
  --mode classic-trailing \
  --carrier carrier.txt \
  --out out.txt \
  --input secret.txt \
  --pqc-pk key.pk

Extract with PQC

snow2 extract \
  --mode classic-trailing \
  --carrier out.txt \
  --out recovered.txt \
  --pqc-sk key.sk
# If key is encrypted:
snow2 extract \
  --mode classic-trailing \
  --carrier out.txt \
  --out recovered.txt \
  --pqc-sk key.sk \
  --pqc-sk-password "my password"

Note: PQC containers are significantly larger than password-based containers (~10 KB overhead for Kyber ciphertext + Dilithium signature). Ensure your carrier has enough lines.


Container Format

SNOW2 supports multiple container versions. The default is now Version 4 (hardened).

Version 4 (hardened, default)

Stripped wire format — no magic bytes or header length on the wire:

[VERSION=4 (1)] [BINARY_HEADER (49)] [CIPHERTEXT]

The 49-byte binary header contains:

[flags (1)] [m_cost_log2 (1)] [t_cost u16 LE (2)] [p_cost (1)] [salt (16)] [nonce (24)] [plaintext_len u32 LE (4)]

Flags byte: bit 0 = pepper_required, bit 1 = compressed, bits 2-3 = mode, bits 4-5 = AEAD algorithm, bits 6-7 = reserved (must be zero).

The header is authenticated as AEAD additional data (AAD). Plaintext is optionally deflate-compressed before encryption.

The v4 embed pipeline wraps this further:

container → [real_len u32 LE (4)][container][random_padding] → constant-size bucket
→ outer AEAD (Argon2-derived key + XChaCha20-Poly1305) → raw bits → embed + random-fill ALL lines

Version 1 (legacy classic)

[MAGIC "SNOW2" (5)] [VERSION=1 (1)] [HEADER_LEN u32 LE (4)] [HEADER_JSON] [CIPHERTEXT]

Password-based, Argon2id → HKDF → XChaCha20-Poly1305 AEAD. Header JSON authenticated as AAD.

Version 3 (compact binary)

[MAGIC "SNOW2" (5)] [VERSION=3 (1)] [HEADER_LEN u32 LE (4)] [BINARY_HEADER (57)] [CIPHERTEXT]

Same crypto as v1, but uses a 57-byte binary header instead of JSON. Deflate-compressed on wire.

Version 2 (PQC)

[MAGIC] [VERSION=2] [HEADER_LEN] [HEADER_JSON] [SIG_LEN u16 LE] [SIGNATURE] [CIPHERTEXT]

Hybrid Kyber1024 + XChaCha20-Poly1305, signed with Dilithium5.

Encrypted Secret Key Format (PQC)

[SNOW2EK\0 (8)] [VERSION (1)] [SALT (16)] [NONCE (24)] [AEAD CIPHERTEXT]

Version byte is authenticated as AEAD AAD alongside the magic, preventing downgrade attacks.

Backward Compatibility

Extraction automatically detects the container version. V4 outer decryption is tried first; if it fails, the legacy CRC-framed path handles v1/v3 containers.


Security Model

What SNOW2 protects against

  • Casual readers: the carrier renders as ordinary text; nothing is visible when the file is read
  • Analysis of the decoded bitstream (v4): once the channel is decoded, the bytes are indistinguishable from uniform random (chi-squared ≈ 255); every carrier line carries content, so there is no message/padding boundary inside the channel
  • Wrong password/pepper: AEAD authentication fails — no partial decryption
  • Carrier tampering: Outer + inner AEAD catch any modification; legacy CRC-32 catches stego corruption in old containers
  • Header tampering: Binary header is AEAD AAD — any modification causes auth failure
  • Message length analysis (v4): Constant-size padding masks actual payload size
  • KDF bounds: Extraction-side bounds reject absurd KDF parameters from hostile containers
  • Weak KDF at embed time: Embedding validates KDF params against the same bounds
  • Key file exposure: PQC secret keys can be encrypted at rest with password-derived AEAD
  • File permission leaks: Sensitive files written with restricted permissions via atomic rename

What SNOW2 does NOT protect against

  • Channel detection: a scan for zero-width characters or trailing whitespace locates the channel in either mode. V4's full-line coverage makes a modified carrier more uniform than natural text, not less. See Detectability.
  • Concealing that a pepper is required: the outer AEAD key is password-only by design, so the password alone reveals that a v4 container is present and whether it is marked pepper_required. See Why Two AEAD Layers.
  • Active carrier modification: If someone can modify the carrier text (trim whitespace, normalize Unicode), extraction will fail. This is by design — integrity is mandatory.
  • Carrier comparison: If an adversary has both the original carrier and the modified carrier, they can diff the files and detect embedding.
  • Traffic analysis: SNOW2 does not hide the fact that a file has been modified (file size changes, metadata, etc.)
  • Guaranteed secure deletion: File shredding is best-effort. SSDs, CoW filesystems, and journals may retain data.
  • WASM security boundaries: The browser demo uses zeroize-on-drop but cannot mlock memory

Important security caveats

  • Trailing whitespace is fragile. Most text editors (VS Code, vim, Sublime, etc.) have settings to strip trailing whitespace on save. Git hooks, linters, and formatting tools often do the same. If the carrier passes through any of these, embedded data is destroyed. This is inherent to whitespace steganography, not a SNOW2 bug.

  • Zero-width Unicode is more resilient, but not bulletproof. Zero-width characters (U+200B, U+200C) survive copy/paste in many contexts (browsers, rich-text editors, some messaging apps). However, Unicode normalization (NFC/NFD/NFKC/NFKD), some messaging platforms (Slack, Discord), and clipboard sanitizers may strip them.

  • CRC-32 is not a cryptographic integrity check (legacy only). In v1/v3 containers, the CRC-32 in the bitstream framing catches accidental corruption early. V4 containers use outer AEAD instead, which provides full cryptographic integrity over the entire bitstream.

  • Shred/wipe is best-effort only. On SSDs with wear-leveling, CoW filesystems (btrfs, ZFS), or journaling filesystems, overwritten data may persist. For high-assurance deletion, use full-disk encryption and destroy the key.

  • PQC is optional and experimental. Post-quantum crypto (Kyber1024 + Dilithium5) is available as an optional feature for users who want it. It is not the default identity of the tool. PQC containers are significantly larger (~10 KB overhead) and use different trust assumptions (keypair-based, not password-based). The parameter sets are NIST round-3 Kyber and Dilithium, not FIPS 203 ML-KEM / FIPS 204 ML-DSA, and are not interoperable with them.

  • The browser/WASM demo is not the main security boundary. It is a convenience UI for demonstration. In WASM, mlock is unavailable, and JavaScript's garbage collection may leave copies of sensitive data in memory.


Building

Standard release binary

cargo build --release

With PQC support

cargo build --release --features pqc

Static Linux build (musl)

rustup target add x86_64-unknown-linux-musl
sudo apt-get install -y musl-tools
cargo build --release --target x86_64-unknown-linux-musl

Run tests

# 178 tests, default features
cargo test --no-fail-fast -- --test-threads=2

# 188 tests, adds tests/pqc_roundtrip.rs
cargo test --features pqc --no-fail-fast -- --test-threads=2

--no-fail-fast matters: without it cargo test stops at the first failing binary, so a break in an early suite hides everything after it.

Both configurations are gated in CI as separate jobs, along with cargo clippy --all-targets -- -D warnings (with and without --features pqc) and cargo fmt --check. Note that tests/steganalysis.rs asserts the detectability properties described under Detectability — if you change the embedding so the channel becomes harder to find, those tests are meant to fail and prompt a README update.


Browser / WASM Demo

SNOW2 compiles to WebAssembly for fully client-side encryption in the browser.

The web_demo/ folder contains a static web UI that loads the SNOW2 Rust core via WASM. It uses the websafe-zw mode (zero-width embedding), which is more tolerant of browser copy/paste behavior.

Features:

  • Fully client-side — no server-side crypto, all processing in the browser
  • Configurable Argon2id KDF parameters
  • Pepper / Signal Key support with optional pepper-required policy
  • Carrier generation and download
  • Extract with UTF-8 preview and raw base64 data

Build the WASM package

cargo install wasm-pack
cd snow2_wasm
wasm-pack build --target web --out-dir web_demo/pkg
cp -r web_demo/pkg ../web_demo/pkg

Serve locally

python3 -m http.server 8000 -d web_demo

Then open http://localhost:8000.

Important: The browser demo is convenience UI, not the security boundary. Users should understand whitespace normalization risks when copy/pasting. The WASM environment cannot use mlock and provides weaker memory protections than native builds.


What has changed vs. original SNOW

The original SNOW (by Matthew Kwan, late 1990s) was a C program that:

  • Used ICE encryption (a block cipher by the same author)
  • Encoded data in trailing whitespace (tabs and spaces)
  • Ran on Unix and Windows
  • Was simple, elegant, and educational

SNOW2 preserves the spirit but is a complete rewrite:

  • Language: Rust (memory-safe, no buffer overflows)
  • Encryption: Dual-layer AEAD — inner XChaCha20-Poly1305 (Argon2id key + pepper) + outer XChaCha20-Poly1305 (Argon2id key, password-only)
  • Key derivation: Argon2id + HKDF-SHA256 (memory-hard, domain-separated)
  • Integrity: Dual AEAD authentication (outer + inner), legacy CRC-32 for old containers
  • Payload indistinguishability: Constant-size padding, random carrier fill, and outer encryption make the decoded bitstream indistinguishable from uniform random (chi-squared ≈ 255). The channel itself remains detectable in both modes.
  • Modes: Classic trailing whitespace (tribute mode, 1 bit/line) + zero-width Unicode (web-friendly, 8 bits/line)
  • Optional PQC: Hybrid Kyber1024 + Dilithium5 (NIST round-3 parameter sets, not FIPS 203/204)
  • Secure memory: mlock'd buffers with dual guard pages, zeroize-on-drop, MADV_DONTDUMP

What remains the same:

  • Text-steganography identity ("hide in plain sight")
  • Simplicity and educational clarity
  • Whitespace as the invisible channel
  • Single-binary CLI tool
  • The charm of hiding messages where nobody looks

Project Structure

src/
  main.rs             CLI entry point (clap)
  lib.rs              Library API (embed / extract / embed_with_options)
  config.rs           EmbedOptions, EmbedSecurityOptions, PqKeys
  container.rs        SNOW2 container format v1/v3/v4 (seal / open / serialize / parse)
  crypto.rs           AEAD, KDF (Argon2id + HKDF), outer encryption, random bytes
  secure_mem.rs       SecureVec (mlock + guard pages on native, zeroize on WASM)
  secure_fs.rs        Atomic writes, permission hardening, best-effort shredding
  pqc.rs              Post-quantum crypto: Kyber1024 + Dilithium5 [optional]
  stego/
    mod.rs            Bit/byte conversion (raw + CRC-32 framing for legacy)
    classic_trailing.rs   Trailing whitespace steganography (+ v4 random padding)
    websafe_zw.rs         Zero-width Unicode steganography (+ v4 random padding)
snow2_wasm/           WASM crate for browser demo
  src/lib.rs          wasm-bindgen bindings
web_demo/             Static web UI (HTML/CSS/JS + WASM pkg)
tests/
  roundtrip.rs        Classic embed/extract roundtrip tests
  pepper_policy.rs    Pepper policy, KDF bounds, embed-side validation,
                      malformed container rejection tests
  pqc_roundtrip.rs    PQC keygen + embed/extract roundtrip [requires --features pqc]
  negative_edge_cases.rs  Malformed input, corruption, boundary tests
  cross_platform.rs   CRLF/LF, Unicode, platform survivability tests
  adversarial.rs      Hostile inputs, truncation, cross-mode confusion
  robustness.rs       Carrier mangling, KDF profiles, atomic writes
  websafe_zw_platform.rs  Zero-width survivability across platform behaviours
  steganalysis.rs     Executable form of the README's steganalysis claims:
                      payload uniformity AND channel detectability
fuzz/
  fuzz_targets/       Fuzz targets for container parse, bitstream, stego extract
scripts/
  make_carrier.sh     Helper to generate carrier files

Tribute

Original SNOW was written by Matthew Kwan and released under the GNU GPL.

SNOW2 is an independent modern rewrite created in admiration of that work. It does not reuse original code but preserves the idea, the elegance, and the educational spirit.


License

GPL-3.0-or-later

About

Browser-based tribute to Matthew Kwan's SNOW steganography tool — hide encrypted messages inside ordinary text using whitespace. XChaCha20-Poly1305 · Argon2id · HKDF. Modern Rust compiled to WASM.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages