How It Works
This page describes the technical implementation of Stegstr's steganographic pipeline: how data is hidden in images, encrypted, and exchanged.
Steganography: DWT (Haar 2D)
Stegstr uses Discrete Wavelet Transform (Haar 2D) steganography rather than raw pixel LSB. This offers better robustness and visual quality.
- Transform: A 2×2 Haar wavelet splits each block into four sub-bands: LL (approximation), LH, HL, HH (detail coefficients). Stegstr embeds data in the LSB of the LH (horizontal detail) coefficients for all three color channels (R, G, B).
- Capacity: For a tile of width tw × height th, capacity is
(tw/2) × (th/2) × 3bits (3 bits per 2×2 block, one per channel). A 256×256 tile provides about 6 KB of payload space. - Tile-based redundancy: The payload is embedded in multiple 256×256 tiles across the image. If the image is cropped, the decoder can still recover the data by scanning tiles with a sliding window (step 128 px).
Payload Format (Raw Layer)
The data embedded in the image has this structure:
- Magic:
STEGSTR(7 bytes, ASCII) — identifies Stegstr data - Length: 4 bytes, big-endian — length of the payload in bytes
- Payload: The actual bytes (usually encrypted; see below)
If the payload is decrypted, it is UTF-8 JSON:
{ "version": 1, "events": [ ... ] }
The events array contains Nostr-style events: kind 1 (notes), kind 4 (DMs), kind 0 (profiles), and others. This is the Stegstr bundle.
Encryption Layer
Before being embedded, the bundle is typically encrypted. Two modes:
Open (Any Stegstr user)
- Uses AES-GCM with a key derived from
SHA-256("stegstr-decrypt-v1"). - Encrypted format:
STEGSTR1(8 bytes) + version byte (1) + 12-byte IV + ciphertext. - Any Stegstr installation can decrypt—no Nostr key required.
Recipients (Specific pubkeys only)
- The bundle is encrypted with a random symmetric key K.
- K is encrypted per recipient using NIP-04 (ECDH + shared secret).
- Structure:
{ "t": "r", "s": senderPubkey, "r": [{ "p": pubkey, "k": encryptedK }], "c": base64(iv+ciphertext) }. This envelope is then app-encrypted (outer AES-GCM) before embedding. - Only listed recipients (or the sender) can decrypt. Include yourself if you want to open it later.
Detect Flow
When you load an image with Detect:
- Load PNG, crop to even dimensions (required for DWT).
- Decode payload from DWT (full image first; if not found, scan 256×256 tiles with step 128).
- Parse magic and length, extract payload bytes.
- If payload starts with
STEGSTR1, decrypt (app key or recipients envelope). - Parse JSON bundle, merge events into the feed (and DMs).
Embed Flow
When you save to an image with Embed:
- Collect current events from the feed and local store (notes, DMs, profiles, etc.).
- Serialize to JSON:
{ "version": 1, "events": [...] }. - Encrypt (open or recipients mode).
- Prepend magic + 4-byte length, embed into cover image via DWT (tile-based).
- Save as PNG.
Image Requirements
- Format: PNG (lossless). Avoid JPEG or other lossy formats—recompression can destroy the hidden data.
- Size: Must be at least 2×2 (after cropping to even dimensions). Larger images support more capacity and redundancy.
- Editing: Avoid cropping, resizing, or re-saving in other editors after embedding; this can corrupt the payload.