# Letterpress: handwritten BMP steganography

Run `./start.sh`, then open http://127.0.0.1:8765. The app uses the existing parent
Python environment in this workspace. For a standalone copy, run `./setup.sh`
once (Python 3.12 recommended), then `./start.sh`. Dependencies download during
setup; generation and decoding run entirely locally. Use `--device cpu` or
`--device cuda` to select a device. The default is automatic.

## Using the app

Each editable field has a live status indicator beside it: a green check means
valid, and an orange exclamation mark explains what needs attention. Generate
is enabled when all encoding fields pass, the padded text fits the page, and
the local model is ready. Decoder fields have their own checks for Analyze.
An empty secret is valid and fills the payload with zero bits.

1. Enter uppercase cover text containing only A–Z, 0–9, spaces and newlines.
   Each space is a white tile; each newline starts a new line.
2. Enter the secret as ASCII text (character values 0–127), stored as eight
   bits per character. Spaces, newlines, punctuation and `=` are accepted;
   non-ASCII characters are rejected.
3. Enter an encoding key and IV, each exactly 16 ASCII characters, interpreted
   literally as 16 bytes. These are not hexadecimal fields or passwords that
   undergo a key derivation function. Choose a fresh IV for each new letter
   encoded with the same key.
4. Generate. The app counts only letters/digits, excluding layout whitespace,
   and appends the necessary prefix of `ENDOFLETTER` (0–9 characters) to reach a
   multiple of ten. The padded text appears in the cover editor.
5. Every ten generated glyphs carry 102 payload bits. Capacity is
   `floor(number_of_blocks * 102 / 8)` ASCII characters: 12 for one block,
   25 for two, 38 for three, and 51 for four. Characters can span block
   boundaries. Short secrets receive trailing NUL characters, then 0–6 zero
   bits complete the final block. Excessive secrets are rejected. The UI
   reports the number of NUL fill characters.
6. The one preview is decoded directly from the actual downloadable 24-bit BMP.
   Its glyphs have black ink on white paper. Tile sizes are 28 or 56 pixels;
   exact nearest-neighbor doubling retains every native pixel.
7. In the frame below, use the separate decoding key and IV and press Analyze.
   Generation copies its credentials there for convenience; you can change them.
   The app displays all recovered characters, including NUL fill, plus bit errors,
   character errors, and bit errors for each ten-glyph block. Green means every
   payload bit matches; red means at least one differs.

Editing the cover, secret or encoding credentials does not alter an already
rendered letter. Analyze always uses the saved generation snapshot. There is
only one letter preview. The server retains at most two letter snapshots in
memory; they expire on restart or after subsequent generations. Downloaded BMPs
contain pixels only, without keys, IVs, secrets or layout metadata. This version
analyzes the in-session letter using its known character labels and positions;
it does not import arbitrary BMPs or infer cover characters with OCR.

## Exact protocol, version 2 (8-bit ASCII)

- Serialize each ASCII character as an eight-bit byte, most significant bit
  first. Pad with zero bits to fill the available contiguous 102-bit blocks.
  The turbo, AES mask, glyph generation and analysis steps are unchanged.
- `ccsds_turbo_102_640.py` is an exact bundled copy of the supplied source.
  It implements an **experimental shortened** CCSDS-inspired rate-1/6 turbo code,
  not a standards-compliant CCSDS frame/interleaver. It emits 636 mother-code
  bits plus four repeated systematic bits = 640 channel bits. Its fixed
  interleaver seed is 13100640; decoder defaults: 12 iterations, damping 0.8.
- AES-128-CTR encrypts zero bytes to produce one continuous mask over the whole
  letter, starting with the supplied 128-bit IV/counter and advancing the full
  counter in big-endian order. There is no restart at glyph or turbo boundaries.
  Expand each mask byte most significant bit first. One ten-glyph block uses
  640 mask bits = 80 bytes = five AES blocks.
- XOR the turbo bits with the mask, then reshape in reading order into 64-bit
  vectors. Channel bit 1 becomes `+sqrt(2/pi)`; 0 becomes `-sqrt(2/pi)`.
  The amplitude is 0.7978845608028654. The epoch-10 EMA generator produces a
  28×28 glyph for its known class. Convert to inverted, rounded uint8 pixels.
- For analysis, crop the actual saved BMP; undo integer enlargement and restore
  the generator intensity convention with `1 - pixel/127.5`. The corresponding
  label's best analyzer produces 64 logits `log(P(1)/P(0))`.
- Convert to the codec's opposite LLR convention, then reverse the mask by
  flipping each masked LLR's sign: `LLR = -logit * (1 - 2*mask_bit)`.
  Turbo-decode consecutive 640-LLR blocks to 102 bits, concatenate the decoded
  blocks, and regroup complete eight-bit bytes. Ignore only the final partial
  byte for text display; its bits are still included in the error count.
  Recovered erroneous bytes above 127 are displayed using Latin-1 so they
  remain visible without losing bit information. NUL fill is present in the
  recovered field but may be invisible.
- The original payload is consulted **only after** neural inference and turbo
  decoding, to calculate bit/character errors. No original latent vectors or
  transmitted channel bits are retained for decoding. Error checking compares
  every padded payload bit. This is an experiment comparison, not an embedded
  authentication tag or a guarantee that new letters decode without errors.

AES implementation reference: [cryptography AES and CTR API](https://cryptography.io/en/latest/hazmat/primitives/symmetric-encryption/).

## Included models and dependencies

`models/generator.pt` contains the trained epoch-10 EMA generator and class map.
`models/analyzers/{0..9,A..Z}.pt` contain all 36 inference-only best checkpoints
from the completed 50-epochs-per-label experiment. Their selected epochs,
validation accuracies, source checkpoint paths and bundle SHA-256 checksums are
in `models/analyzers/manifest.json`. Optimizer states, image blobs and training
vectors are unnecessary and are not bundled. The training files remain intact
in the parent experiment directory.

`stego.py` handles the protocol, session snapshots, model loading and analysis.
`renderer.py` handles layout and BMP rendering. `server.py` serves only the
allowlisted static assets and local JSON API on 127.0.0.1. AES credentials are
not saved in sessions, downloads or application logs. Session reference secrets
remain only in memory for the requested error comparison. There is no API that
serves checkpoint files. Four analyzer models at a time are cached in memory.

First decoding may take longer because Numba compiles the turbo decoder.
The previous held-out native-image bit accuracy averaged 85.8993% across the
36 analyzers; individual predictions can be wrong. Turbo correction helps but
its end-to-end performance is not guaranteed for every text/key/secret.

## Verification

Run `../.venv/bin/python tests.py` from this directory, or `.venv/bin/python
tests.py` after standalone setup. Tests cover strict cover/secret/key validation,
padding and capacity, glyph/background layout, overflow, BMP format,
reproducibility, the NIST AES-CTR known-answer vector, continuous MSB-first mask
ordering, noiseless turbo recovery, correction of selected bit errors, actual
BMP generation and neural decoding across all 36 labels, wrong-key errors and
HTTP endpoints. `node --check static/app.js` checks frontend JavaScript syntax.

The actual all-label test uses 40 glyphs (36 labels plus `ENDO`) and 51 ASCII
characters (408 payload bits). Its result and BMP hash are saved in
`examples/stego_roundtrip.json`; the corresponding page is
`examples/stego_letter.bmp`. Protocol tests cover ASCII byte ordering, non-ASCII
rejection, capacity across byte/block boundaries, zero fill, and noiseless turbo
recovery. Integration tests check actual BMP analysis and wrong-key errors;
model predictions may still contain errors. No automated browser runner is
available here.
