Skip to content

fix: nsec import + backup failures and storage.sync data loss#1

Open
vveerrgg wants to merge 3 commits into
mainfrom
fix/nsec-import-and-backup
Open

fix: nsec import + backup failures and storage.sync data loss#1
vveerrgg wants to merge 3 commits into
mainfrom
fix/nsec-import-and-backup

Conversation

@vveerrgg

Copy link
Copy Markdown
Collaborator

Summary

Fixes a cluster of bugs around key import, backup export, and storage.sync merging — several of which could silently lose key material or lock a device out permanently.

Bugs fixed

storage.sync data loss

  • Index clobber — profiles were merged by array index, so a reordered or partial sync could overwrite one identity's key material with another's. Now matched by pubKey.
  • Bunker wipe — a bunker/remote-signer profile (privKey:'' but a real identity) was mistaken for a blank fresh-install slot and wiped. Fresh-install detection now treats it as a real identity.
  • Permanent lockoutisEncrypted was synced while the password verifier intentionally is not, so a second device adopting isEncrypted=true could never unlock. Encryption state is now strictly device-local, plus a startup self-heal clears a bogus flag when there's no verifier and no encrypted blobs.

Key + backup flows

  • savePrivateKey silent failure — the MV3 handler returned a Promise, which Chrome does not deliver to sendMessage callers, so imported keys could fail while the UI showed success. Now replies via sendResponse + return true, with rollback of the half-created profile on failure.
  • Backup without a master password — export used the in-memory session key, so users with no master password (or a locked extension) couldn't back up. Now encrypts with a dedicated backup password supplied at export time.

Tests

  • Extracted a pure computeMergeUpdates() so the merge rules are unit-testable.
  • Added test/sync-merge.test.js (9) and test/backup-password.test.js (4) regression suites.
  • Full suite: 167 passed, 2 skipped.

Dev environment

  • Bumped local toolchain to Node 22 LTS; added engines.node >=20.19 and an .nvmrc (vitest 4 / vite 8 need require(esm), default only on Node 20.19+).

Notes

  • distros/safari/ rebuilt and included (Xcode Cloud needs it tracked).
  • Xcode Cloud auto-build only fires on main, so it'll build on merge.

🤖 Generated with Claude Code

vveerrgg added 3 commits June 12, 2026 23:25
Sync merge:
- Match profiles by pubKey, not array index, so a reordered or partial
  sync can no longer overwrite one identity's key material with another's.
- Treat bunker/remote-signer profiles (privKey:'' but a real identity) as
  identities in fresh-install detection, so they aren't wiped as blanks.
- Stop syncing isEncrypted: the verifier is never synced, so adopting a
  remote isEncrypted=true permanently locked the second device out.
- Add startup self-heal that clears a bogus isEncrypted flag when no
  password verifier and no encrypted blobs exist.
- Extract pure computeMergeUpdates() so the merge rules are unit-testable.

Key/backup flows:
- savePrivateKey now replies via sendResponse + return true (MV3 does not
  deliver Promise returns), so imported keys can't silently fail.
- Roll back the half-created profile when a key fails to persist.
- Backup export encrypts with a supplied backup password instead of the
  session key, so users with no master password can export, even locked.

Tests: add sync-merge and backup-password regression suites (+13).
vitest 4 / vite 8 rely on require(esm), which is only default on Node
20.19+. Add an engines floor and an .nvmrc pinning the dev/CI runtime to
Node 22 LTS so the test suite runs without experimental flags.
Findings from a focused security/correctness audit of the touched subsystems:

- Bunker exposure (critical): gate bunkerServer.start/stop behind
  isExtensionSender. They were callable from any web page via
  window.nostr.nip46.startBunker(), which returned a secret-bearing
  bunker:// string granting unlimited remote signing of the user's key.
- Bunker connect: fail closed when the server has no secret, instead of
  authenticating any client.
- savePrivateKey (critical): when encryption is enabled but the session is
  locked (no session key), throw instead of silently persisting the key as
  plaintext into a vault the user believes is encrypted.
- Stale session-key cache (critical): getPlaintextPrivKey now verifies the
  index-cached key derives to the profile's pubkey before use. Deleting a
  profile shifts indices without updating the index-keyed cache, so a sign
  could otherwise use a different identity's key.
- setPermission: use index == null (not !index) so per-site grants for
  profile 0 target profile 0, not whatever profile is active.
- content.js permission sheet: always reply (catch errors) so a failed sheet
  can't hang the background ask() until its 10s timeout silently denies.
- Sync version compare: numeric semver compare so 1.10.0 isn't treated as
  older than 1.9.0 by string comparison.

Tests: add compareSemver coverage (+3).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant