A modern Rust tribute to Matthew Kwan's SNOW steganography tool.
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.
The original SNOW was brilliant because it taught two powerful concepts quickly:
- Encryption protects meaning.
- Steganography hides existence.
SNOW2 keeps that same spirit:
- Small
- Understandable
- Educational
- Respectful of the original design
But cryptographically modern.
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.
SNOW2 upgrades the crypto model completely.
- XChaCha20-Poly1305
- Large nonce space
- Misuse-resistant
- Authenticated encryption (confidentiality + integrity)
- 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
- 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
KdfParams::hardened()— 256 MiB / t=4 / p=1 for high-value secretsEmbedSecurityOptions::hardened()— hardened KDF + mandatory pepper
- 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
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.
- 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()callsmlockallto 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)
- 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
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.
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.
- 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"
- 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
- Constant-size containers — payloads are padded to fixed-size buckets (multiples of 64 bytes), masking the actual message length
- Plaintext compression — deflate compression before encryption reduces the data footprint
- Stripped wire format — no magic bytes or header length on the wire (saves 9 bytes, removes ASCII signatures)
- 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.
SNOW2 supports multiple embedding strategies.
Recommendation: For reliable day-to-day use, prefer
websafe-zwmode or work with file-based carriers that won't be processed by editors, mail clients, or CI tools. Reserveclassic-trailingfor educational purposes, controlled environments, or when faithfulness to the original SNOW is desired.
- 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.
- 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)
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.
- A
grepfor U+200B / U+200C, acat -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.
- 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-zwmode or exchange carriers as untouched files
- 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"
- 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
cargo build --release
./target/release/snow2 --helpclassic-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.txtsnow2 embed \
--mode websafe-zw \
--carrier carrier.txt \
--out out.txt \
--message "hello snow2" \
--password "pw"Tip:
websafe-zwis recommended for most use cases — it survives copy/paste and editor saves far better thanclassic-trailing. Useclassic-trailingwhen you want exact fidelity to the original SNOW behaviour.
snow2 extract \
--mode websafe-zw \
--carrier out.txt \
--out recovered.bin \
--password "pw"
cat recovered.binInspect a carrier file for capacity, existing embedded data, or corruption risks:
snow2 scan --carrier carrier.txtOverwrite and remove a file (see caveats below):
snow2 shred --path sensitive.txt --passes 3Note: 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.
# 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"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"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-requiredIf someone tries to extract without the pepper, it will fail.
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 1SNOW2 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-kyber0.8 - Dilithium5 — lattice-based digital signatures, via
pqcrypto-dilithium0.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\0magic + version byte) for future evolution - Version byte authenticated as AEAD AAD to prevent downgrade attacks
- Versioned format (
- 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-kyberandpqcrypto-dilithiumwrap 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 topqcrypto-mlkem/pqcrypto-mldsaand minting a new container version.
cargo build --release --features pqcsnow2 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"snow2 embed \
--mode classic-trailing \
--carrier carrier.txt \
--out out.txt \
--input secret.txt \
--pqc-pk key.pksnow2 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.
SNOW2 supports multiple container versions. The default is now Version 4 (hardened).
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
[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.
[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.
[MAGIC] [VERSION=2] [HEADER_LEN] [HEADER_JSON] [SIG_LEN u16 LE] [SIGNATURE] [CIPHERTEXT]
Hybrid Kyber1024 + XChaCha20-Poly1305, signed with Dilithium5.
[SNOW2EK\0 (8)] [VERSION (1)] [SALT (16)] [NONCE (24)] [AEAD CIPHERTEXT]
Version byte is authenticated as AEAD AAD alongside the magic, preventing downgrade attacks.
Extraction automatically detects the container version. V4 outer decryption is tried first; if it fails, the legacy CRC-framed path handles v1/v3 containers.
- 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
- 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
-
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,
mlockis unavailable, and JavaScript's garbage collection may leave copies of sensitive data in memory.
cargo build --releasecargo build --release --features pqcrustup target add x86_64-unknown-linux-musl
sudo apt-get install -y musl-tools
cargo build --release --target x86_64-unknown-linux-musl# 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.
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
cargo install wasm-pack
cd snow2_wasm
wasm-pack build --target web --out-dir web_demo/pkg
cp -r web_demo/pkg ../web_demo/pkgpython3 -m http.server 8000 -d web_demoThen 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.
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
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
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.
GPL-3.0-or-later