Add apps/desktop, a thin Tauri v2 shell wrapping the SAME @parking/web SPA so the desktop and browser UIs never drift: dev loads the Vite dev server (HMR), prod bundles the web app's dist/. No business logic in the shell (device/auth/ ledger stay in @parking/server); deny-by-default capabilities. apps/web (single UI source of truth): - lib/origin.ts: centralize the backend origin (API_BASE/apiUrl/wsUrl from VITE_API_BASE); no-op in the browser, lets the desktop build target Fastify. - lib/kiosk.ts: block the right-click context menu in PROD only (dev keeps it + devtools). - lib/desktop-updater.ts: prompt-on-update auto-update (no-op in browser/offline) → downloadAndInstall + relaunch; i18n update.* keys (sq+en). - .env.production: VITE_API_BASE wired to the Fastify origin for the bundle. Desktop: - window starts maximized (not fullscreen — operator keeps OS access). - auto-update via tauri-plugin-updater + -process; self-hosted endpoint is a PLACEHOLDER to fill in. Updater keypair: pubkey embedded in tauri.conf.json; private key + password kept OUTSIDE the repo (~/.parking-updater-keys) and as TAURI_SIGNING_* build secrets. - Turbo build is a no-op; the real signed bundle is `pnpm --filter @parking/desktop bundle` (verified → .deb/.rpm/.AppImage + .sig signatures). Verified: cargo check clean; turbo run build lint 14/14 green; i18n parity holds; no key/sig/bundle artifacts in the repo. Wiki (security + desktop analysis recorded alongside): - new concepts/tpm.md (TPM 2.0: how it works, sealed-LUKS auto-unlock + non- extractable signing key, limits — live-root, bus-sniff — TPM-vs-ATECC608 by platform). - new decisions/desktop-shell-tauri.md (Tauri v2 over Electron; best-case Ubuntu 26.04 LTS, worst-case Windows+WSL → kiosk browser; full as-built). - pull-the-disk attack trace on append-only-event-chain; ATECC608 not-in-a-PC caveat; cross-links from disk-os-hardening / threat-model. - open-questions #11 (appliance WebKitGTK), #12 (TPM hardening impl), #13 (startup verifyChain self-check); index/overview/log/standing-decisions. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
3.1 KiB
type, tags, sources, updated
| type | tags | sources | updated | ||||
|---|---|---|---|---|---|---|---|
| concept |
|
|
2026-06-20 |
Threat Model
The second foundational force (with offline-first). The central insight is a reframing of who the adversary is. (See parking-system-architecture §3.)
The key reframing
Early thinking focused on protecting the database at rest — SQLCipher, LUKS, BitLocker, tpm. All of that defends against an outsider who steals the machine or boots from external media.
That is the wrong primary threat. The most likely adversary is the legitimate operator at the booth. While the app runs, the database is decrypted in memory and the operator has full authorised access through the app. Encryption does nothing against the classic parking fraud: take the cash, then void/delete the entry/exit record so the books balance.
Consequences
The controls that actually address insider/operator fraud are different in kind:
- append-only-event-chain — events appended, never edited/deleted; a "void" is itself a recorded event, hash-chained, and atecc608-signed (unforgeable).
- reconciliation against an authority the operator can't alter — this is what remote sync really is: a fraud-control mechanism, not just a backup.
- disk-os-hardening still worthwhile (defeats boot-from-USB) but not the main event; with LUKS in place, SQLCipher is optional defence-in-depth.
The same reframing recurs at the device layer: the uhppote-controller's real problem is unauthenticated commands (uhppote-udp-protocol), addressed by detection (event-log-ingestion) or prevention (esp32-custom-controller).
Worked example — "store the price" ≠ "account for the sale" (found + fixed 2026-06-20). Every money-taking action must append a signed
paymentevent, or it is invisible to reconciliation. A concrete miss: selling a subscription wrote only the mutablesubscriptionsmaster row (the agreed price) and appended nothing to the ledger, so the cash the operator collected showed up in the feed/drawer/Z-report nowhere — a clean off-book channel (three real sales, 27,000 ALL, untraceable). The fix is the textbook control: append a signedpayment(subscriptionSale: true) at sale time so it folds into the shift like any taking. The lesson generalises: whenever a feature records an amount in a mutable table, ask "where is the signed event that says money changed hands?" — a price in master data is not an accountable transaction. See subscription "Collecting the fee".
Direction shift: the system is heading toward fully unmanned operation — no operator, no booth (autonomous-direction). That removes the booth-operator as the primary adversary, but swaps in unattended-machine threats (tailgating, plate spoofing, physical tampering, forced entry). The append-only signed log + reconciliation controls carry over; the emphasis moves from "catch the cashier" to "trust the automated record and detect tampering."