A card-based one-time keybook demo for teaching key distribution, one-time pads, stream ciphers, and the danger of key reuse.
DeckBook is an educational exhibit-style web app (Cipher Museum theme) that models this core idea:
The deck order is the key. The clue only tells you which key to use.
| Watch the cipher run | Crack a reused key | Play the intercept challenge |
|---|---|---|
![]() |
![]() |
![]() |
- Accessibility: 0 axe-core violations across WCAG 2.1 A/AA, enforced in CI
on the initial page, all reference panels expanded, and Challenge mode
(e2e/a11y.spec.ts). Full keyboard operation,
screen-reader labelling, and
prefers-reduced-motionsupport throughout. - Tested: 54 unit tests (Vitest) for all pure logic, plus 8 Playwright end-to-end tests (full encrypt→share→decrypt round-trip, challenge solve, and the accessibility audits). Both suites run on every push via CI.
- Lean & offline: fully client-side, no backend. The production bundle is ~34 KB JS + ~6 KB CSS gzipped, and installs as an offline-capable PWA.
- Lighthouse CI: lighthouse.yml runs Lighthouse (desktop preset, 3 runs) against the built site on every push and pull request, gating accessibility and reporting performance, best-practices, and SEO. Each run uploads a public report (URL in the job log).
This project is inspired by Solitaire-style manual encryption teaching material, including:
DeckBook is an educational demonstration, not production cryptography.
- Do not use this app to protect real secrets.
- Use modern, audited cryptographic tools for real security.
- One-time key material and key identifiers
- Why key reuse fails
- Why key distribution is hard
- Manual/physical keybook operational risks
- Why modern key exchange exists (including post-quantum KEM context)
The cipher is a Vigenere-style add/subtract over the 26 letters A–Z. The twist is that the keystream comes from a shuffled deck of cards.
-
Letters become numbers.
A = 0, B = 1, …, Z = 25. Spaces, digits, and punctuation are stripped before encryption. -
Cards become a keystream. Each card has a stable value
0..51(Ace of Spades = 0, King of Clubs = 51). Each card contributes one keystream letter:keystream[i] = deck[i].value mod 26One card per letter, so a full deck produces 52 keystream letters and a single deck key can encrypt at most 52 letters. Each of the 26 alphabet letters is hit by exactly two card values, so the keystream is uniformly distributed.
-
Encrypt by adding mod 26, decrypt by subtracting.
cipher[i] = ( plain[i] + keystream[i] ) mod 26 plain[i] = ( cipher[i] - keystream[i] + 26 ) mod 26
For messages longer than 52 letters, Advanced multi-deck mode consumes additional fresh decks in sequence (letters 0–51 use the first deck, 52–103 the second, and so on). Each deck is used exactly once.
Both sides must already hold the same private DeckBook. Only the index
code (a public label like LANTERN-42) and the ciphertext travel over
the public channel. The deck order itself is the secret.
The deck order is the key, so it has to be genuinely unpredictable. Generation is the part of DeckBook that is truly cryptographic-grade, even though the card cipher around it is deliberately educational. Four things guarantee the randomness (all in src/cipher.ts):
- A real cryptographic source. Every random number comes from the
Web Crypto CSPRNG,
crypto.getRandomValues(), seeded by the operating system's entropy. The predictableMath.random()is never used. - No modulo bias.
secureRandomIntdraws integers with rejection sampling: a naiverandomUint32 % 52is skewed because 2³² is not a multiple of 52, so it discards the "leftover" high values and only accepts from an exact multiple. Every value0…max-1is then exactly equally likely. - A provably-fair shuffle.
secureShuffleis a Fisher–Yates shuffle driven bysecureRandomInt. Given an unbiased RNG, all52!orderings are equally probable — it avoids the common "swap with any position" bug that skews the distribution. - Independent keys.
generateDeckBookshuffles a freshcreateStandardDeck()for every entry, so no two keys share a seed or state, and each gets a SHA-256 fingerprint so sender and receiver can confirm they hold the same deck order.
That's roughly 8.06 × 10⁶⁷ (= 52!) possible deck orders. The
randomness is genuinely strong — as strong as your device's CSPRNG. What
keeps DeckBook educational rather than production-grade is not the
randomness but the rest of the protocol: you still have to share the deck
secretly and never reuse a key. The same explanation is available in-app
under Generate DeckBook → "How are these decks made truly random?".
- Watch It Work — an animated visualizer that runs the cipher one card at a time. A card flips off the deck, becomes a keystream number, and shifts one letter of your message, with play / pause / step / speed controls. It is a sandbox and never consumes real keys.
- Live letter-frequency histograms — as you type in the Encrypt panel, two bar charts update in real time: your plaintext (spiky, with typical-English ghost bars) vs. your ciphertext (flat). The "no favorite letters" lesson, visible instantly.
- Key Reuse Attack Lab with crib dragging — encrypt two messages with the same key, then crack them by hand. Type a guessed word ("crib"), slide it along the ciphertext difference, and watch the other message leak out where your guess is right. Ranked "most English-looking positions" hints included. No key or deck order ever enters the attack — reuse alone leaks the plaintext.
- QR / share-link handoff — every encrypted message produces a QR code and a deep link carrying only the index code and ciphertext (never the deck order). Scan it on another device; that device decrypts only if it already holds the matching DeckBook. A real message crossing a real public channel.
- Printable physical deck sheet — a print stylesheet outputs the deck order as a clean sheet so you can arrange a real deck and verify a letter by hand.
- Challenge mode ("Eve's Intercept") — a shareable, deep-linkable CTF-style
puzzle. An operator reused one key for two messages; you play the
eavesdropper and recover both using crib dragging alone, with a live progress
meter and a confetti win state. Three puzzles from easy to hard; share a
specific one with
#play=<id>. - Story framing — a running Alice → Bob mission with Eve on the wire ties the steps, the simulator, and the challenge into one narrative.
- Installable, offline-capable PWA — a web manifest and service worker make DeckBook installable on a phone or museum kiosk and fully usable with no network. Open Graph / Twitter cards give shared links a polished preview.
- A ready-to-run teaching guide (objectives, a timed 45–60 min lesson mapped to the app, discussion questions, assessment, standards tie-ins) and a printable student worksheet.
- An in-app For Educators panel and a Glossary of every key term.
- Numbered step flow — the core send/receive path is labelled 1–5 (Generate → Pick a key → Prepare the deck → Encrypt → Decrypt) with a compact overview in the hero, so the sequence is obvious at a glance.
- Collapsible reference sections — the expository panels (How the Cipher Works, Security Model, What is DeckBook?, and the rest) start collapsed as keyboard-operable disclosures, keeping the page short; the guided walkthrough and presenter mode auto-expand whatever they navigate to.
- Secure deck generation using
crypto.getRandomValues()(noMath.random()) - Fisher-Yates shuffle with rejection sampling for unbiased integer selection
- 52-card deck model with consistent 0-51 mapping
- DeckBook modes: 10 / 100 / 1,000 keys
- Human-readable index codes and SHA-256-derived fingerprints
- Receiver setup view with top-to-bottom checklist
- A-Z modular encryption/decryption
- Multi-deck message mode for long plaintexts
- Two-Party Simulator (Alice / public channel / Bob)
- Used/unused key tracking and explicit reuse warnings
- Import/export DeckBook JSON (for educational simulation)
- Local persistence in browser storage
- Mistake simulator panel
- Mobile-responsive layout and accessibility improvements
- Vite
- TypeScript
- Vanilla CSS
- Fully client-side (no backend)
- Capacitor (Android/iOS) and Tauri (desktop) native packaging — see Native Apps
npm installnpm run devnpm run buildnpm run preview-
Unit tests (Vitest) cover the pure cipher, attack-analysis, share-link, and card-face modules:
npm test -
End-to-end tests (Playwright) drive the real UI through the full protocol — generate a DeckBook, encrypt, follow the share link, and decrypt back to the original plaintext — plus the "a device without the DeckBook cannot read the message" case and the animated visualizer:
npm run test:e2e
Both suites run automatically on every push and pull request via ci.yml.
This repo includes a Pages workflow at deploy-pages.yml.
- Open repository Settings.
- Go to Pages.
- Set Source to GitHub Actions.
- Push to
main. - Workflow builds the app and deploys
dist/to GitHub Pages.
The same client-side build is packaged as installable native apps — nothing in
src/ changes. Mobile uses Capacitor (a native
WebView shell around dist/), and desktop uses Tauri
(the OS's own WebView plus a small Rust host, so a build is a few MB rather than
Electron's ~100 MB).
| Target | Tooling | Local command | CI |
|---|---|---|---|
| Android | Capacitor | npm run cap:android (opens Android Studio) |
mobile.yml → debug .apk |
| iOS | Capacitor | npm run cap:ios (opens Xcode, needs macOS) |
mobile.yml → unsigned build |
| Windows / macOS / Linux | Tauri | npm run tauri:dev / npm run tauri:build |
desktop-tauri.yml → installers |
- App identity: bundle ID
com.systemslibrarian.deckbook, name DeckBook. - Native projects (
android/,ios/,src-tauri/) are committed. After changing web code, runnpm run cap:syncto copy the freshdist/into the mobile projects (the TauribeforeBuildCommandrebuildsdist/automatically). - Icons for all native targets are generated from the DeckBook mark with
npm run icons:native(Tauri icons vianpx tauri icon).
- Android: JDK 21 + Android SDK (Android Studio). The Windows dev machine
can scaffold and edit the project; CI's Linux runner produces the
.apk. - iOS: a Mac with Xcode — required to compile. CI's macOS runner builds
an unsigned app; a signed
.ipafor the App Store needs an Apple Developer certificate + provisioning profile (add as CI secrets). - Desktop: the Rust toolchain (
rustup). Tauri uses WebView2 on Windows, WKWebView on macOS, and WebKitGTK on Linux.
Both workflows run on every push/PR and upload build artifacts. The desktop workflow additionally attaches installers to a draft GitHub Release when you push a version tag:
git tag v1.0.0 && git push origin v1.0.0Debug builds (npm run android:device, CI's .apk) are signed with the
throwaway debug key and are fine for testing. A release build for the Play
Store must be signed with your own keystore. The Gradle config reads the
keystore details from a gitignored android/key.properties — the keystore
and its passwords are never committed.
-
Generate a keystore once (or reuse an existing one):
keytool -genkey -v -keystore deckbook-release.jks \ -alias deckbook -keyalg RSA -keysize 2048 -validity 10000
-
Copy android/key.properties.example to
android/key.propertiesand fill in thestoreFilepath, passwords, and alias. -
Build the signed release bundle:
cd android && ./gradlew bundleRelease # -> app/build/outputs/bundle/release/*.aab
For CI, the same config falls back to environment variables
(DECKBOOK_KEYSTORE, DECKBOOK_STORE_PASSWORD, DECKBOOK_KEY_ALIAS,
DECKBOOK_KEY_PASSWORD) so a release job can decode a base64 keystore secret
and sign without a local key.properties.
The android-release job in mobile.yml does
exactly this on a version tag. Add these repository secrets (Settings →
Secrets and variables → Actions) to enable it:
| Secret | Value |
|---|---|
DECKBOOK_KEYSTORE_BASE64 |
base64 -w0 deckbook-release.jks output |
DECKBOOK_STORE_PASSWORD |
keystore password |
DECKBOOK_KEY_ALIAS |
key alias (e.g. deckbook) |
DECKBOOK_KEY_PASSWORD |
key password |
Pushing a v* tag then builds a signed .aab and attaches it to the tag's
draft release. Without the secrets the job fails fast with a clear message; the
debug .apk job is unaffected.
With a phone attached over USB (debugging authorized), deploy the latest code:
npm run android:device # build -> cap sync -> assembleDebug -> install -> launchThe UI is designed to be usable on small screens and with keyboard navigation.
- Semantic sections and clear labels
- Text-based USED/UNUSED state indicators (not color-only)
- High contrast dark theme with amber accents
- Responsive card/grid layouts
- Touch-friendly controls and compact mobile behavior
- Reduced-motion support via
prefers-reduced-motion
The model demonstrates security only when:
- Deck orders are generated with cryptographic randomness.
- Both parties share the same private DeckBook beforehand.
- Each deck key is used once.
- Used keys are never reused.
- Deck order is never transmitted publicly.
- Index code does not reveal deck order.
- Human error and message-length constraints are handled carefully.
- index.html app shell
- src/cipher.ts pure cipher core (encrypt/decrypt, deck, shuffle)
- src/analysis.ts pure attack/analysis math (crib dragging, frequencies)
- src/share.ts pure share-link encode/decode (QR payload)
- src/qr.ts QR image hydration helper
- src/visualizer.ts animated "Watch It Work" panel
- src/main.ts app logic and UI rendering
- src/styles.css visual design and responsive styles
- src/challenge.ts "Eve's Intercept" CTF puzzle module
- docs/ teaching guide and student worksheet
- scripts/generate-icons.mjs PWA/social icon generator
- scripts/generate-native-icons.mjs native Android/iOS icon generator
- capacitor.config.ts Capacitor config (mobile shell over
dist/) - android/ + ios/ Capacitor native mobile projects
- src-tauri/ Tauri desktop app (Rust host +
tauri.conf.json) - tests/ Vitest unit tests for cipher, analysis, share, card-face, and challenge modules
- e2e/ Playwright end-to-end smoke tests
- vite.config.ts Vite + Vitest config for static deployment
- playwright.config.ts Playwright config (starts the dev server)
- ci.yml build + unit + e2e (incl. a11y) workflow
- lighthouse.yml + lighthouserc.json Lighthouse CI
- deploy-pages.yml GitHub Pages deployment workflow
- mobile.yml Capacitor Android/iOS build workflow
- desktop-tauri.yml Tauri desktop build/release workflow
MIT


