# Connection URI and key exchange A WizardConnect session starts with the dapp generating a connection URI. The wallet scans this URI (typically as a QR code) and both sides use it to establish an encrypted channel. ## URI format ``` wiz://?p=&s= ``` When using a non-default relay: ``` wiz://:?p=&s=&pr= ``` | Parameter | Encoding | Size | Meaning | |-----------|----------|------|---------| | `p` | bech32-padded | 32 bytes | Dapp's Nostr public key (x-only secp256k1) | | `s` | bech32-padded | 8 bytes | Shared secret for key exchange verification | | `hostname` | URL authority | — | Relay hostname (default: `relay.cauldron.quest`) | | `port` | URL authority | — | Relay port (default: `443`) | | `pr` | query param | — | `ws` or `wss` (default: `wss`, omitted when default) | The URI omits the authority entirely when relay defaults are used, keeping QR codes compact. ### QR code encoding — alphanumeric mode QR codes support several character encodings. **Alphanumeric mode** encodes each character in 5.5 bits instead of 8 bits (byte mode), producing ~30 % smaller codes for the same data. The alphanumeric charset covers: `A–Z`, `0–9`, and the symbols `$`, `%`, `*`, `+`, `-`, `.`, `/`, `:`, and space. The bech32 alphabet (`a–z`, `2–7`) maps entirely into this charset when uppercased — every bech32 character is either a letter or a digit. The URI scheme characters (`WIZ`, `://`) and port numbers are also fully alphanumeric-safe. However, the standard URI separators `?`, `=`, and `&` are **not** in the alphanumeric charset. Their percent-encoded forms are safe: | Char | Encoding | All chars alphanumeric? | |------|----------|------------------------| | `?` | `%3F` | `%` ✅ `3` ✅ `F` ✅ | | `=` | `%3D` | `%` ✅ `3` ✅ `D` ✅ | | `&` | `%26` | `%` ✅ `2` ✅ `6` ✅ | `encodeKeyExchangeURI()` returns both a standard URI and a QR-safe URI: ```typescript const { uri, qrUri } = encodeKeyExchangeURI(publicKey, secret); // uri → "wiz://?p=qpzry9x8...&s=qpzry9x8" (standard, for copy-paste) // qrUri → "WIZ://%3FP%3DQPZRY9X8...%26S%3DQPZRY9X8" (QR alphanumeric-safe) ``` Use `uri` for display and clipboard copy. Use `qrUri` for QR code generation. `decodeKeyExchangeURI()` accepts both formats — it detects the QR encoding by looking for `%3f` without a `?` and reverses the substitutions before parsing. Both uppercase and lowercase variants are accepted. ### Example URIs Default relay, standard format (as produced by `encodeKeyExchangeURI`): ``` wiz://?p=qpzry9x8gf2tvdw0s3jn54khce6mua7lqpzry9x8gf2tvdw0s3jn54khce6mua7l&s=qpzry9x8 ``` QR-alphanumeric-safe format (use this for QR code generation): ``` WIZ://%3FP%3DQPZRY9X8GF2TVD...%26S%3DQPZRY9X8 ``` Custom relay: ``` wiz://my-relay.example.com:8443?p=qpzry9x8...&s=qpzry9x8&pr=wss ``` ## Credentials The dapp generates a fresh keypair and a random 8-byte secret for each new connection: ```typescript interface KeyExchangeCredentials { privateKey: string; // hex, 32 bytes — dapp's Nostr private key publicKey: string; // hex, 32 bytes — derived from privateKey (x-only) secret: string; // hex, 8 bytes — shared secret for MITM prevention } ``` `generateKeyExchangeCredentials()` creates a fresh set. `encodeKeyExchangeURI(publicKey, secret)` produces the scannable URI from them. ### Reconnection A dapp can reconnect to an existing session by passing `existingCredentials: { privateKey, secret }` to `initiateDappRelay()`. The wallet's public key is re-exchanged on reconnect. ## Key exchange flow Once the wallet scans the URI, the following happens over the relay: ``` 1. Wallet sends: wallet_ready { ..., public_key: , secret: } (wallet_ready carries both the application handshake data and the key exchange fields) 2. Dapp receives wallet_ready: - Verifies secret matches its own credentials.secret. - Calls client.setPairedPublicKey(walletPublicKey). - Fires keyexchangecomplete event. - Processes the session data (xpubs) as usual. - From this point, all outbound messages are encrypted to the wallet's pubkey. 3. Both sides now have each other's Nostr public keys. Subsequent messages are encrypted and filtered to the paired peer. ``` The key exchange data (`public_key`, `secret`) is embedded directly in `wallet_ready`. The wallet sends a single message and the dapp learns the wallet's pubkey atomically with the session data. ### Why a shared secret? The secret prevents a MITM or a curious third party from completing the key exchange with a wallet using a stolen URI. The wallet must echo back the correct 8-byte secret; if it doesn't match, the dapp ignores the `wallet_ready`. Eight bytes (64-bit) is enough entropy for a short-lived pairing code — it is not a long-term secret. ### Why Nostr keys? Nostr keys are secp256k1 keys, the same curve used in Bitcoin/BCH. Using a Nostr relay and NIP-17 gift wrap gets us: - A widely-deployed, censorship-resistant message bus. - End-to-end encryption with a simple, audited encryption scheme. - No WizardConnect-operated server in the critical path — anyone can run a relay. The "Nostr x-only pubkey" format is just the 32-byte x-coordinate of the secp256k1 public key (no 02/03 prefix). `deriveNostrPublicKey(privateKey)` computes this from a 32-byte private key. ## Peer filtering After key exchange, `RelayClient` compares the `pubkey` field of every decrypted Nostr event against the paired public key. Messages from any other pubkey are dropped with a warning log. `wallet_ready` bypasses this filter because the dapp does not yet know the wallet's pubkey when it first arrives — it is the message that *establishes* the pairing. ## API surface ```typescript // Generate fresh credentials (called internally by initiateDappRelay) generateKeyExchangeCredentials(): KeyExchangeCredentials // Encode a URI from an existing keypair — returns both standard and QR-safe forms encodeKeyExchangeURI(publicKey: string, secret: string, options?): KeyExchangeURIResult interface KeyExchangeURIResult { uri: string; // standard URI for copy-paste: wiz://?p=...&s=... qrUri: string; // QR-alphanumeric-safe URI: WIZ://%3FP%3D...%26S%3D... } // Decode and validate a wiz:// URI // Accepts standard format, QR format (%3F/%3D/%26), uppercase, and mixed-case decodeKeyExchangeURI(uri: string): DecodedKeyExchangeURI interface DecodedKeyExchangeURI { publicKey: string; // hex, 32 bytes secret: string; // hex, 8 bytes hostname: string; port: number; protocol: "ws" | "wss"; } ```