Files
thePR0M3TH3ANandClaude Fable 5 f0b5f40fd4 Audit response: fix BL-16 through BL-20 and the low findings, same day
BL-16 (high): a remote signer's auth_url is attacker-controlled input that
became a clickable href. sanitizeAuthUrl in core now admits only https
(or loopback http) -- javascript:/data:/blob:/custom schemes are dropped
without consuming the surfacing slot -- and the widget link shows the
hostname. Hostile-scheme tests at both layers.

BL-17: the post-creation PRF fallback assertion is pinned via
allowCredentials to the credential that was just created, so a multi-
passkey account sheet can no longer silently bind an older passkey's
account.

BL-18: queryOnce's timeout path sent CLOSE before settling, unguarded --
a relay dying mid-query stranded the promise and everything queued behind
the worker's serial queue. The send is guarded; settling is unconditional.
MockRelay.close() now terminates live sockets (it previously waited on
them forever), and a suppressEose switch models the silent relay.

BL-19: relays are no longer trusted to honor limits. Buffered queries
enforce filter.limit locally, drop replayed duplicate ids, and cap at
1000 events; a 20-copy replay now yields one event.

BL-20: the .semgrep.yml try/catch fallback pattern never parsed, so the
entropy rules silently did not run. Rewritten in parseable form and
verified under the pinned Semgrep 1.171.0: fires on hostile code, exits
0 on the clean tree.

Lows: NIP-07 and NIP-46 verify returned signed events (signature, pinned
session identity, field equality with the request); Nip46Client.close()
wipes the client and conversation keys (§11.10); passkey labels carry a
short suffix so multiple accounts are distinguishable; postcss bumped to
8.5.25 (GHSA-fxqj-rqcc-2cmp); GitHub Actions pinned to commit SHAs.
Deferred as interop (recorded in TODO): NIP-46 protocol logout and
switch_relays. 12 new adversarial tests; 256 total.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-04 13:10:20 -04:00

149 lines
8.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Changelog
Tracks changes to `@bitlogin/core` and `@bitlogin/widget` that matter to a
site with its own vendored copy (see "Updating your integration" in
`README.md`). Not every commit needs an entry — only ones where a site
sitting on an older vendored copy is missing a fix or behavior change it
would plausibly want. Newest first.
## 2026-08-04 (security-audit response, same day)
- **External signers are now verified, not trusted.** NIP-07 and NIP-46
`signEvent` results are checked for a valid signature, the session's
identity, and field-for-field equality with the requested event; NIP-46
`auth_url` is surfaced only as a validated https URL with its hostname
shown. Relay queries enforce `filter.limit` locally, drop replayed
duplicates, cap buffering, and can no longer be stranded by a relay dying
mid-query. Passkey PRF fallback assertions are pinned to the
just-created credential. If your integration relied on an extension
returning altered events, that now errors — by design.
## 2026-08-04
- **CSP fix every self-hosting site needs: add `'wasm-unsafe-eval'` to
`script-src`.** Argon2id (hash-wasm) compiles a WASM module in the crypto
worker; a `script-src 'self'` policy without that keyword blocks
`WebAssembly.compile` and breaks every password flow. The keyword permits
WASM compilation only — JS `eval` stays banned. Related: if your service
worker precaches the widget, fetch with `cache: "reload"` during install —
an asset revalidating to a 304 keeps its OLD stored headers, and a worker
script cached with a stale CSP runs under that stale policy (this shipped
once; see the demo's `sw.js` for the pattern).
- **One login surface, four methods.** Behind a collapsed "More sign-in
options" menu (password stays primary): NIP-07 extension delegation,
NIP-46 remote signers (bunker:// paste + a dot-style nostrconnect QR;
NIP-44 transport only; memory-only sessions), and passkey sign-in
(WebAuthn PRF derives the account credential under a frozen contract —
zero site setup). Alternative-method sessions are signer-only: no vault
(`requestNwcConnection` resolves null), no persistence.
- **Host API additions.** `bitlogin-login` now carries `detail.method` and
`detail.capabilities` (existing `detail.publicKey` unchanged);
`window.bitlogin` gains `activeMethod()` / `activeSession()` — use these
for session state, NOT `isActiveSigner()`, which stays a window.nostr
ownership check and is deliberately false during NIP-07 sessions.
`window.nostr` is now an element-routed provider that follows the active
signer.
- **UI polish.** Screen headers with icon back buttons, right-aligned
"Forgot password?", icon+description option rows with 56px touch targets.
- New widget dependency bundled in: `qrcode-generator` (QR for
nostrconnect). File count and deployment layout unchanged.
## 2026-07-30
- **Widget: `requestNwcConnection()` — the Connection Vault authorization
layer (reveal mode).** Apps ask for a wallet; the widget owns sign-in,
one-tap approval for an origin-bound connection, or guided import via
Bitcoin Connect's chooser (lazy chunks — never downloaded unless the flow
opens) or a pasted URI, stored as an encrypted vault record. Dashboard
gains a "Wallet connections" management screen (revoke app access vs
remove-from-BitLogin, honestly distinguished). The vault root now rides
the encrypted session cache, so restored sessions can use connections
without re-login; the sudo key is never cached. New deploy note: the
widget now ships FIVE files (two extra lazy chunks) that must travel
together, and hosts using the wallet flow need `style-src-attr
'unsafe-inline'` in their CSP.
- **Widget: `offerNwcConnection()` — offer-to-save for app-obtained
wallets.** The inverse of `requestNwcConnection`: an app that got an NWC
URI through its own chooser or paste box can offer the user a portable
copy. Consent-gated in the widget's own UI, never silent (the write goes
to the user's account); duplicates by wallet+secret resolve
`already-saved` with a silent origin-binding refresh and no UI, so apps
can fire it after every successful connect.
- **Connection Vault protocol layer in `@bitlogin/core`** (`src/vault/`):
encrypted per-connection records signed by a derived vault identity, the
finalized §CV6 key hierarchy with pinned test vectors, the NWC credential
profile (lossless URI parse/export), relay sync with quorum readback and
per-record rollback detection, and the two-tier model — connectable
records vs sudo-gated personal records (`connection-vault.md` §18).
- **Capsules gained paired optional fields** `connection_vault_root` and
`vault_sudo_key` (both or neither; validated). Registration mints them for
new accounts; `enableConnectionVault` is the phrase-gated, fail-closed,
idempotent migration for existing accounts. Login, password change, and
phrase recovery all carry them forward.
**Integration caveat:** a site on an OLDER vendored widget that performs a
password change will rebuild the credential capsule WITHOUT these fields,
stranding connectable records until a phrase ceremony repairs them —
update vendored copies before enabling the vault for users.
## 2026-07-21
- **Fix: registration/login could fail quorum if a single built-in relay was
down.** `BUILTIN_VAULT_RELAYS` had only 3 relays and `BUILTIN_DISCOVERY_RELAYS`
only 2, sharing `relay.nostr.band` between both lists. `publishAndVerify()`
requires a fixed `minAcks`/`minReadbacks` of 2 regardless of relay count, so
losing that one relay left zero margin — surfacing as "Registration did not
reach the required relay acknowledgement and readback quorum." Added
`nostr.wine` and `relay.snort.social` to the vault list, and
`nostr-pub.wellorder.net` to discovery. (`681f622`)
**Re-vendor if:** your site uses the default built-in relay list (i.e. you
don't pass your own `vault-relays`/`discovery-relays` attributes). If you
already pass your own relay list, this doesn't affect you.
- **Fix: widget completely unstyled (giant logo, unstyled buttons) under a
strict `style-src` CSP.** `element.ts` injected its CSS as an inline
`<style>` tag inside the shadow root on every render. A host with
`style-src 'self'` and no `'unsafe-inline'` silently drops an inline
`<style>`'s rules entirely — visible as the brand SVG rendering at its raw
~590×119px intrinsic size instead of the intended 20px-tall lockup, with
every other shadow-DOM element similarly falling back to unstyled browser
defaults. Switched to building the stylesheet once as a `CSSStyleSheet` and
assigning it via `adoptedStyleSheets`, which isn't subject to `style-src`.
(`e8712f9`)
**Re-vendor if:** your site's CSP sets `style-src` without `'unsafe-inline'`
(check your CSP header/meta tag). If your `style-src` already includes
`'unsafe-inline'`, or you have no CSP, this was invisible to you either
way, but re-vendoring is still a good idea (this fix also drops needless
per-render CSS reparsing).
- **Feature: NIP-04 support, for drop-in parity with a real NIP-07
extension.** `window.nostr.nip04` (and the element-scoped
`nip04Encrypt`/`nip04Decrypt`) didn't exist at all before this — only
NIP-44 was implemented. A site (or its own legacy DM code path) calling
`window.nostr.nip04.encrypt(...)` while BitLogin was the active signer got
`Cannot read properties of undefined` instead of a working call. (`2838402`)
**Re-vendor if:** your site (or any third-party code running on it) uses
`window.nostr.nip04` — a real NIP-07 extension always has it, so anything
written against "whatever `window.nostr` provides" may already assume it's
there. No attribute/config changes needed; this is purely additive.
## 2026-07-22
- **Feature: sessions persist across a page reload.** Previously every reload
destroyed the crypto worker, requiring the full login-name + password flow
again — re-running Argon2id and a relay quorum read on every navigation,
not just the first sign-in. After a successful register/login/recover/
changePassword, the everyday private key and active capsule events are now
cached in IndexedDB; `connectedCallback()` tries to restore that cache
before ever showing the welcome screen, firing `bitlogin-login` exactly as
a fresh login would if one is found. Logout clears it. (`9e3787d`)
**Security note:** this means the decrypted everyday private key now sits
in the browser's IndexedDB for that origin until logout — the same
tradeoff every NIP-07 extension and crypto wallet already makes for the
same "don't ask for the password every navigation" reason, scoped by
IndexedDB's own per-origin isolation. The recovery phrase and login
name/password are never cached.
**Re-vendor if:** you want this — it's the reason to re-vendor this entry.
No attribute/config or host-integration changes needed; a host page's
existing `bitlogin-login` listener (see "Embedding BitLogin" below) fires
the same way whether the sign-in was just typed or silently restored.