From 6f16cafd4193f50db865a23ea4245eabf1a89233 Mon Sep 17 00:00:00 2001 From: pablof7z Date: Thu, 6 Jul 2023 22:29:31 +0200 Subject: [PATCH] =?UTF-8?q?lots=20of=20things=20that=20I=20forgot=20to=20c?= =?UTF-8?q?ommit=20=F0=9F=98=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 + package-lock.json | 122 ++++++++----- package.json | 10 +- .../20230703092203_revokedat/migration.sql | 2 + prisma/schema.prisma | 1 + src/client.ts | 127 +++++++++++--- src/commands/start.ts | 2 +- src/daemon/admin/commands/create_new_key.ts | 1 - src/daemon/admin/commands/revoke_user.ts | 24 +++ src/daemon/admin/index.ts | 163 ++++++++++++------ .../admin/validations/request-from-admin.ts | 18 ++ src/daemon/run.ts | 28 ++- src/utils/dm-user.ts | 19 ++ 13 files changed, 393 insertions(+), 128 deletions(-) create mode 100644 prisma/migrations/20230703092203_revokedat/migration.sql create mode 100644 src/daemon/admin/commands/revoke_user.ts create mode 100644 src/daemon/admin/validations/request-from-admin.ts create mode 100644 src/utils/dm-user.ts diff --git a/README.md b/README.md index 3e7094e..6e9ff9b 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,11 @@ $ npm run nsecbunkerd start ## Testing with `nsecbunker-client` +nsecbunker ships with a simple client that can request signatures from an nsecbunkerd: +``` +nsecbunker-client sign "hi, I'm signing from the command line with my nsecbunkerd!" +``` # Authors diff --git a/package-lock.json b/package-lock.json index 83ffefe..b563c4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,27 +1,27 @@ { "name": "nsecbunkerd", - "version": "0.6.1", + "version": "0.6.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "nsecbunkerd", - "version": "0.6.1", + "version": "0.6.5", "license": "CC BY-NC-ND 4.0", "dependencies": { "@inquirer/password": "^1.1.2", "@inquirer/prompts": "^1.2.3", - "@nostr-dev-kit/ndk": "^0.6.0", - "@prisma/client": "^4.16.1", + "@nostr-dev-kit/ndk": "^0.6.5", + "@prisma/client": "^4.16.2", "@scure/base": "^1.1.1", "@types/yargs": "^17.0.24", - "@typescript-eslint/eslint-plugin": "^5.60.0", - "@typescript-eslint/parser": "^5.60.0", "dotenv": "^16.3.1", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.27.5", "eventemitter3": "^5.0.1", + "isomorphic-ws": "^5.0.0", "websocket-polyfill": "^0.0.3", + "ws": "^8.13.0", "yargs": "^17.7.2" }, "bin": { @@ -31,7 +31,7 @@ "devDependencies": { "@types/debug": "^4.1.8", "@types/node": "^18.16.18", - "prisma": "^4.16.1", + "prisma": "^4.16.2", "ts-node": "^10.9.1", "typescript": "^5.1.3" } @@ -961,9 +961,9 @@ } }, "node_modules/@nostr-dev-kit/ndk": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-0.6.0.tgz", - "integrity": "sha512-0ptE6OIZhFW+aRRIXAI8PvUIoVU6iQLpiwFtJj48XAUO2EC3WiSuqKLshjg6wj1bbo9qGs1PyFS9AUhUlWWJtg==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-0.6.5.tgz", + "integrity": "sha512-D95sDEonyFJhdTM1YswPOAYKaScnOlvZcxoyt9SlXnibT+gC9AGLtwAmpziK9zPC2h5Hh2GX44BnYXUGolLjZw==", "dependencies": { "@noble/hashes": "^1.3.1", "@noble/secp256k1": "^2.0.0", @@ -987,12 +987,12 @@ } }, "node_modules/@prisma/client": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.16.1.tgz", - "integrity": "sha512-CoDHu7Bt+NuDo40ijoeHP79EHtECsPBTy3yte5Yo3op8TqXt/kV0OT5OrsWewKvQGKFMHhYQ+ePed3zzjYdGAw==", + "version": "4.16.2", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.16.2.tgz", + "integrity": "sha512-qCoEyxv1ZrQ4bKy39GnylE8Zq31IRmm8bNhNbZx7bF2cU5aiCCnSa93J2imF88MBjn7J9eUQneNxUQVJdl/rPQ==", "hasInstallScript": true, "dependencies": { - "@prisma/engines-version": "4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c" + "@prisma/engines-version": "4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81" }, "engines": { "node": ">=14.17" @@ -1007,16 +1007,16 @@ } }, "node_modules/@prisma/engines": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.16.1.tgz", - "integrity": "sha512-gpZG0kGGxfemgvK/LghHdBIz+crHkZjzszja94xp4oytpsXrgt/Ice82MvPsWMleVIniKuARrowtsIsim0PFJQ==", + "version": "4.16.2", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.16.2.tgz", + "integrity": "sha512-vx1nxVvN4QeT/cepQce68deh/Turxy5Mr+4L4zClFuK1GlxN3+ivxfuv+ej/gvidWn1cE1uAhW7ALLNlYbRUAw==", "devOptional": true, "hasInstallScript": true }, "node_modules/@prisma/engines-version": { - "version": "4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c.tgz", - "integrity": "sha512-tMWAF/qF00fbUH1HB4Yjmz6bjh7fzkb7Y3NRoUfMlHu6V+O45MGvqwYxqwBjn1BIUXkl3r04W351D4qdJjrgvA==" + "version": "4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81.tgz", + "integrity": "sha512-q617EUWfRIDTriWADZ4YiWRZXCa/WuhNgLTVd+HqWLffjMSPzyM5uOWoauX91wvQClSKZU4pzI4JJLQ9Kl62Qg==" }, "node_modules/@scure/base": { "version": "1.1.1", @@ -3159,6 +3159,14 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/jest-diff": { "version": "29.5.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", @@ -4006,13 +4014,13 @@ } }, "node_modules/prisma": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.16.1.tgz", - "integrity": "sha512-C2Xm7yxHxjFjjscBEW4tmoraPHH/Vyu/A0XABdbaFtoiOZARsxvOM7rwc2iZ0qVxbh0bGBGBWZUSXO/52/nHBQ==", + "version": "4.16.2", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.16.2.tgz", + "integrity": "sha512-SYCsBvDf0/7XSJyf2cHTLjLeTLVXYfqp7pG5eEVafFLeT0u/hLFz/9W196nDRGUOo1JfPatAEb+uEnTQImQC1g==", "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/engines": "4.16.1" + "@prisma/engines": "4.16.2" }, "bin": { "prisma": "build/index.js", @@ -4937,6 +4945,26 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -5550,9 +5578,9 @@ } }, "@nostr-dev-kit/ndk": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-0.6.0.tgz", - "integrity": "sha512-0ptE6OIZhFW+aRRIXAI8PvUIoVU6iQLpiwFtJj48XAUO2EC3WiSuqKLshjg6wj1bbo9qGs1PyFS9AUhUlWWJtg==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-0.6.5.tgz", + "integrity": "sha512-D95sDEonyFJhdTM1YswPOAYKaScnOlvZcxoyt9SlXnibT+gC9AGLtwAmpziK9zPC2h5Hh2GX44BnYXUGolLjZw==", "requires": { "@noble/hashes": "^1.3.1", "@noble/secp256k1": "^2.0.0", @@ -5576,23 +5604,23 @@ } }, "@prisma/client": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.16.1.tgz", - "integrity": "sha512-CoDHu7Bt+NuDo40ijoeHP79EHtECsPBTy3yte5Yo3op8TqXt/kV0OT5OrsWewKvQGKFMHhYQ+ePed3zzjYdGAw==", + "version": "4.16.2", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.16.2.tgz", + "integrity": "sha512-qCoEyxv1ZrQ4bKy39GnylE8Zq31IRmm8bNhNbZx7bF2cU5aiCCnSa93J2imF88MBjn7J9eUQneNxUQVJdl/rPQ==", "requires": { - "@prisma/engines-version": "4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c" + "@prisma/engines-version": "4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81" } }, "@prisma/engines": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.16.1.tgz", - "integrity": "sha512-gpZG0kGGxfemgvK/LghHdBIz+crHkZjzszja94xp4oytpsXrgt/Ice82MvPsWMleVIniKuARrowtsIsim0PFJQ==", + "version": "4.16.2", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.16.2.tgz", + "integrity": "sha512-vx1nxVvN4QeT/cepQce68deh/Turxy5Mr+4L4zClFuK1GlxN3+ivxfuv+ej/gvidWn1cE1uAhW7ALLNlYbRUAw==", "devOptional": true }, "@prisma/engines-version": { - "version": "4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c.tgz", - "integrity": "sha512-tMWAF/qF00fbUH1HB4Yjmz6bjh7fzkb7Y3NRoUfMlHu6V+O45MGvqwYxqwBjn1BIUXkl3r04W351D4qdJjrgvA==" + "version": "4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81.tgz", + "integrity": "sha512-q617EUWfRIDTriWADZ4YiWRZXCa/WuhNgLTVd+HqWLffjMSPzyM5uOWoauX91wvQClSKZU4pzI4JJLQ9Kl62Qg==" }, "@scure/base": { "version": "1.1.1", @@ -7121,6 +7149,12 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "requires": {} + }, "jest-diff": { "version": "29.5.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", @@ -7722,12 +7756,12 @@ } }, "prisma": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.16.1.tgz", - "integrity": "sha512-C2Xm7yxHxjFjjscBEW4tmoraPHH/Vyu/A0XABdbaFtoiOZARsxvOM7rwc2iZ0qVxbh0bGBGBWZUSXO/52/nHBQ==", + "version": "4.16.2", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.16.2.tgz", + "integrity": "sha512-SYCsBvDf0/7XSJyf2cHTLjLeTLVXYfqp7pG5eEVafFLeT0u/hLFz/9W196nDRGUOo1JfPatAEb+uEnTQImQC1g==", "devOptional": true, "requires": { - "@prisma/engines": "4.16.1" + "@prisma/engines": "4.16.2" } }, "punycode": { @@ -8369,6 +8403,12 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "requires": {} + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 5ff5020..00e3664 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nsecbunkerd", - "version": "0.6.4", + "version": "0.7.1", "description": "nsecbunker daemon", "main": "dist/index.js", "bin": { @@ -35,21 +35,23 @@ "dependencies": { "@inquirer/password": "^1.1.2", "@inquirer/prompts": "^1.2.3", - "@nostr-dev-kit/ndk": "^0.6.5", - "@prisma/client": "^4.16.1", + "@nostr-dev-kit/ndk": "^0.7.4", + "@prisma/client": "^4.16.2", "@scure/base": "^1.1.1", "@types/yargs": "^17.0.24", "dotenv": "^16.3.1", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.27.5", "eventemitter3": "^5.0.1", + "isomorphic-ws": "^5.0.0", "websocket-polyfill": "^0.0.3", + "ws": "^8.13.0", "yargs": "^17.7.2" }, "devDependencies": { "@types/debug": "^4.1.8", "@types/node": "^18.16.18", - "prisma": "^4.16.1", + "prisma": "^4.16.2", "ts-node": "^10.9.1", "typescript": "^5.1.3" } diff --git a/prisma/migrations/20230703092203_revokedat/migration.sql b/prisma/migrations/20230703092203_revokedat/migration.sql new file mode 100644 index 0000000..d25002a --- /dev/null +++ b/prisma/migrations/20230703092203_revokedat/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "KeyUser" ADD COLUMN "revokedAt" DATETIME; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 817166b..91e0564 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -13,6 +13,7 @@ model KeyUser { userPubkey String createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt + revokedAt DateTime? lastUsedAt DateTime? description String? logs Log[] diff --git a/src/client.ts b/src/client.ts index e38144f..714caa4 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,52 +1,129 @@ import NDK, { NDKUser, NDKEvent, NDKPrivateKeySigner, NDKNip46Signer, NostrEvent } from '@nostr-dev-kit/ndk'; +import fs from 'fs'; -const remotePubkey = process.argv[2]; -const content = process.argv[3]; +const command = process.argv[2]; +const remotePubkey = process.argv[3]; +const content = process.argv[4]; +const dontPublish = process.argv.includes('--dont-publish'); +const debug = process.argv.includes('--debug'); -if (!content) { - console.log('Usage: node src/client.js '); +if (!command) { + console.log('Usage: node src/client.js [--dont-publish] [--debug] [--pk ]'); console.log(''); + console.log(`\t: command to run (ping, sign)`); console.log(`\t: npub that should be published as`); - console.log(`\t: event JSON to sign | or kind:1 content string to sign`); + console.log(`\t: event JSON to sign (no need for pubkey or id fields) | or kind:1 content string to sign`); + console.log('\t--dont-publish: do not publish the event to the relay'); + console.log('\t--debug: enable debug mode'); process.exit(1); } async function createNDK(): Promise { const ndk = new NDK({ - explicitRelayUrls: ['wss://nostr.vulpem.com', "wss://67aee52897df.ngrok.app"], + explicitRelayUrls: ['wss://nostr.vulpem.com', "wss://relay.nsecbunker.com"], }); - ndk.pool.on('relay:connect', () => console.log('✅ connected')); - ndk.pool.on('relay:disconnect', () => console.log('❌ disconnected')); + if (debug) { + ndk.pool.on('connect', () => console.log('✅ connected')); + ndk.pool.on('disconnect', () => console.log('❌ disconnected')); + } await ndk.connect(5000); return ndk; } +// switch (command) { +// case 'ping': +// ping(remotePubkey); + +function getPrivateKeyPath() { + const home = process.env.HOME || process.env.USERPROFILE; + return `${home}/.nsecbunker-client-private.key`; +} + +function savePrivateKey(pk: string) { + const path = getPrivateKeyPath(); + if (!fs.existsSync(path)) { + fs.mkdirSync(path); + } + fs.writeFileSync(`${path}/private.key`, pk); +} + +function loadPrivateKey(): string | undefined { + const path = getPrivateKeyPath(); + if (!fs.existsSync(path)) { + return undefined; + } + return fs.readFileSync(`${path}/private.key`).toString(); +} + + (async () => { const remoteUser = new NDKUser({npub: remotePubkey}); const ndk = await createNDK(); - const localSigner = NDKPrivateKeySigner.generate(); - // const localSigner = new NDKPrivateKeySigner('b8baad35c387d7cf84d52e0958d9a02aff214393a85b0703de4146c7a3697bb3'); + + const pk = loadPrivateKey(); + let localSigner: NDKPrivateKeySigner; + + if (pk) { + localSigner = new NDKPrivateKeySigner(pk); + } else { + localSigner = NDKPrivateKeySigner.generate(); + savePrivateKey(localSigner.privateKey!); + } + const signer = new NDKNip46Signer(ndk, remoteUser.hexpubkey(), localSigner); - console.log(`local pubkey`, (await signer.user()).npub); - console.log(`remote pubkey`, remotePubkey); + if (debug) console.log(`local pubkey`, (await localSigner.user()).npub); + if (debug) console.log(`remote pubkey`, remotePubkey); ndk.signer = signer; setTimeout(async () => { - await signer.blockUntilReady(); - console.log(`authorized to sign as`, remotePubkey); + try { + if (debug) console.log(`waiting for authorization (check your nsecBunker)...`); + await signer.blockUntilReady(); + } catch(e) { + console.log('error:', e); + process.exit(1); + } + if (debug) console.log(`authorized to sign as`, remotePubkey); - const event = new NDKEvent(ndk, { - pubkey: remoteUser.hexpubkey(), - kind: 1, - content, - tags: [ - ['client', 'nsecbunker-client'] - ], - } as NostrEvent); + let event; - await event.sign(); - console.log('resulting event', JSON.stringify(await event.toNostrEvent())); - await event.publish(); + try { + const json = JSON.parse(content); + event = new NDKEvent(ndk, json); + if (!event.tags) { event.tags = []; } + if (!event.content) { event.content = ""; } + if (!event.kind) { throw "No kind on the event to sign!"; } + } catch (e) { + event = new NDKEvent(ndk, { + kind: 1, + content, + tags: [ + ['client', 'nsecbunker-client'] + ], + } as NostrEvent); + } + + event.pubkey = remoteUser.hexpubkey(); + + try { + await event.sign(); + if (debug) { + console.log({ + event: event.rawEvent(), + signature: event.sig, + }); + } else { + console.log(event.sig); + } + + if (!dontPublish) { + await event.publish(); + } + + process.exit(0); + } catch(e) { + console.log('sign error', e); + } }, 2000); })(); \ No newline at end of file diff --git a/src/commands/start.ts b/src/commands/start.ts index 92e1945..5a0e2c0 100644 --- a/src/commands/start.ts +++ b/src/commands/start.ts @@ -18,7 +18,7 @@ interface IOpts { export async function start(opts: IOpts) { const configData = await getCurrentConfig(opts.config); - if (opts.adminNpubs) { + if (opts.adminNpubs && opts.adminNpubs.length > 0) { configData.admin.npubs = opts.adminNpubs; } diff --git a/src/daemon/admin/commands/create_new_key.ts b/src/daemon/admin/commands/create_new_key.ts index a7d74e7..640034d 100644 --- a/src/daemon/admin/commands/create_new_key.ts +++ b/src/daemon/admin/commands/create_new_key.ts @@ -93,7 +93,6 @@ async function setupSkeletonProfile(key: NDKPrivateKeySigner) { ['r', 'wss://relay.f7z.io'], ['r', 'wss://relay.snort.social'], ['r', 'wss://relay.damus.io'], - ['r', 'wss://relay.damus.io'], ], pubkey: user.hexpubkey(), } as NostrEvent); diff --git a/src/daemon/admin/commands/revoke_user.ts b/src/daemon/admin/commands/revoke_user.ts new file mode 100644 index 0000000..409015e --- /dev/null +++ b/src/daemon/admin/commands/revoke_user.ts @@ -0,0 +1,24 @@ +import { NDKRpcRequest } from "@nostr-dev-kit/ndk"; +import AdminInterface from "../index.js"; +import prisma from "../../../db.js"; + +export default async function revokeUser(admin: AdminInterface, req: NDKRpcRequest) { + const [ keyUserId ] = req.params as [ string ]; + + if (!keyUserId) throw new Error("Invalid params"); + + const keyUserIdInt = parseInt(keyUserId); + if (isNaN(keyUserIdInt)) throw new Error("Invalid params"); + + await prisma.keyUser.update({ + where: { + id: keyUserIdInt, + }, + data: { + revokedAt: new Date(), + } + }); + + const result = JSON.stringify(["ok"]); + return admin.rpc.sendResponse(req.id, req.pubkey, result, 24134); +} diff --git a/src/daemon/admin/index.ts b/src/daemon/admin/index.ts index eaa7440..892d0c1 100644 --- a/src/daemon/admin/index.ts +++ b/src/daemon/admin/index.ts @@ -1,4 +1,4 @@ -import NDK, { NDKPrivateKeySigner, NDKRpcRequest, NDKRpcResponse, NDKUser } from '@nostr-dev-kit/ndk'; +import NDK, { NDKPrivateKeySigner, NDKRpcRequest, NDKRpcResponse, NDKUser, NostrEvent } from '@nostr-dev-kit/ndk'; import { NDKNostrRpc } from '@nostr-dev-kit/ndk'; import { debug } from 'debug'; import { Key, KeyUser } from '../run'; @@ -8,7 +8,10 @@ import createNewKey from './commands/create_new_key'; import createNewPolicy from './commands/create_new_policy'; import createNewToken from './commands/create_new_token'; import unlockKey from './commands/unlock_key'; +import revokeUser from './commands/revoke_user'; import fs from 'fs'; +import { validateRequestFromAdmin } from './validations/request-from-admin'; +import { dmUser } from '../../utils/dm-user'; export type IAdminOpts = { npubs: string[]; @@ -43,7 +46,7 @@ class AdminInterface { let connectionString = `bunker://${user.npub}`; if (opts.adminRelays.length > 0) { - connectionString += `@${opts.adminRelays.join(',').replace(/wss:\/\//g, '')}`; + connectionString += '@' + encodeURIComponent(`${opts.adminRelays.join(',').replace(/wss:\/\//g, '')}`); } console.log(`\n\nnsecBunker connection string:\n\n${connectionString}\n\n`); @@ -54,11 +57,25 @@ class AdminInterface { this.signerUser = user; this.connect(); + + this.notifyAdminsOfNewConnection(connectionString); }); this.rpc = new NDKNostrRpc(this.ndk, this.ndk.signer!, debug("ndk:rpc")); } + private async notifyAdminsOfNewConnection(connectionString: string) { + const blastrNdk = new NDK({ + explicitRelayUrls: ['wss://blastr.f7z.xyz', 'wss://nostr.mutinywallet.com'], + signer: this.ndk.signer + }); + await blastrNdk.connect(2500); + + for (const npub of this.npubs) { + dmUser(blastrNdk, npub, `nsecBunker has started; use ${connectionString} to connect to it and unlock your key(s)`); + } + } + /** * Get the npub of the admin interface. */ @@ -75,10 +92,10 @@ class AdminInterface { this.ndk.pool.on('relay:connect', () => console.log('✅ nsecBunker Admin Interface ready')); this.ndk.pool.on('relay:disconnect', () => console.log('❌ admin disconnected')); this.ndk.connect(2500).then(() => { + // connect for whitelisted admins this.rpc.subscribe({ "kinds": [24134 as number], // 24134 "#p": [this.signerUser!.hexpubkey()], - "authors": this.npubs.map((npub) => (new NDKUser({npub}).hexpubkey())), }); this.rpc.on('request', (req) => this.handleRequest(req)); @@ -89,30 +106,38 @@ class AdminInterface { } private async handleRequest(req: NDKRpcRequest) { - // await this.validateRequest(req); - try { + await this.validateRequest(req); + switch (req.method) { - case 'get_keys': this.reqGetKeys(req); break; - case 'get_key_users': this.reqGetKeyUsers(req); break; - case 'get_key_tokens': this.reqGetKeyTokens(req); break; - case 'create_new_key': createNewKey(this, req); break; - case 'unlock_key': unlockKey(this, req); break; - case 'create_new_policy': createNewPolicy(this, req); break; - case 'get_policies': this.reqListPolicies(req); break; - - case 'create_new_token': createNewToken(this, req); break; - + case 'get_keys': await this.reqGetKeys(req); break; + case 'get_key_users': await this.reqGetKeyUsers(req); break; + case 'get_key_tokens': await this.reqGetKeyTokens(req); break; + case 'revoke_user': await revokeUser(this, req); break; + case 'create_new_key': await createNewKey(this, req); break; + case 'unlock_key': await unlockKey(this, req); break; + case 'create_new_policy': await createNewPolicy(this, req); break; + case 'get_policies': await this.reqListPolicies(req); break; + case 'create_new_token': await createNewToken(this, req); break; default: console.log(`Unknown method ${req.method}`); + return this.rpc.sendResponse( + req.id, + req.pubkey, + JSON.stringify(['error', `Unknown method ${req.method}`]), + 24134 + ); } } catch (err: any) { console.error(`Error handling request ${req.method}: ${err.message}`, req.params); + return this.rpc.sendResponse(req.id, req.pubkey, JSON.stringify(['error', err?.message]), 24134); } } - private async validateRequest(req: NDKRpcRequest) { - // TODO validate pubkey, validate signature + private async validateRequest(req: NDKRpcRequest): Promise { + if (!await validateRequestFromAdmin(req, this.npubs)) { + throw new Error('You are not designated to administrate this bunker'); + } } /** @@ -220,13 +245,15 @@ class AdminInterface { /** * This function is called when a request is received from a remote user that needs * to be approved by the admin interface. + * + * @returns true if the request is approved, false if it is denied, undefined if it timedout */ public async requestPermission( keyName: string, remotePubkey: string, method: string, param: any - ): Promise { + ): Promise { const keyUser = await prisma.keyUser.findUnique({ where: { unique_key_user: { @@ -254,53 +281,81 @@ class AdminInterface { console.log(`param`, param); console.log(`keyUser`, keyUser); + /** + * If an admin doesn't respond within 10 seconds, report back to the user that the request timed out + */ + setTimeout(() => { + resolve(undefined); + }, 10000); + for (const npub of this.npubs) { const remoteUser = new NDKUser({npub}); console.log(`sending request to ${npub}`, remoteUser.hexpubkey()); + const params = JSON.stringify({ + keyName, + remotePubkey, + method, + param, + description: keyUser?.description, + }); + this.rpc.sendRequest( remoteUser.hexpubkey(), 'acl', - [JSON.stringify({ - keyName, - remotePubkey, - method, - param, - description: keyUser?.description, - })], - 24134, // 24134 + [params], + 24134, (res: NDKRpcResponse) => { - let resObj; - try { - resObj = JSON.parse(res.result); - } catch (e) { - console.log('error parsing result', e); - return; - } - - console.log('request result', resObj); - - switch (resObj[0]) { - case 'always': { - allowAllRequestsFromKey( - remotePubkey, - keyName, - method, - param, - resObj[1], - resObj[2] - ).then(() => { - resolve(true); - }); - break; - } - default: - console.log('request result', res.result); - } + this.requestPermissionResponse( + remotePubkey, + keyName, + method, + param, + resolve, + res + ); } ); } }); } + + public async requestPermissionResponse( + remotePubkey: string, + keyName: string, + method: string, + param: string, + resolve: (value: boolean) => void, + res: NDKRpcResponse + ) { + let resObj; + try { + resObj = JSON.parse(res.result); + } catch (e) { + console.log('error parsing result', e); + return; + } + + switch (resObj[0]) { + case 'always': { + allowAllRequestsFromKey( + remotePubkey, + keyName, + method, + param, + resObj[1], + resObj[2] + ); + resolve(true); + break; + } + case 'never': { + console.log('not implemented'); + break; + } + default: + console.log('request result', res.result); + } + } } -export default AdminInterface; \ No newline at end of file +export default AdminInterface; diff --git a/src/daemon/admin/validations/request-from-admin.ts b/src/daemon/admin/validations/request-from-admin.ts new file mode 100644 index 0000000..38ccd04 --- /dev/null +++ b/src/daemon/admin/validations/request-from-admin.ts @@ -0,0 +1,18 @@ +import { NDKRpcRequest } from "@nostr-dev-kit/ndk"; +import { nip19 } from "nostr-tools"; + +export async function validateRequestFromAdmin( + req: NDKRpcRequest, + npubs: string[], +): Promise { + const hexpubkey = req.pubkey; + + if (!hexpubkey) { + console.log('missing pubkey'); + return false; + } + + const hexpubkeys = npubs.map((npub) => nip19.decode(npub).data as string); + + return hexpubkeys.includes(hexpubkey); +} \ No newline at end of file diff --git a/src/daemon/run.ts b/src/daemon/run.ts index d58be66..9b14f7d 100644 --- a/src/daemon/run.ts +++ b/src/daemon/run.ts @@ -70,11 +70,13 @@ function getKeyUsers(config: IConfig) { for (const user of users) { const keyUser = { + id: user.id, name: user.keyName, pubkey: user.userPubkey, description: user.description || undefined, createdAt: user.createdAt, lastUsedAt: user.lastUsedAt || undefined, + revokedAt: user.revokedAt || undefined, signingConditions: user.signingConditions, // Include signing conditions }; @@ -152,7 +154,13 @@ function callbackForKeyAdminInterface(keyName: string, adminInterface: AdminInte return keyAllowed; } - return adminInterface.requestPermission(keyName, remotePubkey, method, param); + const requestedPerm = await adminInterface.requestPermission(keyName, remotePubkey, method, param); + + if (requestedPerm === undefined) { + throw new Error('adminInterface.requestPermission returned undefined'); + } + + return requestedPerm; } catch(e) { console.log('callbackForKey error:', e); } @@ -205,8 +213,24 @@ class Daemon { this.ndk = new NDK({ explicitRelayUrls: config.nostr.relays, }); - this.ndk.pool.on('connect', (r) => { console.log(`✅ Connected to ${r.url}`); }); + this.ndk.pool.on('relay:connect', (r) => { + if (r) { + console.log(`✅ Connected to ${r.url}`); + } else { + console.log('✅ Connected to relays', this.ndk.pool.urls); + } + }); this.ndk.pool.on('notice', (n, r) => { console.log(`👀 Notice from ${r.url}`, n); }); + + this.ndk.pool.on('relay:disconnect', (r) => { + console.log(`🚫 Disconnected from ${r.url}`); + }); + + setInterval(() => { + const stats = this.ndk.pool.stats(); + + console.log(`📡 ${stats.connected} connected, ${stats.disconnected} disconnected, ${stats.connecting} connecting`); + }, 10000); } async start() { diff --git a/src/utils/dm-user.ts b/src/utils/dm-user.ts new file mode 100644 index 0000000..3ed1fe3 --- /dev/null +++ b/src/utils/dm-user.ts @@ -0,0 +1,19 @@ +import NDK, { NDKUser, NDKEvent, NostrEvent } from "@nostr-dev-kit/ndk"; + +export async function dmUser(ndk: NDK, recipient: NDKUser | string, content: string): Promise { + let targetUser; + + if (typeof recipient === 'string') { + targetUser = new NDKUser({ npub: recipient }); + } else if (recipient instanceof NDKUser) { + targetUser = recipient; + } + + const event = new NDKEvent(ndk, { kind: 4, content } as NostrEvent); + event.tag(targetUser); + await event.encrypt(targetUser); + await event.sign(); + await event.publish(); + + return event; +}