Add ignore logic to stop interfering with replies from other instances

This commit is contained in:
artur 2024-02-16 14:46:36 +03:00
parent a500a2e2a5
commit 46336d817f
3 changed files with 205 additions and 135 deletions

View File

@ -18,7 +18,7 @@ function App() {
const load = useCallback(async () => { const load = useCallback(async () => {
const keys: DbKey[] = await dbi.listKeys() const keys: DbKey[] = await dbi.listKeys()
console.log(keys, 'keys') // console.log(keys, 'keys')
dispatch(setKeys({ keys })) dispatch(setKeys({ keys }))
const loadProfiles = async () => { const loadProfiles = async () => {

View File

@ -1,8 +1,7 @@
import { generatePrivateKey, getPublicKey, nip19 } from 'nostr-tools' import { Event, generatePrivateKey, getPublicKey, nip19, verifySignature } from 'nostr-tools'
import { DbApp, dbi, DbKey, DbPending, DbPerm } from './db' import { DbApp, dbi, DbKey, DbPending, DbPerm } from './db'
import { Keys } from './keys' import { Keys } from './keys'
import NDK, { import NDK, {
IEventHandlingStrategy,
NDKEvent, NDKEvent,
NDKNip46Backend, NDKNip46Backend,
NDKPrivateKeySigner, NDKPrivateKeySigner,
@ -12,13 +11,20 @@ import NDK, {
NDKUser, NDKUser,
} from '@nostr-dev-kit/ndk' } from '@nostr-dev-kit/ndk'
import { NOAUTHD_URL, WEB_PUSH_PUBKEY, NIP46_RELAYS, MIN_POW, MAX_POW, KIND_RPC, DOMAIN } from '../utils/consts' import { NOAUTHD_URL, WEB_PUSH_PUBKEY, NIP46_RELAYS, MIN_POW, MAX_POW, KIND_RPC, DOMAIN } from '../utils/consts'
import { Nip04 } from './nip04' // import { Nip04 } from './nip04'
import { fetchNip05, getReqPerm, getShortenNpub, isPackagePerm } from '@/utils/helpers/helpers' import { fetchNip05, getReqPerm, getShortenNpub, isPackagePerm } from '@/utils/helpers/helpers'
import { NostrPowEvent, minePow } from './pow' import { NostrPowEvent, minePow } from './pow'
//import { PrivateKeySigner } from './signer' //import { PrivateKeySigner } from './signer'
//const PERF_TEST = false //const PERF_TEST = false
enum DECISION {
ASK = '',
ALLOW = 'allow',
DISALLOW = 'disallow',
IGNORE = 'ignore',
}
export interface KeyInfo { export interface KeyInfo {
npub: string npub: string
nip05?: string nip05?: string
@ -36,7 +42,7 @@ interface Key {
interface Pending { interface Pending {
req: DbPending req: DbPending
cb: (allow: boolean, remember: boolean, options?: any) => void cb: (allow: DECISION, remember: boolean, options?: any) => void
notified?: boolean notified?: boolean
} }
@ -63,23 +69,25 @@ class Watcher {
} }
async start() { async start() {
this.sub = this.ndk.subscribe({ this.sub = this.ndk.subscribe(
kinds: [KIND_RPC], {
authors: [(await this.signer.user()).pubkey], kinds: [KIND_RPC],
since: Math.floor((Date.now() / 1000) - 10), authors: [(await this.signer.user()).pubkey],
}, { since: Math.floor(Date.now() / 1000 - 10),
closeOnEose: false, },
cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY {
}) closeOnEose: false,
cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY,
}
)
this.sub.on('event', async (e: NDKEvent) => { this.sub.on('event', async (e: NDKEvent) => {
const peer = e.tags.find(t => t.length >= 2 && t[0] === "p") const peer = e.tags.find((t) => t.length >= 2 && t[0] === 'p')
console.log("watcher got event", { e, peer }) console.log('watcher got event', { e, peer })
if (!peer) return if (!peer) return
const decryptedContent = await this.signer.decrypt( const decryptedContent = await this.signer.decrypt(new NDKUser({ pubkey: peer[1] }), e.content)
new NDKUser({ pubkey: peer[1] }), e.content); const parsedContent = JSON.parse(decryptedContent)
const parsedContent = JSON.parse(decryptedContent); const { id, method, params, result, error } = parsedContent
const { id, method, params, result, error } = parsedContent; console.log('watcher got', { peer, id, method, params, result, error })
console.log("watcher got", { peer, id, method, params, result, error })
if (method || result === 'auth_url') return if (method || result === 'auth_url') return
this.onReply(id) this.onReply(id)
}) })
@ -91,91 +99,128 @@ class Watcher {
} }
class Nip46Backend extends NDKNip46Backend { class Nip46Backend extends NDKNip46Backend {
private allowCb: (params: IAllowCallbackParams) => Promise<DECISION>
private npub: string = ''
public constructor(ndk: NDK, signer: NDKSigner, allowCb: (params: IAllowCallbackParams) => Promise<DECISION>) {
super(ndk, signer, () => Promise.resolve(true))
this.allowCb = allowCb
signer.user().then((u) => (this.npub = nip19.npubEncode(u.pubkey)))
}
public async processEvent(event: NDKEvent) { public async processEvent(event: NDKEvent) {
this.handleIncomingEvent(event) this.handleIncomingEvent(event)
} }
}
class Nip04KeyHandlingStrategy implements IEventHandlingStrategy { protected async handleIncomingEvent(event: NDKEvent) {
private privkey: string const { id, method, params } = (await this.rpc.parseEvent(event)) as any
private nip04 = new Nip04() const remotePubkey = event.pubkey
let response: string | undefined
constructor(privkey: string) { this.debug('incoming event', { id, method, params })
this.privkey = privkey
}
private async getKey(backend: NDKNip46Backend, id: string, remotePubkey: string, recipientPubkey: string) { // validate signature explicitly
if ( if (!verifySignature(event.rawEvent() as Event)) {
!(await backend.pubkeyAllowed({ this.debug('invalid signature', event.rawEvent())
id, return
pubkey: remotePubkey,
// @ts-ignore
method: 'get_nip04_key',
params: recipientPubkey,
}))
) {
backend.debug(`get_nip04_key request from ${remotePubkey} rejected`)
return undefined
} }
return Buffer.from(this.nip04.createKey(this.privkey, recipientPubkey)).toString('hex') const decision = await this.allowCb({
} backend: this,
async handle(backend: NDKNip46Backend, id: string, remotePubkey: string, params: string[]) {
const [recipientPubkey] = params
return await this.getKey(backend, id, remotePubkey, recipientPubkey)
}
}
class EventHandlingStrategyWrapper implements IEventHandlingStrategy {
readonly backend: NDKNip46Backend
readonly npub: string
readonly method: string
private body: IEventHandlingStrategy
private allowCb: (params: IAllowCallbackParams) => Promise<boolean>
constructor(
backend: NDKNip46Backend,
npub: string,
method: string,
body: IEventHandlingStrategy,
allowCb: (params: IAllowCallbackParams) => Promise<boolean>
) {
this.backend = backend
this.npub = npub
this.method = method
this.body = body
this.allowCb = allowCb
}
async handle(
backend: NDKNip46Backend,
id: string,
remotePubkey: string,
params: string[]
): Promise<string | undefined> {
console.log(Date.now(), 'handle', {
method: this.method,
id,
remotePubkey,
params,
})
const allow = await this.allowCb({
backend: this.backend,
npub: this.npub, npub: this.npub,
id, id,
method: this.method, method,
remotePubkey, remotePubkey,
params, params,
}) })
if (!allow) return undefined console.log(Date.now(), 'handle', { method, id, decision, remotePubkey, params })
return this.body.handle(backend, id, remotePubkey, params).then((r) => { if (decision === DECISION.IGNORE) return
console.log(Date.now(), 'req', id, 'method', this.method, 'result', r)
return r const allow = decision === DECISION.ALLOW
}) const strategy = this.handlers[method]
if (allow) {
if (strategy) {
try {
response = await strategy.handle(this, id, remotePubkey, params)
console.log(Date.now(), 'req', id, 'method', method, 'result', response)
} catch (e: any) {
this.debug('error handling event', e, { id, method, params })
this.rpc.sendResponse(id, remotePubkey, 'error', undefined, e.message)
}
} else {
this.debug('unsupported method', { method, params })
}
}
if (response) {
this.debug(`sending response to ${remotePubkey}`, response)
this.rpc.sendResponse(id, remotePubkey, response)
} else {
this.rpc.sendResponse(id, remotePubkey, 'error', undefined, 'Not authorized')
}
} }
} }
// class Nip04KeyHandlingStrategy implements IEventHandlingStrategy {
// private privkey: string
// private nip04 = new Nip04()
// constructor(privkey: string) {
// this.privkey = privkey
// }
// private async getKey(backend: NDKNip46Backend, id: string, remotePubkey: string, recipientPubkey: string) {
// if (
// !(await backend.pubkeyAllowed({
// id,
// pubkey: remotePubkey,
// // @ts-ignore
// method: 'get_nip04_key',
// params: recipientPubkey,
// }))
// ) {
// backend.debug(`get_nip04_key request from ${remotePubkey} rejected`)
// return undefined
// }
// return Buffer.from(this.nip04.createKey(this.privkey, recipientPubkey)).toString('hex')
// }
// async handle(backend: NDKNip46Backend, id: string, remotePubkey: string, params: string[]) {
// const [recipientPubkey] = params
// return await this.getKey(backend, id, remotePubkey, recipientPubkey)
// }
// }
// FIXME why do we need it? Just to print
// class EventHandlingStrategyWrapper implements IEventHandlingStrategy {
// readonly backend: NDKNip46Backend
// readonly method: string
// private body: IEventHandlingStrategy
// constructor(
// backend: NDKNip46Backend,
// method: string,
// body: IEventHandlingStrategy
// ) {
// this.backend = backend
// this.method = method
// this.body = body
// }
// async handle(
// backend: NDKNip46Backend,
// id: string,
// remotePubkey: string,
// params: string[]
// ): Promise<string | undefined> {
// return this.body.handle(backend, id, remotePubkey, params).then((r) => {
// console.log(Date.now(), 'req', id, 'method', this.method, 'result', r)
// return r
// })
// }
// }
export class NoauthBackend { export class NoauthBackend {
readonly swg: ServiceWorkerGlobalScope readonly swg: ServiceWorkerGlobalScope
private keysModule: Keys private keysModule: Keys
@ -190,7 +235,7 @@ export class NoauthBackend {
private pendingNpubEvents = new Map<string, NDKEvent[]>() private pendingNpubEvents = new Map<string, NDKEvent[]>()
private ndk = new NDK({ private ndk = new NDK({
explicitRelayUrls: NIP46_RELAYS, explicitRelayUrls: NIP46_RELAYS,
enableOutboxModel: false enableOutboxModel: false,
}) })
public constructor(swg: ServiceWorkerGlobalScope) { public constructor(swg: ServiceWorkerGlobalScope) {
@ -610,7 +655,7 @@ export class NoauthBackend {
return this.keyInfo(dbKey) return this.keyInfo(dbKey)
} }
private getPerm(req: DbPending): string { private getDecision(req: DbPending): DECISION {
const reqPerm = getReqPerm(req) const reqPerm = getReqPerm(req)
const appPerms = this.perms.filter((p) => p.npub === req.npub && p.appNpub === req.appNpub) const appPerms = this.perms.filter((p) => p.npub === req.npub && p.appNpub === req.appNpub)
@ -619,8 +664,18 @@ export class NoauthBackend {
// non-exact next // non-exact next
if (!perm) perm = appPerms.find((p) => isPackagePerm(p.perm, reqPerm)) if (!perm) perm = appPerms.find((p) => isPackagePerm(p.perm, reqPerm))
console.log('req', req, 'perm', reqPerm, 'value', perm, appPerms) if (perm) {
return perm?.value || '' console.log('req', req, 'perm', reqPerm, 'value', perm, appPerms)
return perm.value === '1' ? DECISION.ALLOW : DECISION.DISALLOW
}
const conn = appPerms.find((p) => p.perm === 'connect')
if (conn && conn.value === '0') {
console.log('req', req, 'perm', reqPerm, 'ignore by connect disallow')
return DECISION.IGNORE
}
return DECISION.ASK
} }
private async connectApp({ private async connectApp({
@ -673,19 +728,19 @@ export class NoauthBackend {
method, method,
remotePubkey, remotePubkey,
params, params,
}: IAllowCallbackParams): Promise<boolean> { }: IAllowCallbackParams): Promise<DECISION> {
// same reqs usually come on reconnects // same reqs usually come on reconnects
if (this.doneReqIds.includes(id)) { if (this.doneReqIds.includes(id)) {
console.log('request already done', id) console.log('request already done', id)
// FIXME maybe repeat the reply, but without the Notification? // FIXME maybe repeat the reply, but without the Notification?
return false return DECISION.IGNORE
} }
const appNpub = nip19.npubEncode(remotePubkey) const appNpub = nip19.npubEncode(remotePubkey)
const connected = !!this.apps.find((a) => a.appNpub === appNpub) const connected = !!this.apps.find((a) => a.appNpub === appNpub)
if (!connected && method !== 'connect') { if (!connected && method !== 'connect') {
console.log('ignoring request before connect', method, id, appNpub, npub) console.log('ignoring request before connect', method, id, appNpub, npub)
return false return DECISION.IGNORE
} }
const req: DbPending = { const req: DbPending = {
@ -700,9 +755,21 @@ export class NoauthBackend {
const self = this const self = this
return new Promise(async (ok) => { return new Promise(async (ok) => {
// called when it's decided whether to allow this or not // called when it's decided whether to allow this or not
const onAllow = async (manual: boolean, allow: boolean, remember: boolean, options?: any) => { const onAllow = async (manual: boolean, decision: DECISION, remember: boolean, options?: any) => {
// confirm // confirm
console.log(Date.now(), allow ? 'allowed' : 'disallowed', npub, method, options, params) console.log(Date.now(), decision, npub, method, options, params)
switch (decision) {
case DECISION.ASK:
throw new Error('Make a decision!')
case DECISION.IGNORE:
return // noop
case DECISION.ALLOW:
case DECISION.DISALLOW:
// fall through
}
const allow = decision === DECISION.ALLOW
if (manual) { if (manual) {
await dbi.confirmPending(id, allow) await dbi.confirmPending(id, allow)
@ -755,35 +822,40 @@ export class NoauthBackend {
// reload // reload
this.perms = await dbi.listPerms() this.perms = await dbi.listPerms()
// confirm pending requests that might now have
// the proper perms
const otherReqs = self.confirmBuffer.filter((r) => r.req.appNpub === req.appNpub)
console.log('updated perms', this.perms, 'otherReqs', otherReqs, 'connected', connected)
for (const r of otherReqs) {
let perm = this.getPerm(r.req)
if (perm) {
r.cb(perm === '1', false)
}
}
} }
// release this promise to send reply
// to this req
ok(decision)
// notify UI that it was confirmed // notify UI that it was confirmed
// if (!PERF_TEST) // if (!PERF_TEST)
this.updateUI() this.updateUI()
// return to let nip46 flow proceed // after replying to this req check pending
ok(allow) // reqs maybe they can be replied right away
if (remember) {
// confirm pending requests that might now have
// the proper perms
const otherReqs = self.confirmBuffer.filter((r) => r.req.appNpub === req.appNpub)
console.log('updated perms', this.perms, 'otherReqs', otherReqs, 'connected', connected)
for (const r of otherReqs) {
const dec = this.getDecision(r.req)
if (dec !== DECISION.ASK) {
r.cb(dec, false)
}
}
}
} }
// check perms // check perms
const perm = this.getPerm(req) const dec = this.getDecision(req)
console.log(Date.now(), 'perm', req.id, perm) console.log(Date.now(), 'decision', req.id, dec)
// have perm? // have perm?
if (perm) { if (dec !== DECISION.ASK) {
// reply immediately // reply immediately
onAllow(false, perm === '1', false) onAllow(false, dec, false)
} else { } else {
// put pending req to db // put pending req to db
await dbi.addPending(req) await dbi.addPending(req)
@ -794,7 +866,7 @@ export class NoauthBackend {
// put to a list of pending requests // put to a list of pending requests
this.confirmBuffer.push({ this.confirmBuffer.push({
req, req,
cb: (allow, remember, options) => onAllow(true, allow, remember, options), cb: (decision, remember, options) => onAllow(true, decision, remember, options),
}) })
// OAuth flow // OAuth flow
@ -827,7 +899,7 @@ export class NoauthBackend {
ndk.connect() ndk.connect()
const signer = new NDKPrivateKeySigner(sk) // PrivateKeySigner const signer = new NDKPrivateKeySigner(sk) // PrivateKeySigner
const backend = new Nip46Backend(ndk, signer, () => Promise.resolve(true)) const backend = new Nip46Backend(ndk, signer, this.allowPermitCallback.bind(this)) // , () => Promise.resolve(true)
const watcher = new Watcher(ndk, signer, (id) => { const watcher = new Watcher(ndk, signer, (id) => {
// drop pending request // drop pending request
dbi.removePending(id).then(() => this.updateUI()) dbi.removePending(id).then(() => this.updateUI())
@ -835,18 +907,16 @@ export class NoauthBackend {
this.keys.push({ npub, backend, signer, ndk, backoff, watcher }) this.keys.push({ npub, backend, signer, ndk, backoff, watcher })
// new method // new method
backend.handlers['get_nip04_key'] = new Nip04KeyHandlingStrategy(sk) // backend.handlers['get_nip04_key'] = new Nip04KeyHandlingStrategy(sk)
// assign our own permission callback // // assign our own permission callback
for (const method in backend.handlers) { // for (const method in backend.handlers) {
backend.handlers[method] = new EventHandlingStrategyWrapper( // backend.handlers[method] = new EventHandlingStrategyWrapper(
backend, // backend,
npub, // method,
method, // backend.handlers[method]
backend.handlers[method], // )
this.allowPermitCallback.bind(this) // }
)
}
// start // start
backend.start() backend.start()
@ -907,11 +977,11 @@ export class NoauthBackend {
const events = await this.ndk.fetchEvents({ const events = await this.ndk.fetchEvents({
kinds: [KIND_RPC], kinds: [KIND_RPC],
"#p": [pubkey as string], '#p': [pubkey as string],
authors: [appPubkey as string] authors: [appPubkey as string],
}); })
console.log("fetched pending for", npub, events.size) console.log('fetched pending for', npub, events.size)
this.pendingNpubEvents.set(npub, [...events.values()]); this.pendingNpubEvents.set(npub, [...events.values()])
} }
public async unlock(npub: string) { public async unlock(npub: string) {
@ -1028,7 +1098,7 @@ export class NoauthBackend {
this.updateUI() this.updateUI()
} else { } else {
console.log('confirming req', id, allow, remember, options) console.log('confirming req', id, allow, remember, options)
req.cb(allow, remember, options) req.cb(allow ? DECISION.ALLOW : DECISION.DISALLOW, remember, options)
} }
} }

View File

@ -47,7 +47,7 @@ const KeyPage = () => {
const isKeyExists = npub.trim().length && key const isKeyExists = npub.trim().length && key
const isPopup = searchParams.get('popup') === 'true' const isPopup = searchParams.get('popup') === 'true'
console.log({ isKeyExists, isPopup }) // console.log({ isKeyExists, isPopup })
if (isPopup && !isKeyExists) { if (isPopup && !isKeyExists) {
searchParams.set('login', 'true') searchParams.set('login', 'true')