From 8e02c77ba1cc943a7f764d1f7721e8f19fb44b20 Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Thu, 24 Sep 2026 18:22:30 +0700 Subject: [PATCH] Docs + a11y: document Cashu wallet seed storage/backup as tracked accepted-risk in SECURITY.md (F1 follow-up); add skip-to-content link and main landmark id (UX U4) Co-authored-by: shakespeare.diy --- SECURITY.md | 42 +++++++++++++++++++++++++++++++++++++-- src/components/Layout.tsx | 10 +++++++++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 1a72950..90376fb 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -247,13 +247,51 @@ oversights: no real protection (the decryption key would live in the same JS context). The meaningful mitigation is preventing script injection (CSP shipped in the bundle so it also covers mobile + no-`innerHTML` discipline) and offering - extension/bunker login for users who want keys off the page. + extension/bunker login for users who want keys off the page. (The Cashu wallet + *seed* is a partial exception — see "Known limitations — Cashu wallet" below.) - **Broad `connect-src`/`img-src`.** Necessary for user-chosen relays, mints, and media hosts. This permits data exfiltration *if* script execution is ever achieved — which is why preventing XSS is the top priority. - **`style-src 'unsafe-inline'`.** Required by the CSS framework; CSS-injection defense is handled by input sanitization rather than CSP. +### Known limitations — Cashu wallet + +The built-in Cashu ecash wallet is **native-only** (browsers block most mints via +CORS). Its BIP-39 mnemonic is the master key to all ecash funds, so it gets +stronger handling than other secrets: + +- **Seed storage location (per platform).** + - **Native (Android / iOS):** the mnemonic is stored in the OS-backed secure + store — Android **KeyStore** / iOS **Keychain** — via + `capacitor-secure-storage-plugin` (`src/lib/secureStorage.ts`), **not** in + plaintext `localStorage`. Existing users on an older build are migrated + transparently on first read (the legacy plaintext `cashu-mnemonic` copy is + moved into secure storage and the plaintext removed). + - **Web:** falls back to `localStorage` (same accepted-risk posture as `nsec` + above). The wallet is effectively native-only in practice, so web plaintext + exposure is a minor surface. + - Ecash **proofs** (the spendable tokens) still live in IndexedDB + (`cashu-wallet`) in plaintext on device — a stolen unlocked device can spend + them, exactly like cash. Loss of the seed without a backup = permanent loss. + +- **Backup behavior (Android / native).** A freshly auto-generated seed sets a + `needsBackup` flag that forces a **mandatory, non-dismissable backup step** + (`RecoveryPhraseDialog` in `requireConfirm` mode): the user must reveal the 12 + words and explicitly confirm ("I've written it down") before the wallet is + usable. This prevents a silently generated seed from being left un-backed-up — + clearing app storage without a backup means the funds are unrecoverable. The + phrase can be re-viewed any time from the wallet ("View Recovery Phrase"). + +- **Planned / status.** Secure-storage migration is **shipped** (native seed now + in Keychain/KeyStore). Remaining accepted risk: proofs-at-rest in IndexedDB and + the web-only `localStorage` fallback. Encrypting proofs on-device is tracked as + a future hardening item but offers limited value while the key must live in the + same JS context (same reasoning as `nsec` above). + + Relevant files: `src/hooks/wallet/useCashu.ts`, `src/lib/secureStorage.ts`, + `src/components/wallet/RecoveryPhraseDialog.tsx`. + --- ## For auditors: where to look first @@ -266,7 +304,7 @@ oversights: | Rich text rendering | `src/components/NoteContent.tsx` | | Login / key handling | `src/components/auth/`, Nostrify login (`nostr:login`) | | Lightning / NWC | `src/hooks/useNWC.ts`, `src/components/WalletModal.tsx` | -| Cashu ecash (native only): proofs in IndexedDB `cashu-wallet`, seed in `cashu-mnemonic` | `src/hooks/wallet/useCashu.ts`, `src/lib/platform.ts` | +| Cashu ecash (native only): proofs in IndexedDB `cashu-wallet`; seed in OS Keychain/KeyStore (secure storage), localStorage fallback on web | `src/hooks/wallet/useCashu.ts`, `src/lib/secureStorage.ts`, `src/lib/platform.ts` | | Admin authority / moderation / takedowns | `src/lib/admins.ts`, `src/hooks/useAdminModeration.ts`, `src/hooks/useDeleteEntity.ts`, `src/components/DeleteEntityDialog.tsx`, `src/components/GearSwapItemCard.tsx` | | CSP & security headers | `index.html` (`` — covers mobile), `public/_headers`, `netlify.toml`, `public/boot-guard.js` | | Native wrapper (same bundle) | `capacitor.config.ts` (`webDir: 'dist'`) | diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index a5f24c2..203105b 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -484,6 +484,14 @@ export function Layout({ children, noPadding }: LayoutProps) { return (
+ {/* Skip-to-content — visually hidden until focused, so keyboard and + screen-reader users can jump past the nav on every page. */} + + Skip to content + {/* Desktop Header */}
@@ -967,7 +975,7 @@ export function Layout({ children, noPadding }: LayoutProps) { {/* Main Content — extra bottom padding on mobile so the fixed bottom nav never overlaps */} {/* noPadding: full-bleed pages (map) manage their own height */} -
+
{children}