From 46336d817fe372ceaffbef3942bcb12ff660ec65 Mon Sep 17 00:00:00 2001 From: artur Date: Fri, 16 Feb 2024 14:46:36 +0300 Subject: [PATCH] Add ignore logic to stop interfering with replies from other instances --- src/App.tsx | 2 +- src/modules/backend.ts | 336 ++++++++++++++++++++------------- src/pages/KeyPage/Key.Page.tsx | 2 +- 3 files changed, 205 insertions(+), 135 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 8ad25ac..b382f04 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,7 +18,7 @@ function App() { const load = useCallback(async () => { const keys: DbKey[] = await dbi.listKeys() - console.log(keys, 'keys') + // console.log(keys, 'keys') dispatch(setKeys({ keys })) const loadProfiles = async () => { diff --git a/src/modules/backend.ts b/src/modules/backend.ts index fc709b8..e99dadf 100644 --- a/src/modules/backend.ts +++ b/src/modules/backend.ts @@ -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 { Keys } from './keys' import NDK, { - IEventHandlingStrategy, NDKEvent, NDKNip46Backend, NDKPrivateKeySigner, @@ -12,13 +11,20 @@ import NDK, { NDKUser, } from '@nostr-dev-kit/ndk' 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 { NostrPowEvent, minePow } from './pow' //import { PrivateKeySigner } from './signer' //const PERF_TEST = false +enum DECISION { + ASK = '', + ALLOW = 'allow', + DISALLOW = 'disallow', + IGNORE = 'ignore', +} + export interface KeyInfo { npub: string nip05?: string @@ -36,7 +42,7 @@ interface Key { interface Pending { req: DbPending - cb: (allow: boolean, remember: boolean, options?: any) => void + cb: (allow: DECISION, remember: boolean, options?: any) => void notified?: boolean } @@ -63,23 +69,25 @@ class Watcher { } async start() { - this.sub = this.ndk.subscribe({ - kinds: [KIND_RPC], - authors: [(await this.signer.user()).pubkey], - since: Math.floor((Date.now() / 1000) - 10), - }, { - closeOnEose: false, - cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY - }) + this.sub = this.ndk.subscribe( + { + kinds: [KIND_RPC], + authors: [(await this.signer.user()).pubkey], + since: Math.floor(Date.now() / 1000 - 10), + }, + { + closeOnEose: false, + cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY, + } + ) this.sub.on('event', async (e: NDKEvent) => { - const peer = e.tags.find(t => t.length >= 2 && t[0] === "p") - console.log("watcher got event", { e, peer }) + const peer = e.tags.find((t) => t.length >= 2 && t[0] === 'p') + console.log('watcher got event', { e, peer }) if (!peer) return - const decryptedContent = await this.signer.decrypt( - new NDKUser({ pubkey: peer[1] }), e.content); - const parsedContent = JSON.parse(decryptedContent); - const { id, method, params, result, error } = parsedContent; - console.log("watcher got", { peer, id, method, params, result, error }) + const decryptedContent = await this.signer.decrypt(new NDKUser({ pubkey: peer[1] }), e.content) + const parsedContent = JSON.parse(decryptedContent) + const { id, method, params, result, error } = parsedContent + console.log('watcher got', { peer, id, method, params, result, error }) if (method || result === 'auth_url') return this.onReply(id) }) @@ -91,91 +99,128 @@ class Watcher { } class Nip46Backend extends NDKNip46Backend { + private allowCb: (params: IAllowCallbackParams) => Promise + private npub: string = '' + + public constructor(ndk: NDK, signer: NDKSigner, allowCb: (params: IAllowCallbackParams) => Promise) { + super(ndk, signer, () => Promise.resolve(true)) + this.allowCb = allowCb + signer.user().then((u) => (this.npub = nip19.npubEncode(u.pubkey))) + } + public async processEvent(event: NDKEvent) { this.handleIncomingEvent(event) } -} -class Nip04KeyHandlingStrategy implements IEventHandlingStrategy { - private privkey: string - private nip04 = new Nip04() + protected async handleIncomingEvent(event: NDKEvent) { + const { id, method, params } = (await this.rpc.parseEvent(event)) as any + const remotePubkey = event.pubkey + let response: string | undefined - constructor(privkey: string) { - this.privkey = privkey - } + this.debug('incoming event', { id, method, params }) - 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 + // validate signature explicitly + if (!verifySignature(event.rawEvent() as Event)) { + this.debug('invalid signature', event.rawEvent()) + return } - 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) - } -} - -class EventHandlingStrategyWrapper implements IEventHandlingStrategy { - readonly backend: NDKNip46Backend - readonly npub: string - readonly method: string - private body: IEventHandlingStrategy - private allowCb: (params: IAllowCallbackParams) => Promise - - constructor( - backend: NDKNip46Backend, - npub: string, - method: string, - body: IEventHandlingStrategy, - allowCb: (params: IAllowCallbackParams) => Promise - ) { - 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 { - console.log(Date.now(), 'handle', { - method: this.method, - id, - remotePubkey, - params, - }) - const allow = await this.allowCb({ - backend: this.backend, + const decision = await this.allowCb({ + backend: this, npub: this.npub, id, - method: this.method, + method, remotePubkey, params, }) - if (!allow) return undefined - return this.body.handle(backend, id, remotePubkey, params).then((r) => { - console.log(Date.now(), 'req', id, 'method', this.method, 'result', r) - return r - }) + console.log(Date.now(), 'handle', { method, id, decision, remotePubkey, params }) + if (decision === DECISION.IGNORE) return + + 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 { +// 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 { readonly swg: ServiceWorkerGlobalScope private keysModule: Keys @@ -190,7 +235,7 @@ export class NoauthBackend { private pendingNpubEvents = new Map() private ndk = new NDK({ explicitRelayUrls: NIP46_RELAYS, - enableOutboxModel: false + enableOutboxModel: false, }) public constructor(swg: ServiceWorkerGlobalScope) { @@ -610,7 +655,7 @@ export class NoauthBackend { return this.keyInfo(dbKey) } - private getPerm(req: DbPending): string { + private getDecision(req: DbPending): DECISION { const reqPerm = getReqPerm(req) const appPerms = this.perms.filter((p) => p.npub === req.npub && p.appNpub === req.appNpub) @@ -619,8 +664,18 @@ export class NoauthBackend { // non-exact next if (!perm) perm = appPerms.find((p) => isPackagePerm(p.perm, reqPerm)) - console.log('req', req, 'perm', reqPerm, 'value', perm, appPerms) - return perm?.value || '' + if (perm) { + 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({ @@ -673,19 +728,19 @@ export class NoauthBackend { method, remotePubkey, params, - }: IAllowCallbackParams): Promise { + }: IAllowCallbackParams): Promise { // same reqs usually come on reconnects if (this.doneReqIds.includes(id)) { console.log('request already done', id) // FIXME maybe repeat the reply, but without the Notification? - return false + return DECISION.IGNORE } const appNpub = nip19.npubEncode(remotePubkey) const connected = !!this.apps.find((a) => a.appNpub === appNpub) if (!connected && method !== 'connect') { console.log('ignoring request before connect', method, id, appNpub, npub) - return false + return DECISION.IGNORE } const req: DbPending = { @@ -700,9 +755,21 @@ export class NoauthBackend { const self = this return new Promise(async (ok) => { // 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 - 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) { await dbi.confirmPending(id, allow) @@ -755,35 +822,40 @@ export class NoauthBackend { // reload 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 // if (!PERF_TEST) this.updateUI() - // return to let nip46 flow proceed - ok(allow) + // after replying to this req check pending + // 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 - const perm = this.getPerm(req) - console.log(Date.now(), 'perm', req.id, perm) + const dec = this.getDecision(req) + console.log(Date.now(), 'decision', req.id, dec) // have perm? - if (perm) { + if (dec !== DECISION.ASK) { // reply immediately - onAllow(false, perm === '1', false) + onAllow(false, dec, false) } else { // put pending req to db await dbi.addPending(req) @@ -794,7 +866,7 @@ export class NoauthBackend { // put to a list of pending requests this.confirmBuffer.push({ req, - cb: (allow, remember, options) => onAllow(true, allow, remember, options), + cb: (decision, remember, options) => onAllow(true, decision, remember, options), }) // OAuth flow @@ -827,7 +899,7 @@ export class NoauthBackend { ndk.connect() 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) => { // drop pending request dbi.removePending(id).then(() => this.updateUI()) @@ -835,18 +907,16 @@ export class NoauthBackend { this.keys.push({ npub, backend, signer, ndk, backoff, watcher }) // new method - backend.handlers['get_nip04_key'] = new Nip04KeyHandlingStrategy(sk) + // backend.handlers['get_nip04_key'] = new Nip04KeyHandlingStrategy(sk) - // assign our own permission callback - for (const method in backend.handlers) { - backend.handlers[method] = new EventHandlingStrategyWrapper( - backend, - npub, - method, - backend.handlers[method], - this.allowPermitCallback.bind(this) - ) - } + // // assign our own permission callback + // for (const method in backend.handlers) { + // backend.handlers[method] = new EventHandlingStrategyWrapper( + // backend, + // method, + // backend.handlers[method] + // ) + // } // start backend.start() @@ -907,11 +977,11 @@ export class NoauthBackend { const events = await this.ndk.fetchEvents({ kinds: [KIND_RPC], - "#p": [pubkey as string], - authors: [appPubkey as string] - }); - console.log("fetched pending for", npub, events.size) - this.pendingNpubEvents.set(npub, [...events.values()]); + '#p': [pubkey as string], + authors: [appPubkey as string], + }) + console.log('fetched pending for', npub, events.size) + this.pendingNpubEvents.set(npub, [...events.values()]) } public async unlock(npub: string) { @@ -1028,7 +1098,7 @@ export class NoauthBackend { this.updateUI() } else { console.log('confirming req', id, allow, remember, options) - req.cb(allow, remember, options) + req.cb(allow ? DECISION.ALLOW : DECISION.DISALLOW, remember, options) } } diff --git a/src/pages/KeyPage/Key.Page.tsx b/src/pages/KeyPage/Key.Page.tsx index 59a6053..9761f93 100644 --- a/src/pages/KeyPage/Key.Page.tsx +++ b/src/pages/KeyPage/Key.Page.tsx @@ -47,7 +47,7 @@ const KeyPage = () => { const isKeyExists = npub.trim().length && key const isPopup = searchParams.get('popup') === 'true' - console.log({ isKeyExists, isPopup }) + // console.log({ isKeyExists, isPopup }) if (isPopup && !isKeyExists) { searchParams.set('login', 'true')