theseus/DESIGN-integrated-wallet.md
Local Dev 755e97b5bb Ship Theseus 0.0.8: window.bcnr dApp API + eTLD+1 permission origins
Merges a parallel session's work with the multi-source BNS story from 0.0.7.

The dApp side (parallel session)
--------------------------------
* bcnr-preload.js — installs `window.bcnr` on every page via contextBridge.
  Read-only surface: resolveName(name), isRegistered(name), getBcnrTlds(),
  getRecordVersion(name), plus getPermissionOrigin() for diagnostics. All
  Promises; a missing name returns null (not throw). No signing, no wallet
  unlock — that surface is designed but deliberately out of scope for 0.0.8
  (see TheseusNavigator/DESIGN-integrated-wallet.md).
* bcnr-origin.js — pure function that computes the eTLD+1 permission origin
  for a URL. ICANN suffixes via `psl` (same PSL Chromium uses, handles
  .co.uk / .github.io / etc); BNS names key off the on-chain TLD list so
  foo.wallet becomes a public suffix as soon as `wallet` appears there.
  Match browser cookie / MetaMask semantics: a grant on pay.merchant.com
  covers account.merchant.com but not evil.com.
* dev/bcnr-selftest.js, dev/origin-selftest.mjs — self-tests, no I/O.
* main.js wires bcnr-preload.js into session.defaultSession.setPreloads() so
  it runs BEFORE per-WebContentsView preloads; adds bcnr:* IPC handlers.
* preload.js + chrome.html — small hooks so the shell picks up window.bcnr
  the same way regular content does.
* package.json — psl dep, bcnr-preload.js/bcnr-origin.js in `files`.

Also included
-------------
* AriadneResolver/mobile/.../UpdateCheck.java — in-app update-check for the
  Android app; already active in the shipped 0.11 APK (build.ps1 -Recurse
  picked it up), formalising the source now.
* TheseusNavigator/snapshots/bns-name-snapshot.json — refreshed bundled
  starter (73 beacon txs, root c37b8596…c54e414ba).
* Site pages + manifest updated to point at 0.0.8.

  TheseusNavigator-Setup-0.0.8.exe    95.4 MB
    21939743eafdfe8742a6b7c4b987bd2782384d7bc41289cb80a7e08019dc9f02
  TheseusNavigator-0.0.8-portable.exe 92.7 MB
    2aa429fe39dc0fa4ac040fc6d6eb31b0f890c8a83175c49fcb50f052c480d39d
2026-08-31 01:38:55 +02:00

450 lines
21 KiB
Markdown

# Theseus integrated BCH wallet — design
Companion to [`ROADMAP-identity-wallet.md`](ROADMAP-identity-wallet.md) Strand B
and [`DESIGN-password-manager.md`](DESIGN-password-manager.md) (same crypto
discipline, distinct purpose subtree). Same shape as the password design doc:
threat model → permission model → API → UI → sequencing → open questions.
The reference point is deliberate: **MetaMask in Brave.** A wallet the user
already trusts, injected as `window.bcnr` into every page, permissioned per
origin, and additionally exposed to *outside* dApps via WizardConnect so a
mobile-only site can pair Theseus like a hardware wallet.
Everything below is chipnet only until explicitly directed otherwise.
---
## 0. Scope
**In:**
- `window.bcnr` provider injected on every page (read-only immediately;
permissioned methods gated per origin).
- The user's wallet lives in Theseus. No external key material.
- Modal-confirmed operations: `signMessage`, `sendPayment`, `registerName`,
`updateName`, `transferName`.
- WizardConnect **responder**: dApps outside Theseus (or inside it) can pair
via `wiz://` URI + QR to sign transactions with the Theseus wallet.
- Settings → Wallet section: connected sites, per-site/per-method revoke,
transaction history.
- Reuse of the already-shipped `BuiltInWallet`
([Argus/src/lib/wallet-web.js:BuiltInWallet](../Argus/src/lib/wallet-web.js))
as the signing core.
- Reuse of the already-shipped registration/update flow
([Argus/src/lib/register-tx.js](../Argus/src/lib/register-tx.js) +
[Argus/src/lib/registrar.js](../Argus/src/lib/registrar.js)).
**Out (deliberate, deferred, or handled elsewhere):**
- Ethereum / Solana / other-chain. BCH only in this strand.
- Hardware-wallet abstraction (Ledger/Trezor). Phase 4 conversation.
- WalletConnect v2. Explicitly dropped 2026-07-27 in favour of WizardConnect
(see [`Decentralized.DNS/REGISTRATION-STATUS.md`](../Decentralized.DNS/REGISTRATION-STATUS.md)).
We do not want a Reown project ID as a dependency.
- CashConnect V0. Cannot mint name certificates by design
([`REGISTRATION-STATUS.md`](../Decentralized.DNS/REGISTRATION-STATUS.md) §
*CashConnect cannot mint*).
- Nostr messenger (NIP-07 client for arbitrary Nostr apps). Separate purpose
subtree, Strand B.5 in the roadmap, not this design.
- Multi-account UX. One account per Theseus profile in v1.
---
## 1. Threat model
The provider becomes malware infrastructure the moment its permission model
fails. Three attack surfaces to lock down:
### 1.1 Malicious page reading the provider surface
Every loaded page sees `window.bcnr`. It must not be possible to enumerate
addresses, balances, or names without an explicit user grant.
- **Read-only methods** (`resolveName`, `getBcnrTlds`, `isRegistered`) are
chain queries — they leak nothing about the user. No permission.
- **All identifying methods** (`getAccounts`, `signMessage`, `sendPayment`,
`registerName`, `updateName`, `transferName`) require an origin permission
that started with a modal.
- **`getAccounts` returns `[]` for un-granted origins**, not a rejection.
Rejection is a signal; empty is opaque.
### 1.2 Compromised page after permission granted
An origin can be captured (XSS, subdomain takeover, malicious ad on a trusted
host) after the user granted a permission. Assume this.
- **Per-method scope**, not just per-origin. Granting "sign message" does not
grant "send payment". A page has to earn each modal separately.
- **`sendPayment` has a per-approval amount cap.** Approving `example.bch` for
payments does not approve unlimited spending — the modal shows an amount, and
a bigger amount re-prompts. "Approve unlimited" is not a UI option.
- **Every mint / update / transfer is modal-confirmed every time.** On-chain
writes are irrevocable; there is no "trust for the session" mode.
### 1.3 Origin confusion (phishing, homoglyph, subdomain)
- Origin is bound to the **eTLD+1 via the public suffix list snapshot** shipped
with Theseus (same rule as password autofill, A.2 in the roadmap). No runtime
fetch, no third-party dependency.
- **BCNR names get a distinct origin tier** in the confirmation modal:
`alice.bch` is a first-class origin (backed by an on-chain certificate),
`alice.com` is a second-class origin (registrar-leased). The modal shows the
distinction so the user knows what they're trusting.
- **The recipient's cash address is shown verbatim in the confirmation.** If
the address changes between two confirmations from the same origin during one
session, a red warning banner appears — "the address this site is asking you
to sign for has changed since your last approval."
### 1.4 What we are NOT defending against
- A user manually copying their recovery phrase into a phishing site. No wallet
can defend against this and pretending otherwise misleads users.
- A user who unlocks the wallet on a shared machine and walks away. Auto-lock
helps but is not a substitute.
- Compromise of Theseus itself (RCE, malicious extension, keylogger). The
password vault and the wallet share the same threat model here — game over.
---
## 2. Permission model
### 2.1 Per-origin, per-method, per-limit
Stored in `<userData>/wallet-permissions.json`:
```json
{
"silentmode.bch": {
"grantedAt": 1728000000000,
"methods": {
"signMessage": { "grants": 12 },
"sendPayment": { "capSats": 100000, "used": 45000, "grants": 3 },
"registerName": { "grants": 1 }
}
}
}
```
- `capSats` is the **per-approval** cap the site was granted, not a cumulative
budget. A payment above it re-prompts with a new modal.
- `used` and `grants` are for the Settings audit trail. The user sees "this
site has spent X and signed Y messages" — accountability.
- **No wildcard origins.** Every subdomain is a separate grant.
### 2.2 First-use flow
1. Page calls `bcnr.requestAccount()` (or any permissioned method).
2. Theseus renders a **connect modal** as a `WebContentsView` overlay (same
pattern as popover, engine-picker, downloads).
3. Modal shows:
- Origin, with the BCNR-vs-ICANN badge.
- What is being asked (specific method, specific parameters).
- "Connect to this site with account `bchtest:qxxx…yyy`" (the wallet's
receive address 0, truncated with copy-full option).
- **Two buttons**: `Approve once` (single-shot, no permission stored) and
`Approve for this site` (grant persisted).
4. On approve, the permission is written and the promise resolves.
5. On reject, the promise resolves with the same "empty result" pattern as an
un-granted origin (no distinguishing signal to the page).
### 2.3 Revocation
Settings > Wallet > Connected sites. Table:
`origin | granted | last used | scopes | [revoke]`. Per-scope revoke checkboxes,
plus a nuclear "revoke all". Revocation clears `wallet-permissions.json` and
takes effect on the origin's next call (no restart needed).
### 2.4 What the provider CAN'T do
- Read the vault, seed, or any other purpose subtree.
- Sign anything without a modal.
- Enumerate connected sites (that's a settings-scope API, not page-scope).
- Watch the wallet balance across origins (each origin sees only its own
granted accounts — usually one).
---
## 3. `window.bcnr` provider — the API
Injected by a preload extension registered via
`session.defaultSession.setPreloads(...)` so it is present on every page. The
preload uses `contextBridge.exposeInMainWorld("bcnr", …)` and forwards to main
via IPC on a dedicated `bcnr:*` channel.
### 3.1 Read-only (no permission)
```ts
bcnr.resolveName(name: string): Promise<Entry | null>
bcnr.isRegistered(name: string): Promise<boolean>
bcnr.getBcnrTlds(): Promise<string[]> // ['bch','p2p','nav',...]
bcnr.getRecordVersion(name: string): Promise<number> // for cache invalidation
```
### 3.2 Identity (permissioned; first call → modal)
```ts
bcnr.requestAccount(): Promise<string | null> // returns cashaddr or null
bcnr.getAccounts(): Promise<string[]> // [] if not granted
bcnr.signMessage({ message: string, address?: string, userPrompt?: string }):
Promise<{ signature: string, address: string }> // BIP-137
```
### 3.3 Payments (permissioned; per-amount modal)
```ts
bcnr.sendPayment({
to: string, // cashaddr OR BCNR name resolvable to an `a` record
amountSats: bigint,
memo?: string,
userPrompt?: string,
}): Promise<{ txid: string }>
```
### 3.4 Name operations (each modal-confirmed)
```ts
bcnr.registerName({ name: string, records?: Record<string,string>, userPrompt?: string }):
Promise<{ txid: string, category: string }>
bcnr.updateName({ name: string, records: Record<string,string>, userPrompt?: string }):
Promise<{ txid: string }>
bcnr.transferName({ name: string, toAddress: string, userPrompt?: string }):
Promise<{ txid: string }>
```
### 3.5 Events
```ts
bcnr.on('accountsChanged', (accounts: string[]) => void)
bcnr.on('permissionsRevoked', (methods: string[]) => void)
bcnr.on('lock', () => void) // wallet auto-locked
```
### 3.6 Match with WizardConnect / WC2 shape where cheap
`signMessage` returns `{signature, address}` — same shape as
`bch_signMessage` in the wc2-bch-bcr namespace (see
[Parameters/WalletConnect/WalletConnect_integration_reference.md](../../Deviant/Parameters/WalletConnect/WalletConnect_integration_reference.md)
§4.2). Pages that already speak WC2 can adapt with a two-line shim.
We do NOT re-expose the WC2 wire format (`stringify` with `<Uint8Array: 0x..>`
markers) at the provider layer — that is a WC2 transport concern, not a
provider API concern. The provider takes plain values; the WizardConnect
responder (below) is where the wire format matters.
---
## 4. WizardConnect responder
The MetaMask analogue is "the wallet is here in the browser." The extra thing
we get from WizardConnect is "a dApp on another device — a mobile site, a
desktop app — can pair with the Theseus wallet by scanning a `wiz://` QR." This
is exactly the responder side of what Deviant is finishing
([`Parameters/WalletConnect/WIZARDCONNECT-INTEGRATION-GUIDE.md`](../../Deviant/Parameters/WalletConnect/WIZARDCONNECT-INTEGRATION-GUIDE.md)),
adapted to Theseus.
### 4.1 Wallet-side pieces
Same shape as Deviant:
- `@wizardconnect/wallet@0.2.2` for the SDK; already tested against the
0.2.4-core wire protocol. `nostr-tools` and `ws` are already in Theseus's
deps (verified 2026-08-13).
- A **`WalletAdapter`** wrapping Theseus's `BuiltInWallet`. Theseus's wallet
is sync-in-main-process behind the unlock modal, which is simpler than
Deviant's async biometric-KeyStore case:
- `walletName: "Theseus"`, `walletIcon: <shipped png>`
- `getRelayPrivateKey(uri): Uint8Array` = `sha256(sha256(utf8(uri)))`
(identical to Deviant's implementation — deterministic per URI so a
reconnect keeps the same Nostr identity, different dApps cannot
correlate; matches `@wizardconnect/wallet@0.2.2` types)
- `getXpub(path): string` — reads pre-cached xpubs from `BuiltInWallet`
- `getPublicKey(path, index)` — throws; the shipped SDK never calls it
- `signTransaction(request): Promise<{ signedTransaction: string }>`
unwraps the `WcSignTransactionRequest` (the bug Deviant hit and fixed —
see the integration guide §4), renders the same signing modal the
`window.bcnr` provider uses, and signs via `coSignTransaction`-equivalent
logic in `wallet-web.js`
- Signing uses **SIGHASH_ALL | FORKID | UTXOS (0x61)**. Normative in the
WizardConnect spec; Theseus's signer must accept this flag alongside 0x41.
See Deviant `signer.ts:51` `WIZARDCONNECT_SIGHASH` for the reference.
### 4.2 Pairing UX
- Address-bar chip when a `wiz://` URI is present on a page (paste-detect).
- Settings > Wallet > "Pair with a dApp" — QR scanner (uses the camera if
available; otherwise paste), takes a `wiz://` URI, opens the wallet-side
connection.
- **Sign-request modal is the SAME modal** as the in-Theseus `bcnr` provider.
A dApp asking for a signature via `window.bcnr` from a loaded page and a
dApp asking via WizardConnect from a paired external site both hit the same
confirmation surface. This is the property that makes the responder feel
"already part of the browser" rather than a second wallet UX.
### 4.3 Session persistence
- Reuse `@wizardconnect/dapp`'s `session` mechanism on the wallet side: the
Nostr identity per URI is deterministic, so a reconnect after a Theseus
restart is transparent.
- Persist active pairings in `<userData>/wallet-wz-sessions.json`.
- Settings > Wallet > Paired dApps: list, revoke, "disconnect all".
### 4.4 The distinction to keep visible in code
**In-Theseus dApp using `window.bcnr` = local IPC.** No relay, no Nostr, no
transport. Fast and private.
**External dApp using WizardConnect = Nostr relay.** Encrypted end-to-end (NIP-17
gift-wrap), but the relay sees timing and message sizes. Users should know they
picked the second one when they scanned a QR.
The wallet doesn't need to distinguish for *signing purposes* — same modal,
same signer, same permission model — but the address-bar chip and the settings
page should visibly separate `Connected sites` from `Paired dApps`.
---
## 5. UI surfaces
New WebContentsView overlays, each needing a preload extension registered and
listed in `TheseusNavigator/package.json` `build.files` (per `GOTCHAS.md`):
| Overlay | Preload | Purpose |
|---|---|---|
| `wallet-connect-modal.html` | `wallet-connect-preload.js` | first-use connect prompt |
| `wallet-sign-modal.html` | `wallet-sign-preload.js` | signature/payment/name-op confirmation |
| `wallet-pair-modal.html` | `wallet-pair-preload.js` | WizardConnect QR scan / URI paste |
The **address-bar chip** grows an icon when:
- The current origin has an active permission (green key)
- The current page contains a `wiz://` URI (purple wand)
- The wallet is locked and the page is asking for something (grey with a lock)
Click the chip → shows a summary: which origin, which methods granted, spend
so far, revoke options.
**Settings > Wallet** — a new pane alongside Passwords:
- Account row: address, balance, "backup phrase" (behind master-password
re-prompt)
- Connected sites table
- Paired dApps table (WizardConnect)
- Transaction history (local; the on-chain source of truth is queryable via
the chip-per-tx explorer link)
- Auto-lock timeout
- **Danger zone**: reset wallet (wipes local state, does not touch on-chain
registrations — the certificates stay in the on-chain wallet the seed
controls)
---
## 6. Storage & unlock
- **Wallet key material lives in `<userData>/wallet.enc`**, encrypted the same
way passwords are: PBKDF2 → AES-256-GCM under the master password (see
[DESIGN-password-manager.md](DESIGN-password-manager.md) for the discipline).
- **Same master password** unlocks both the vault and the wallet. Unlocking one
unlocks the other for the session. The user picks the password once.
- **Distinct purpose subtree** — the seed derives a wallet purpose root via
HKDF with `info = "silentmode/wallet/0"`, independent of the password purpose
root at `"silentmode/passwords/0"`. Extend `password-vault.js`'s
`seedToPurposeRoot(...)` — it already takes a purpose string.
- **The signing key is derived on demand** from the purpose root, not held in
RAM long-term. Each `signMessage` / `sendPayment` reprises the derivation
from the unlocked purpose root.
- **Auto-lock** re-encrypts the purpose root under a fresh session key that is
discarded on lock. Unlock re-derives from the master password.
Cross-strand rule: **never derive a wallet key from the password purpose root
(or vice versa).** This is the discipline that keeps a website-XSS-driven
password leak from also leaking wallet keys. Documented explicitly in
`DESIGN-password-manager.md`.
---
## 7. Sequencing — the roadmap in order
| Phase | Roadmap ref | What ships | Depends on |
|---|---|---|---|
| B.1 | this doc | design doc, user aligned on permission model | — |
| B.2a | roadmap B.2 | preload injection scaffold, read-only APIs only | — |
| B.2b | roadmap B.2 | eTLD+1 origin binding (share PSL snapshot with A.2) | — |
| B.3a | roadmap B.3 | wallet unlock flow reused from password vault | A.2 shipped |
| B.3b | roadmap B.3 | `signMessage` + connect modal + settings-connected-sites | B.2 + B.3a |
| B.3c | roadmap B.3 | `sendPayment` with per-amount cap | B.3b |
| B.4 | roadmap B.4 | `registerName` / `updateName` / `transferName` | B.3b |
| **W.1** | *(new)* | WizardConnect responder — WalletAdapter + pair modal | B.3b |
| **W.2** | *(new)* | Settings-paired-dApps, session persistence, revoke | W.1 |
WizardConnect is new to the roadmap — the existing `B.6 — Advanced connect-
wallet UX` bullet was vague and `B.5 — Nostr messenger` conflates transport
with client. Suggest updating the roadmap: **replace `B.6` with `W.1`+`W.2`
above**, keep `B.5` for a Nostr messenger client if that's still wanted, but
they are independent projects that happen to share a transport.
W.1 depends on B.3b (the sign modal) so the same modal serves both surfaces.
Do NOT ship W.1 before there is a working sign modal, or the WizardConnect
responder will ship its own approval UI and the two will drift.
---
## 8. Open questions — decide before writing B.2+ code
1. **Chain switching.** Chipnet only in v1 — but what does `window.bcnr` do on
a mainnet dApp that expects mainnet? Options:
(a) Error with a clear message,
(b) present a "this is a chipnet wallet" modal,
(c) support both networks with a per-origin chain preference.
*Recommend (a) for v1; revisit at mainnet switch.*
2. **`bcnr.sendPayment` recipient by name.** If `to: "alice.bch"`, do we
resolve the `a` record (payment cashaddr) automatically, or force the caller
to `bcnr.resolveName` first and pass a cashaddr? *Recommend auto-resolve;
show the resolved address in the modal for the user to check.*
3. **QR scanner for pairing.** Camera-based (Electron desktop can access
camera, but macOS permission is friction) vs paste-only. *Recommend
paste-first, camera as an enhancement in W.2 or later.*
4. **BCNR-name provenance in the modal.** Show a "registered on-chain since
height X, by category Y" line for BCNR-native origins? Helpful for advanced
users, noise for others. *Recommend a collapsed disclosure that opens on
click, not a default-visible field.*
5. **Rate-limiting on read-only methods.** A page loop calling `resolveName`
thousands of times could DoS the resolver pool. *Recommend a per-origin
token bucket in the preload — 20 requests/second, drops the excess with a
normal-looking error.*
6. **Backup phrase display.** Behind the master password re-prompt, plain
text or blur-until-hover? *Recommend blur-until-hover with a "why is this
dangerous" panel that must be dismissed once before the phrase reveals.*
7. **Transaction history storage.** Local JSON, encrypted like the vault, or
just derived from chain queries on demand? *Recommend derived-on-demand for
v1 — no new storage, no sync surface, and the chain is authoritative.*
8. **`transferName` — does it need commit-reveal?** No, because a certificate
transfer is just a token send; the commit-reveal spec applies to
*registrations*. But the modal should still say "this sends the on-chain
ownership token to a stranger and cannot be undone."
---
## 9. What this design does NOT decide
- The exact modal visual design. Ship a functional MVP; polish in a UX pass.
- The wallet's home screen (balance, recent transactions). Settings > Wallet
is sufficient in v1; a dedicated pane is a v2 conversation.
- On-chain price display (fiat quotes). Deliberate — Silent Mode does not run
a fiat oracle, and pulling from an external price API undoes half the point
of the project.
- Multi-account UX. Single account per profile; sub-accounts / vanity
accounts are a future strand.
- Whether the wallet ships to Ariadne mobile as a companion. That is a
separate mobile project; the design here informs it but does not require it.
---
## 10. References (do not re-derive)
- Existing wallet:
[`Argus/src/lib/wallet-web.js:BuiltInWallet`](../Argus/src/lib/wallet-web.js)
- Existing registration flow: [`Argus/src/lib/registrar.js`](../Argus/src/lib/registrar.js),
[`Argus/src/lib/register-tx.js`](../Argus/src/lib/register-tx.js)
- Existing dApp-side WizardConnect (SilentMode registrar): [`Argus/src/lib/connect/wizardconnect.js`](../Argus/src/lib/connect/wizardconnect.js)
- Existing wallet-side WizardConnect (Deviant, chipnet-validated):
[`../../Deviant/Parameters/WalletConnect/WIZARDCONNECT-INTEGRATION-GUIDE.md`](../../Deviant/Parameters/WalletConnect/WIZARDCONNECT-INTEGRATION-GUIDE.md)
- WizardConnect LGPL analysis: [`../Decentralized.DNS/WIZARDCONNECT-LICENSE-FINDING.md`](../Decentralized.DNS/WIZARDCONNECT-LICENSE-FINDING.md)
- Crypto discipline (purpose subtrees, PBKDF2, HKDF):
[`DESIGN-password-manager.md`](DESIGN-password-manager.md)
- Registration flow status: [`../Decentralized.DNS/REGISTRATION-STATUS.md`](../Decentralized.DNS/REGISTRATION-STATUS.md)