Skip to content

Add Grain: hide an AES-256-GCM-encrypted file inside a PNG - #36

Merged
tatangharyadi merged 2 commits into
mainfrom
feat/tatangharyadi/grain-steganography
Aug 16, 2026
Merged

Add Grain: hide an AES-256-GCM-encrypted file inside a PNG#36
tatangharyadi merged 2 commits into
mainfrom
feat/tatangharyadi/grain-steganography

Conversation

@tatangharyadi

Copy link
Copy Markdown
Owner

Summary

  • Adds Grain (F07): client-side, in-browser file steganography — AES-256-GCM encryption behind a passphrase (PBKDF2-derived key, 600,000 iterations), then LSB steganography to hide the ciphertext in a carrier PNG's pixels. Nothing is ever uploaded; grain.html sets connect-src 'none'.
  • New files: grain.html, css/grain.css, js/grain.js, scripts/check_grain.py (+ fixtures), .claude/rules/grain.md, specs/F07_GRAIN.md.
  • Updated: sitemap.xml, AGENTS.md (sixth JS-file exception + new verification step), specs/PRD.md (architecture diagram, F07 section, verification table), index.html (colophon link), .github/workflows/ci.yml (new CI step).
  • All mechanical CI checks pass locally: check_htmx.py, check_corpus.py, check_palette.py, check_repo.py, check_grain.py, gen_game_data.py --check, build_game.sh --check.

Decisions that differ from the brief

  • ctLen is computed as plaintext.length + 16 before encrypting (single crypto.subtle.encrypt() call), rather than encrypting twice to measure ciphertext length — AES-GCM ciphertext length depends only on plaintext length, not on additionalData.
  • Added grain--payload-name and grain--stego-name status paragraphs: the visually-hidden file inputs clip the native "chosen file" text, so these are the only place a file selection is confirmed to screen readers and sighted users alike.
  • Added a filename.txt fixture alongside the other committed fixtures.
  • .grain--page uses ordinary block layout, not the display: contents hide/show pattern #iris--page uses — both encode and decode sections are always visible here, so there's nothing to toggle.
  • Added a colophon paragraph in index.html linking to Grain. Investigated whether the existing "First-party JavaScript / One module, js/ask.js" colophon readout was stale now that Grain adds a sixth site-wide JS file — it isn't: that readout is scoped to index.html's own script usage specifically, which is unchanged.
  • Incidentally fixed a pre-existing stale bullet in specs/PRD.md's "Out of scope" section (it already omitted js/iris.js before this PR) while updating the JS file count for Grain.

What's NOT done yet

  • No human/browser verification: full encode→decode round trip, wrong-passphrase vs. tampered-image failure parity, clean network panel (connect-src 'none' holds), keyboard/focus traversal, both colour schemes, all three breakpoints, Lighthouse.
  • The "Human"-marked acceptance criteria in specs/F07_GRAIN.md (F07-AC01, AC02, AC03's live half, AC04, AC05, AC06's live half, AC07, AC09) are still open.
  • Two "Deferred" items in specs/F07_GRAIN.md: measured stego-PNG size and PBKDF2 timing on real hardware.

Test plan

  • scripts/check_grain.py passes (regression pin against committed fixtures — not a proof; stdlib Python has no AES-GCM to verify the real encryption against)
  • check_htmx.py, check_corpus.py, check_palette.py, check_repo.py, gen_game_data.py --check, build_game.sh --check all pass
  • Human: full round trip in a real browser (F07-AC01)
  • Human: clean network panel, both colour schemes, keyboard traversal, three breakpoints

Client-side only — PBKDF2 key derivation, AES-256-GCM encryption behind
a passphrase, then least-significant-bit steganography to write the
ciphertext into a carrier PNG's pixels. Nothing is ever uploaded;
grain.html sets connect-src 'none'.

Sixth argued exception to the JS-file rule, alongside js/ask.js,
js/game.js, js/mocap.js, js/mocap-retarget.js and js/iris.js (AGENTS.md).
scripts/check_grain.py pins the header format, KDF and ciphertext
against committed fixtures generated with Node's webcrypto and a
from-scratch stdlib PNG encoder, since stdlib Python has no AES-GCM to
verify the encryption itself against.
All Human-evidence acceptance criteria have now been run against a real
browser: encode/decode round trip, wrong passphrase, tampered image,
undersized carrier, out-of-range KDF header, connect-src 'none' holding
even against the page's own blob URLs, keyboard traversal, and a
Lighthouse audit in both colour schemes.
@tatangharyadi
tatangharyadi merged commit 573a443 into main Aug 16, 2026
2 checks passed
@tatangharyadi
tatangharyadi deleted the feat/tatangharyadi/grain-steganography branch August 16, 2026 09:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant