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 <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-09-24 18:22:30 +07:00
parent 00482fc332
commit 8e02c77ba1
2 changed files with 49 additions and 3 deletions

View file

@ -247,13 +247,51 @@ oversights:
no real protection (the decryption key would live in the same JS context). The 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 meaningful mitigation is preventing script injection (CSP shipped in the bundle
so it also covers mobile + no-`innerHTML` discipline) and offering 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, - **Broad `connect-src`/`img-src`.** Necessary for user-chosen relays, mints,
and media hosts. This permits data exfiltration *if* script execution is ever and media hosts. This permits data exfiltration *if* script execution is ever
achieved — which is why preventing XSS is the top priority. achieved — which is why preventing XSS is the top priority.
- **`style-src 'unsafe-inline'`.** Required by the CSS framework; CSS-injection - **`style-src 'unsafe-inline'`.** Required by the CSS framework; CSS-injection
defense is handled by input sanitization rather than CSP. 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 ## For auditors: where to look first
@ -266,7 +304,7 @@ oversights:
| Rich text rendering | `src/components/NoteContent.tsx` | | Rich text rendering | `src/components/NoteContent.tsx` |
| Login / key handling | `src/components/auth/`, Nostrify login (`nostr:login`) | | Login / key handling | `src/components/auth/`, Nostrify login (`nostr:login`) |
| Lightning / NWC | `src/hooks/useNWC.ts`, `src/components/WalletModal.tsx` | | 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` | | 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` (`<meta>` — covers mobile), `public/_headers`, `netlify.toml`, `public/boot-guard.js` | | CSP & security headers | `index.html` (`<meta>` — covers mobile), `public/_headers`, `netlify.toml`, `public/boot-guard.js` |
| Native wrapper (same bundle) | `capacitor.config.ts` (`webDir: 'dist'`) | | Native wrapper (same bundle) | `capacitor.config.ts` (`webDir: 'dist'`) |

View file

@ -484,6 +484,14 @@ export function Layout({ children, noPadding }: LayoutProps) {
return ( return (
<div className="min-h-screen bg-black text-white"> <div className="min-h-screen bg-black text-white">
{/* Skip-to-content — visually hidden until focused, so keyboard and
screen-reader users can jump past the nav on every page. */}
<a
href="#main"
className="sr-only focus:not-sr-only focus:fixed focus:top-3 focus:left-3 focus:z-[100] focus:rounded-lg focus:bg-[#ccff00] focus:px-4 focus:py-2 focus:text-sm focus:font-black focus:uppercase focus:tracking-wide focus:text-black focus:shadow-lg"
>
Skip to content
</a>
{/* Desktop Header */} {/* Desktop Header */}
<header className="hidden md:flex sticky top-0 z-50 border-b border-red-500/30 bg-black/95 backdrop-blur"> <header className="hidden md:flex sticky top-0 z-50 border-b border-red-500/30 bg-black/95 backdrop-blur">
<div className="container flex items-center justify-between h-16 px-4"> <div className="container flex items-center justify-between h-16 px-4">
@ -967,7 +975,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
{/* Main Content — extra bottom padding on mobile so the fixed bottom nav never overlaps */} {/* Main Content — extra bottom padding on mobile so the fixed bottom nav never overlaps */}
{/* noPadding: full-bleed pages (map) manage their own height */} {/* noPadding: full-bleed pages (map) manage their own height */}
<main className={noPadding ? undefined : "pb-[calc(5rem+env(safe-area-inset-bottom,0px))] md:pb-6"}> <main id="main" className={noPadding ? undefined : "pb-[calc(5rem+env(safe-area-inset-bottom,0px))] md:pb-6"}>
{children} {children}
</main> </main>