Skip to content

systemslibrarian/DeckBook

Repository files navigation

DeckBook

CI Lighthouse

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.

▶ Try the live exhibit

Watch the cipher run Crack a reused key Play the intercept challenge
Watch It Work: a card flips off the deck and shifts one letter Key Reuse Attack Lab: crib dragging recovers the plaintext Challenge mode: recover two messages from ciphertext

Quality

  • 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-motion support 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).

Inspiration

This project is inspired by Solitaire-style manual encryption teaching material, including:

Disclaimer

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.

What It Teaches

  • 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)

How the Cipher Works

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.

  1. Letters become numbers. A = 0, B = 1, …, Z = 25. Spaces, digits, and punctuation are stripped before encryption.

  2. 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 26
    

    One 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.

  3. 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.

How Keys Are Generated (and Why They're Random)

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):

  1. A real cryptographic source. Every random number comes from the Web Crypto CSPRNG, crypto.getRandomValues(), seeded by the operating system's entropy. The predictable Math.random() is never used.
  2. No modulo bias. secureRandomInt draws integers with rejection sampling: a naive randomUint32 % 52 is skewed because 2³² is not a multiple of 52, so it discards the "leftover" high values and only accepts from an exact multiple. Every value 0…max-1 is then exactly equally likely.
  3. A provably-fair shuffle. secureShuffle is a Fisher–Yates shuffle driven by secureRandomInt. Given an unbiased RNG, all 52! orderings are equally probable — it avoids the common "swap with any position" bug that skews the distribution.
  4. Independent keys. generateDeckBook shuffles a fresh createStandardDeck() 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?".

Feature Highlights

Interactive learning

  • 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.

Make it an experience

  • 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.

For educators

  • 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.

Guided, uncluttered layout

  • 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.

Core cipher and key management

  • Secure deck generation using crypto.getRandomValues() (no Math.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

Tech Stack

  • Vite
  • TypeScript
  • Vanilla CSS
  • Fully client-side (no backend)
  • Capacitor (Android/iOS) and Tauri (desktop) native packaging — see Native Apps

Local Development

1. Install dependencies

npm install

2. Run development server

npm run dev

3. Build for production

npm run build

4. Preview production build locally

npm run preview

Testing

  • 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.

GitHub Pages Deployment

This repo includes a Pages workflow at deploy-pages.yml.

One-time repository settings

  1. Open repository Settings.
  2. Go to Pages.
  3. Set Source to GitHub Actions.

Publish flow

  • Push to main.
  • Workflow builds the app and deploys dist/ to GitHub Pages.

Native Apps

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, run npm run cap:sync to copy the fresh dist/ into the mobile projects (the Tauri beforeBuildCommand rebuilds dist/ automatically).
  • Icons for all native targets are generated from the DeckBook mark with npm run icons:native (Tauri icons via npx tauri icon).

Build prerequisites

  • 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 .ipa for 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.

CI artifacts and releases

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.0

Release signing (Android)

Debug 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.

  1. Generate a keystore once (or reuse an existing one):

    keytool -genkey -v -keystore deckbook-release.jks \
      -alias deckbook -keyalg RSA -keysize 2048 -validity 10000
  2. Copy android/key.properties.example to android/key.properties and fill in the storeFile path, passwords, and alias.

  3. 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.

One-command device redeploy

With a phone attached over USB (debugging authorized), deploy the latest code:

npm run android:device   # build -> cap sync -> assembleDebug -> install -> launch

Accessibility and Mobile Notes

The 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

Security Model (Educational)

The model demonstrates security only when:

  1. Deck orders are generated with cryptographic randomness.
  2. Both parties share the same private DeckBook beforehand.
  3. Each deck key is used once.
  4. Used keys are never reused.
  5. Deck order is never transmitted publicly.
  6. Index code does not reveal deck order.
  7. Human error and message-length constraints are handled carefully.

Project Structure

License

MIT

About

Deal a shuffled deck of cards into a keystream to hands-on teach one-time keys, key reuse attacks, and stream ciphers. Encrypt messages, share via QR, then crack a reused deck with crib dragging. A client-side Cipher Museum PWA with CTF challenge mode and educator materials.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages