Make the secret key (nsec) discoverable and backupable without obfuscating it, while keeping the npub/nsec asymmetry every Nostr client relies on: npub is shareable (plain, copy, QR); nsec is an unrecoverable password (masked, gated reveal + gated copy, NIP-49 encrypted option, never a QR). Desktop: - New BackupKeysCard in the settings/profile screen: npub (copy + QR), nsec masked-by-default with reveal/copy gated behind the existing PrivacyLock (new LockScope.KeyBackup), plaintext + NIP-49 encrypted copy, strong "cannot be recovered" warning, external-signer note when no key. - Upgrade NewKeyWarningCard: per-key copy, encrypted copy, stronger warning, soft "I have saved my keys" acknowledgement. - Extract shared copyToClipboard + best-effort copyToClipboardThenClear util. Android: - Soft, dismissible post-signup "Back up your keys" nudge on the home feed (per-account hasBackedUpKeys flag; only freshly-generated accounts nudged; "Back up now" opens the existing AccountBackupScreen). - FLAG_SECURE on the key-backup screen (screenshot / app-switcher redaction). - Best-effort clipboard auto-clear 60s after copying the plaintext nsec. Deferred: compose paste-guard (nsec self-doxx warning) — the send path is reimplemented across ~17 *PostViewModels with no shared choke point. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6.9 KiB
Key Backup & nsec Exposure — Plan
Date: 2026-08-11
Scope: Desktop (desktopApp) primary, Android (amethyst) secondary, shared strings/logic in commons/quartz.
Problem
User report (Nostr): new account creation gives no discoverable way to find/save the keypair. "Couldn't find it in settings, no option to save when generated." Keys are crucial → must reinforce, not obfuscate.
Ask: make nsec discoverable + backupable; npub sharable (copy/QR); check both Android and Desktop.
Key asymmetry (design principle — non-negotiable)
Every surveyed client agrees:
- npub = public identity → plain by default, copy + QR, share freely.
- nsec = password that can NEVER be reset → masked by default, gated reveal + gated copy, never rendered as a QR, prefer encrypted (NIP-49).
So "show nsec with npub for sharing" splits: npub is for sharing; nsec is for private backup only. The plan treats them differently.
Current state
Android (amethyst) — mostly done, one gap
ui/.../keyBackup/AccountBackupScreen.kt— full backup screen: biometric-gated copy nsec, NIP-49 encrypted (ncryptsec1) copy, plaintext + encrypted QR, strong warnings (account_backup_tips2_md/tips3_md). RouteAccountBackup.- Gap: signup (
SignUpViewModel.signup→AccountSessionManager.createNewAccount,val keyPair = KeyPair()) generates silently. No post-signup nudge to back up. Screen exists but discoverability relies on the user hunting the drawer.
Desktop (desktopApp) — large gaps
ui/auth/NewKeyWarningCard.kt— shows npub+nsec once at creation as plainSelectableKeyText(manual-select). No copy button, no QR, no encrypted option, weak warning.ui/DevSettingsSection.kt— full copy UI but debug-mode only; normal users can't re-reach nsec.ui/profile/ProfileInfoCard.kt— npub + hex only (no copy? verify), no QR.- No user-facing Backup Keys screen. No settings path to nsec. No NIP-49 export. No QR for npub.
AccountState.LoggedInalready exposes.npub,.nsec,.pubKeyHex→ wiring a backup screen is trivial.commons/jvmMain/.../keystorage/SecureKeyStorage.kt— OS keychain + encrypted fallback; can retrieve raw key.
Shared / quartz (reuse — don't rebuild)
nip19Bech32/ByteArrayExt.kt:toNsec(),toNpub().nip49PrivKeyEnc/Nip49.kt:encrypt()/decrypt()ncryptsec1.- Android QR:
ui/.../qrcode(QrCodeDrawer). Desktop QR:QrCodeCanvas.kt(currently only NIP-46/NIP-47 URIs) — reuse for npub.
Recommended design
Adopt the hidden-password camp (Amethyst Android's existing model) + persistent post-signup backup nudge (Snort pattern). Rationale: matches Android, keeps signup fast, avoids a hard gate, but nags until backed up.
Firm rules across both platforms:
- nsec masked by default; reveal is an explicit gated action.
- Copy nsec is itself gated (copy is the real leak vector) + shows warning.
- Offer NIP-49 encrypted (ncryptsec1) copy/export alongside plaintext.
- npub always plain, copyable, QR. nsec never QR.
- Hide the entire private-key section for external-signer / bunker / read-only
sessions (
nsec == null) — show "This account uses an external signer" note. - Explicit "cannot be recovered" warning verbatim-strong (reuse Android's
account_backup_tips2_md).
Platform reveal-gate:
- Android: biometric (already wired in
AccountBackupScreen). - Desktop: no biometric → gate reveal/copy behind a confirm dialog
("Show secret key?" YES/CANCEL) +
showKeystoggle, matching DevSettings but user-facing and with the encrypted option. (Optional later: OS auth via existing PrivacyLock/master-password machinery if present — verify.)
Implementation
Phase 1 — Desktop Backup Keys screen (biggest gap)
- New
desktopApp/.../ui/settings/BackupKeysScreen.kt(or.../ui/keyBackup/):- npub row: plain, Copy, "Show QR" (reuse
QrCodeCanvas). - nsec section: masked → confirm-dialog reveal → Copy (plaintext) + "Copy
encrypted" (password field →
Nip49().encrypt). Strong warning banner. - Hidden when
account.nsec == null(external/read-only) → info note.
- npub row: plain, Copy, "Show QR" (reuse
- Add entry point in Settings sidebar/account area (near
ProfileInfoCardinMain.kt~2191). Label "Backup Keys" / "Account Keys". - Reuse
AccountState.LoggedIn.{npub,nsec,pubKeyHex}; clipboard via existingcopyToClipboard(AWT) — factor out ofDevSettingsSection.
Phase 2 — Desktop NewKeyWarningCard upgrade
- Add Copy button per key (not just selection).
- Add "Copy encrypted (recommended)" + password field.
- Strengthen warning copy to match Android "no recovery" strength.
- Add "I've saved my keys" acknowledgement affordance before
onContinue(soft; not a hard checkbox gate — see open Q). - Do NOT add nsec QR.
Phase 3 — Android post-signup backup nudge
- After
signup(), route to / surface a dismissible "Back up your keys" prompt with "Back up now" (→AccountBackupScreen) / "I already saved them". - Persist "backed up" flag per account; keep nudging (home banner or settings badge) until acknowledged. No silent dismissal.
Phase 4 — Shared polish (both platforms)
- Copy-warning string on every nsec copy ("like a password, cannot be reset").
FLAG_SECURE(Android) on reveal to redact screenshots/recents. Desktop: no direct equivalent — skip.- (Stretch) timed clipboard auto-clear after nsec copy — genuinely novel, no
client does it. Verify feasibility (Android
ClipboardManager, Desktop AWT). - (Stretch) Coracle-style paste-guard on compose: warn if a note body starts
with
nsec1.
Testing
- Desktop: generate account → reveal/copy plaintext + encrypted → decrypt
round-trips (
Nip49) → QR shows npub not nsec → external-signer account hides nsec section. Manual sheet. - Android: signup → nudge appears → backup screen reachable → encrypted copy
round-trips. Existing
AccountBackupScreenunit coverage if any. - Reuse quartz
Nip49tests; add commons test for any extracted helper.
Non-goals / deferred
- iOS (no mature target yet).
- BIP-39 / NIP-06 mnemonic backup (Snort) — separate feature.
- Full OS-biometric gate on Desktop (no primitive) — confirm dialog instead.
Unanswered questions
- Design camp: confirm hidden-password + nudge (recommended) vs. keep Desktop's show-at-creation as the primary backup moment?
- Hard "I saved it" checkbox gate at signup, or soft dismissible nudge? (survey: soft wins; hard gate largely unclaimed.)
- Desktop reveal gate: confirm-dialog only, or wire existing PrivacyLock/master password if one exists? (verify what Desktop already has.)
- Extract a shared
commonsbackup composable, or keep Android + Desktop screens separate (Android biometric vs Desktop dialog diverge)? - Timed clipboard auto-clear: in scope now or stretch?
- Paste-guard on compose (nsec self-doxx prevention): this feature or separate?
- Does Desktop
ProfileInfoCardalready copy npub / need a QR button there too?