Files
Alex Gleasonandfiatjaf a2fd116f81 fix(nip46): require a secret in nostrconnect:// URIs
fromURI() matched the bunker's response against
`uri.searchParams.get('secret')`, which is `null` when the URI carries no
secret parameter. A bunker answering `{"result": null}` then satisfies
`response.result === null` and is adopted as the client's signer.

Since the client subscribes to the relays named in the URI and the
connection event is public, any participant on those relays can win that
race and sign on the user's behalf.

The secret is the only thing distinguishing the bunker we asked from
everyone else who saw the URI, so treat a missing or empty one as a
programming error and reject before subscribing. createNostrConnectURI()
already always sets it; this only affects URIs built elsewhere.
2026-08-19 17:06:48 -03:00

28 lines
1.1 KiB
TypeScript

import { test, expect } from 'bun:test'
import { BunkerSigner, createNostrConnectURI } from './nip46.ts'
import { generateSecretKey, getPublicKey } from './pure.ts'
const clientSecretKey = generateSecretKey()
const clientPubkey = getPublicKey(clientSecretKey)
test('createNostrConnectURI always includes the secret', () => {
const uri = new URL(createNostrConnectURI({ clientPubkey, relays: ['wss://relay.example.com'], secret: 'hunter2' }))
expect(uri.searchParams.get('secret')).toEqual('hunter2')
})
test('fromURI rejects a URI without a secret', async () => {
const uri = `nostrconnect://${clientPubkey}?relay=wss://relay.example.com`
// otherwise a bunker replying `{"result": null}` would match `get('secret')`
// and become the signer for this client
await expect(BunkerSigner.fromURI(clientSecretKey, uri)).rejects.toThrow(/no secret/)
})
test('fromURI rejects a URI with an empty secret', async () => {
const uri = `nostrconnect://${clientPubkey}?relay=wss://relay.example.com&secret=`
await expect(BunkerSigner.fromURI(clientSecretKey, uri)).rejects.toThrow(/no secret/)
})