From c43f1cc95e1cf4e48afea1f35d18776d27818175 Mon Sep 17 00:00:00 2001 From: pablof7z
Date: Sun, 4 Jun 2023 10:03:02 +0200
Subject: [PATCH] Policies and single-use tokens
---
SECURITY-MODEL.md | 26 ++--
package-lock.json | 78 ++++++------
package.json | 8 +-
.../20230603115339_policies/migration.sql | 19 +++
.../20230603121616_add_deleted/migration.sql | 2 +
.../20230603122649_desc/migration.sql | 2 +
.../20230603134135_tokens/migration.sql | 19 +++
.../20230603145715_add_keyname/migration.sql | 30 +++++
prisma/schema.prisma | 47 +++++++-
src/config/index.ts | 5 +-
.../admin/commands/create_new_policy.ts | 33 +++++
src/daemon/admin/commands/create_new_token.ts | 30 +++++
src/daemon/admin/index.ts | 114 +++++++++++++++---
src/daemon/backend/index.ts | 64 ++++++++++
src/daemon/lib/acl/index.ts | 16 +--
15 files changed, 402 insertions(+), 91 deletions(-)
create mode 100644 prisma/migrations/20230603115339_policies/migration.sql
create mode 100644 prisma/migrations/20230603121616_add_deleted/migration.sql
create mode 100644 prisma/migrations/20230603122649_desc/migration.sql
create mode 100644 prisma/migrations/20230603134135_tokens/migration.sql
create mode 100644 prisma/migrations/20230603145715_add_keyname/migration.sql
create mode 100644 src/daemon/admin/commands/create_new_policy.ts
create mode 100644 src/daemon/admin/commands/create_new_token.ts
diff --git a/SECURITY-MODEL.md b/SECURITY-MODEL.md
index aa55631..f203b59 100644
--- a/SECURITY-MODEL.md
+++ b/SECURITY-MODEL.md
@@ -1,31 +1,25 @@
# Security Model
-The premise of nsecBunker is that you can store Nostr private keys (nsecs), use them remotely
-under certain policies, but these keys can never be exfiltrated from nsecBunker.
+The premise of nsecBunker is that you can store Nostr private keys (nsecs), use them remotely under certain policies, but these keys can never be exfiltrated from nsecBunker.
All communication with nsecBunker happens through encrypted, ephemeral nostr events.
## Keys
Within nsecBunker there are two distinct sets of keys:
-### User keys
-The keys that users want to sign with (e.g. your personal or company's key).
-These keys are stored encrypted with a passphrase; the same way Lightning Network's LND
-stores keys locally: every time you start nsecBunker, you must enter the passphrase to decrypt it.
+### User keys (aka target keys)
+The keys that users want to sign with (e.g. your personal or company's keys).
+
+These keys are stored encrypted with a passphrase; the same way Lightning Network's LND stores keys locally: every time you start nsecBunker, you must enter the passphrase to decrypt it.
+
Without this passphrase, keys cannot be used.
### nsecBunker's key
-nsecBunker generates it's own private key, which is used solely to communicate
-with the nsecBunker administration UI. If these keys are compromised, no key material is at risk.
+nsecBunker generates it's own private key, which is used solely to communicate with the nsecBunker administration UI. If these keys are compromised, no key material is at risk.
-To interact with nsecBunker's administration UI, the administrator(s)' keys must be whitelisted
-within nsecBunker. All communication between the administrator and the nsecBunker is end-to-end
-encrypted with these two set of keys.
+To interact with nsecBunker's administration UI, the administrator(s)' keys must be whitelisted within nsecBunker. All communication between the administrator and the nsecBunker is end-to-end encrypted with these two set of keys.
-Non-whitelisted keys simply cannot talk to nsecBunker's Administration UI, which is why even if
-the nsecBunker connection string that is created when you setup your nsecBunker is leaked, nothing
-happens.
+Non-whitelisted keys simply cannot talk to nsecBunker's Administration UI.
## Nostr Connect
-nsecBunker listens on certain relays (specified in the config file) for keys that are attempting to
-sign with the
\ No newline at end of file
+nsecBunker listens on certain relays (specified in the config file) for keys that are attempting to sign with the target keys.
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 7980eec..5a4dc40 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,18 +1,18 @@
{
"name": "nsecbunkerd",
- "version": "0.5.7",
+ "version": "0.5.9",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nsecbunkerd",
- "version": "0.5.7",
+ "version": "0.5.9",
"license": "CC BY-NC-ND 4.0",
"dependencies": {
"@inquirer/password": "^1.0.0",
"@inquirer/prompts": "^1.0.0",
- "@nostr-dev-kit/ndk": "^0.3.26",
- "@prisma/client": "^4.14.1",
+ "@nostr-dev-kit/ndk": "^0.3.32",
+ "@prisma/client": "^4.15.0",
"@scure/base": "^1.1.1",
"@types/yargs": "^17.0.24",
"@typescript-eslint/eslint-plugin": "^5.57.0",
@@ -32,7 +32,7 @@
"devDependencies": {
"@types/debug": "^4.1.7",
"@types/node": "^18.15.11",
- "prisma": "^4.14.1",
+ "prisma": "^4.15.0",
"ts-node": "^10.9.1",
"typescript": "^5.0.3"
}
@@ -1117,9 +1117,9 @@
}
},
"node_modules/@nostr-dev-kit/ndk": {
- "version": "0.3.26",
- "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-0.3.26.tgz",
- "integrity": "sha512-Blr8T2G2CuhCK4JRqZxLE2ZsvXHrBTtJEfNN3Iw8uzMw5uhYM+oaNNVOZ3Gwf0Hth4nxTjsZSqYiUvqf5t7DRw==",
+ "version": "0.3.32",
+ "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-0.3.32.tgz",
+ "integrity": "sha512-f6fttPt6rRFNM0JV+atNZudz3MUpS4cZtPHZv8DQOXgfEOzjJCG5yOl0n9AUTsPYQirsGq3glW/gZs/LkxrP8g==",
"dependencies": {
"@noble/secp256k1": "^2.0.0",
"@scure/base": "^1.1.1",
@@ -1143,12 +1143,12 @@
}
},
"node_modules/@prisma/client": {
- "version": "4.14.1",
- "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.14.1.tgz",
- "integrity": "sha512-TZIswkeX1ccsHG/eN2kICzg/csXll0osK3EHu1QKd8VJ3XLcXozbNELKkCNfsCUvKJAwPdDtFCzF+O+raIVldw==",
+ "version": "4.15.0",
+ "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.15.0.tgz",
+ "integrity": "sha512-xnROvyABcGiwqRNdrObHVZkD9EjkJYHOmVdlKy1yGgI+XOzvMzJ4tRg3dz1pUlsyhKxXGCnjIQjWW+2ur+YXuw==",
"hasInstallScript": true,
"dependencies": {
- "@prisma/engines-version": "4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c"
+ "@prisma/engines-version": "4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944"
},
"engines": {
"node": ">=14.17"
@@ -1163,16 +1163,16 @@
}
},
"node_modules/@prisma/engines": {
- "version": "4.14.1",
- "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.14.1.tgz",
- "integrity": "sha512-APqFddPVHYmWNKqc+5J5SqrLFfOghKOLZxobmguDUacxOwdEutLsbXPVhNnpFDmuQWQFbXmrTTPoRrrF6B1MWA==",
+ "version": "4.15.0",
+ "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.15.0.tgz",
+ "integrity": "sha512-FTaOCGs0LL0OW68juZlGxFtYviZa4xdQj/rQEdat2txw0s3Vu/saAPKjNVXfIgUsGXmQ72HPgNr6935/P8FNAA==",
"devOptional": true,
"hasInstallScript": true
},
"node_modules/@prisma/engines-version": {
- "version": "4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c",
- "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c.tgz",
- "integrity": "sha512-3jum8/YSudeSN0zGW5qkpz+wAN2V/NYCQ+BPjvHYDfWatLWlQkqy99toX0GysDeaUoBIJg1vaz2yKqiA3CFcQw=="
+ "version": "4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944",
+ "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944.tgz",
+ "integrity": "sha512-sVOig4tjGxxlYaFcXgE71f/rtFhzyYrfyfNFUsxCIEJyVKU9rdOWIlIwQ2NQ7PntvGnn+x0XuFo4OC1jvPJKzg=="
},
"node_modules/@scure/base": {
"version": "1.1.1",
@@ -4199,13 +4199,13 @@
}
},
"node_modules/prisma": {
- "version": "4.14.1",
- "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.14.1.tgz",
- "integrity": "sha512-z6hxzTMYqT9SIKlzD08dhzsLUpxjFKKsLpp5/kBDnSqiOjtUyyl/dC5tzxLcOa3jkEHQ8+RpB/fE3w8bgNP51g==",
+ "version": "4.15.0",
+ "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.15.0.tgz",
+ "integrity": "sha512-iKZZpobPl48gTcSZVawLMQ3lEy6BnXwtoMj7hluoGFYu2kQ6F9LBuBrUyF95zRVnNo8/3KzLXJXJ5TEnLSJFiA==",
"devOptional": true,
"hasInstallScript": true,
"dependencies": {
- "@prisma/engines": "4.14.1"
+ "@prisma/engines": "4.15.0"
},
"bin": {
"prisma": "build/index.js",
@@ -5872,9 +5872,9 @@
}
},
"@nostr-dev-kit/ndk": {
- "version": "0.3.26",
- "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-0.3.26.tgz",
- "integrity": "sha512-Blr8T2G2CuhCK4JRqZxLE2ZsvXHrBTtJEfNN3Iw8uzMw5uhYM+oaNNVOZ3Gwf0Hth4nxTjsZSqYiUvqf5t7DRw==",
+ "version": "0.3.32",
+ "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-0.3.32.tgz",
+ "integrity": "sha512-f6fttPt6rRFNM0JV+atNZudz3MUpS4cZtPHZv8DQOXgfEOzjJCG5yOl0n9AUTsPYQirsGq3glW/gZs/LkxrP8g==",
"requires": {
"@noble/secp256k1": "^2.0.0",
"@scure/base": "^1.1.1",
@@ -5898,23 +5898,23 @@
}
},
"@prisma/client": {
- "version": "4.14.1",
- "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.14.1.tgz",
- "integrity": "sha512-TZIswkeX1ccsHG/eN2kICzg/csXll0osK3EHu1QKd8VJ3XLcXozbNELKkCNfsCUvKJAwPdDtFCzF+O+raIVldw==",
+ "version": "4.15.0",
+ "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.15.0.tgz",
+ "integrity": "sha512-xnROvyABcGiwqRNdrObHVZkD9EjkJYHOmVdlKy1yGgI+XOzvMzJ4tRg3dz1pUlsyhKxXGCnjIQjWW+2ur+YXuw==",
"requires": {
- "@prisma/engines-version": "4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c"
+ "@prisma/engines-version": "4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944"
}
},
"@prisma/engines": {
- "version": "4.14.1",
- "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.14.1.tgz",
- "integrity": "sha512-APqFddPVHYmWNKqc+5J5SqrLFfOghKOLZxobmguDUacxOwdEutLsbXPVhNnpFDmuQWQFbXmrTTPoRrrF6B1MWA==",
+ "version": "4.15.0",
+ "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.15.0.tgz",
+ "integrity": "sha512-FTaOCGs0LL0OW68juZlGxFtYviZa4xdQj/rQEdat2txw0s3Vu/saAPKjNVXfIgUsGXmQ72HPgNr6935/P8FNAA==",
"devOptional": true
},
"@prisma/engines-version": {
- "version": "4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c",
- "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c.tgz",
- "integrity": "sha512-3jum8/YSudeSN0zGW5qkpz+wAN2V/NYCQ+BPjvHYDfWatLWlQkqy99toX0GysDeaUoBIJg1vaz2yKqiA3CFcQw=="
+ "version": "4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944",
+ "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944.tgz",
+ "integrity": "sha512-sVOig4tjGxxlYaFcXgE71f/rtFhzyYrfyfNFUsxCIEJyVKU9rdOWIlIwQ2NQ7PntvGnn+x0XuFo4OC1jvPJKzg=="
},
"@scure/base": {
"version": "1.1.1",
@@ -8073,12 +8073,12 @@
}
},
"prisma": {
- "version": "4.14.1",
- "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.14.1.tgz",
- "integrity": "sha512-z6hxzTMYqT9SIKlzD08dhzsLUpxjFKKsLpp5/kBDnSqiOjtUyyl/dC5tzxLcOa3jkEHQ8+RpB/fE3w8bgNP51g==",
+ "version": "4.15.0",
+ "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.15.0.tgz",
+ "integrity": "sha512-iKZZpobPl48gTcSZVawLMQ3lEy6BnXwtoMj7hluoGFYu2kQ6F9LBuBrUyF95zRVnNo8/3KzLXJXJ5TEnLSJFiA==",
"devOptional": true,
"requires": {
- "@prisma/engines": "4.14.1"
+ "@prisma/engines": "4.15.0"
}
},
"punycode": {
diff --git a/package.json b/package.json
index d2929a5..b5402b0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "nsecbunkerd",
- "version": "0.5.8",
+ "version": "0.6.0",
"description": "nsecbunker daemon",
"main": "dist/index.js",
"bin": {
@@ -35,8 +35,8 @@
"dependencies": {
"@inquirer/password": "^1.0.0",
"@inquirer/prompts": "^1.0.0",
- "@nostr-dev-kit/ndk": "^0.3.26",
- "@prisma/client": "^4.14.1",
+ "@nostr-dev-kit/ndk": "^0.3.32",
+ "@prisma/client": "^4.15.0",
"@scure/base": "^1.1.1",
"@types/yargs": "^17.0.24",
"@typescript-eslint/eslint-plugin": "^5.57.0",
@@ -52,7 +52,7 @@
"devDependencies": {
"@types/debug": "^4.1.7",
"@types/node": "^18.15.11",
- "prisma": "^4.14.1",
+ "prisma": "^4.15.0",
"ts-node": "^10.9.1",
"typescript": "^5.0.3"
}
diff --git a/prisma/migrations/20230603115339_policies/migration.sql b/prisma/migrations/20230603115339_policies/migration.sql
new file mode 100644
index 0000000..e96a7f0
--- /dev/null
+++ b/prisma/migrations/20230603115339_policies/migration.sql
@@ -0,0 +1,19 @@
+-- CreateTable
+CREATE TABLE "Policy" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "name" TEXT NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "expiresAt" DATETIME
+);
+
+-- CreateTable
+CREATE TABLE "PolicyRule" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "method" TEXT NOT NULL,
+ "kind" TEXT,
+ "maxUsageCount" INTEGER,
+ "currentUsageCount" INTEGER,
+ "policyId" INTEGER,
+ CONSTRAINT "PolicyRule_policyId_fkey" FOREIGN KEY ("policyId") REFERENCES "Policy" ("id") ON DELETE SET NULL ON UPDATE CASCADE
+);
diff --git a/prisma/migrations/20230603121616_add_deleted/migration.sql b/prisma/migrations/20230603121616_add_deleted/migration.sql
new file mode 100644
index 0000000..f592988
--- /dev/null
+++ b/prisma/migrations/20230603121616_add_deleted/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "Policy" ADD COLUMN "deletedAt" DATETIME;
diff --git a/prisma/migrations/20230603122649_desc/migration.sql b/prisma/migrations/20230603122649_desc/migration.sql
new file mode 100644
index 0000000..3d10ccf
--- /dev/null
+++ b/prisma/migrations/20230603122649_desc/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "Policy" ADD COLUMN "description" TEXT;
diff --git a/prisma/migrations/20230603134135_tokens/migration.sql b/prisma/migrations/20230603134135_tokens/migration.sql
new file mode 100644
index 0000000..63c0814
--- /dev/null
+++ b/prisma/migrations/20230603134135_tokens/migration.sql
@@ -0,0 +1,19 @@
+-- CreateTable
+CREATE TABLE "Token" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "token" TEXT NOT NULL,
+ "clientName" TEXT NOT NULL,
+ "createdBy" TEXT NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "deletedAt" DATETIME,
+ "expiresAt" DATETIME,
+ "redeemedAt" DATETIME,
+ "keyUserId" INTEGER,
+ "policyId" INTEGER,
+ CONSTRAINT "Token_keyUserId_fkey" FOREIGN KEY ("keyUserId") REFERENCES "KeyUser" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
+ CONSTRAINT "Token_policyId_fkey" FOREIGN KEY ("policyId") REFERENCES "Policy" ("id") ON DELETE SET NULL ON UPDATE CASCADE
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Token_token_key" ON "Token"("token");
diff --git a/prisma/migrations/20230603145715_add_keyname/migration.sql b/prisma/migrations/20230603145715_add_keyname/migration.sql
new file mode 100644
index 0000000..039c9d7
--- /dev/null
+++ b/prisma/migrations/20230603145715_add_keyname/migration.sql
@@ -0,0 +1,30 @@
+/*
+ Warnings:
+
+ - Added the required column `keyName` to the `Token` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- RedefineTables
+PRAGMA foreign_keys=OFF;
+CREATE TABLE "new_Token" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "keyName" TEXT NOT NULL,
+ "token" TEXT NOT NULL,
+ "clientName" TEXT NOT NULL,
+ "createdBy" TEXT NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "deletedAt" DATETIME,
+ "expiresAt" DATETIME,
+ "redeemedAt" DATETIME,
+ "keyUserId" INTEGER,
+ "policyId" INTEGER,
+ CONSTRAINT "Token_keyUserId_fkey" FOREIGN KEY ("keyUserId") REFERENCES "KeyUser" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
+ CONSTRAINT "Token_policyId_fkey" FOREIGN KEY ("policyId") REFERENCES "Policy" ("id") ON DELETE SET NULL ON UPDATE CASCADE
+);
+INSERT INTO "new_Token" ("clientName", "createdAt", "createdBy", "deletedAt", "expiresAt", "id", "keyUserId", "policyId", "redeemedAt", "token", "updatedAt") SELECT "clientName", "createdAt", "createdBy", "deletedAt", "expiresAt", "id", "keyUserId", "policyId", "redeemedAt", "token", "updatedAt" FROM "Token";
+DROP TABLE "Token";
+ALTER TABLE "new_Token" RENAME TO "Token";
+CREATE UNIQUE INDEX "Token_token_key" ON "Token"("token");
+PRAGMA foreign_key_check;
+PRAGMA foreign_keys=ON;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 2293ab3..be46f9c 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -17,9 +17,10 @@ model KeyUser {
description String?
signingConditions SigningCondition[]
logs Log[]
- createdAt DateTime @default(now())
- updatedAt DateTime @default(now()) @updatedAt
- lastUsedAt DateTime?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @default(now()) @updatedAt
+ lastUsedAt DateTime?
+ Token Token[]
@@unique([keyName, userPubkey], name: "unique_key_user")
}
@@ -45,3 +46,43 @@ model Log {
KeyUser KeyUser? @relation(fields: [keyUserId], references: [id])
keyUserId Int?
}
+
+model Policy {
+ id Int @id @default(autoincrement())
+ name String
+ description String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @default(now()) @updatedAt
+ deletedAt DateTime?
+ expiresAt DateTime?
+ rules PolicyRule[]
+ Token Token[]
+}
+
+model PolicyRule {
+ id Int @id @default(autoincrement())
+ method String
+ kind String?
+ maxUsageCount Int?
+ currentUsageCount Int?
+
+ Policy Policy? @relation(fields: [policyId], references: [id])
+ policyId Int?
+}
+
+model Token {
+ id Int @id @default(autoincrement())
+ keyName String
+ token String @unique
+ clientName String
+ createdBy String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @default(now()) @updatedAt
+ deletedAt DateTime?
+ expiresAt DateTime?
+ redeemedAt DateTime?
+ KeyUser KeyUser? @relation(fields: [keyUserId], references: [id])
+ keyUserId Int?
+ policy Policy? @relation(fields: [policyId], references: [id])
+ policyId Int?
+}
diff --git a/src/config/index.ts b/src/config/index.ts
index fd9239c..af395bf 100644
--- a/src/config/index.ts
+++ b/src/config/index.ts
@@ -18,8 +18,11 @@ export interface IConfig {
const defaultConfig: IConfig = {
nostr: {
relays: [
+ 'wss://relay.damus.io',
'wss://nos.lol',
- // 'wss://relay.damus.io'
+ 'wss://relay.snort.social',
+ "wss://relay.nsecbunker.com",
+ "wss://nostr.vulpem.com",
]
},
admin: {
diff --git a/src/daemon/admin/commands/create_new_policy.ts b/src/daemon/admin/commands/create_new_policy.ts
new file mode 100644
index 0000000..e924c43
--- /dev/null
+++ b/src/daemon/admin/commands/create_new_policy.ts
@@ -0,0 +1,33 @@
+import { NDKRpcRequest } from "@nostr-dev-kit/ndk";
+import AdminInterface from "../index.js";
+import prisma from "../../../db.js";
+
+export default async function createNewPolicy(admin: AdminInterface, req: NDKRpcRequest) {
+ const [ _policy ] = req.params as [ string ];
+
+ if (!_policy) throw new Error("Invalid params");
+
+ const policy = JSON.parse(_policy);
+
+ const policyRecord = await prisma.policy.create({
+ data: {
+ name: policy.name,
+ expiresAt: policy.expires_at,
+ }
+ });
+
+ for (const rule of policy.rules) {
+ await prisma.policyRule.create({
+ data: {
+ policyId: policyRecord.id,
+ kind: rule.kind.toString(),
+ method: rule.method,
+ maxUsageCount: rule.use_count,
+ currentUsageCount: 0,
+ }
+ });
+ }
+
+ const result = JSON.stringify(["ok"]);
+ return admin.rpc.sendResponse(req.id, req.pubkey, result, 24134);
+}
\ No newline at end of file
diff --git a/src/daemon/admin/commands/create_new_token.ts b/src/daemon/admin/commands/create_new_token.ts
new file mode 100644
index 0000000..df145a2
--- /dev/null
+++ b/src/daemon/admin/commands/create_new_token.ts
@@ -0,0 +1,30 @@
+import { NDKRpcRequest } from "@nostr-dev-kit/ndk";
+import AdminInterface from "../index.js";
+import prisma from "../../../db.js";
+
+export default async function createNewToken(admin: AdminInterface, req: NDKRpcRequest) {
+ const [ keyName, clientName, policyId, durationInHours ] = req.params as [ string, string, string, string? ];
+
+ if (!clientName || !policyId) throw new Error("Invalid params");
+
+ const policy = await prisma.policy.findUnique({ where: { id: parseInt(policyId) }, include: { rules: true } });
+
+ if (!policy) throw new Error("Policy not found");
+
+ console.log({clientName, policy, durationInHours});
+
+ const token = [...Array(64)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
+ const data: any = {
+ keyName, clientName, policyId,
+ createdBy: req.pubkey,
+ token
+ };
+ if (durationInHours) data.expiresAt = new Date(Date.now() + (parseInt(durationInHours) * 60 * 60 * 1000));
+
+ const tokenRecord = await prisma.token.create({data});
+
+ if (!tokenRecord) throw new Error("Token not created");
+
+ const result = JSON.stringify(["ok"]);
+ return admin.rpc.sendResponse(req.id, req.pubkey, result, 24134);
+}
\ No newline at end of file
diff --git a/src/daemon/admin/index.ts b/src/daemon/admin/index.ts
index 05c8fc3..5b0c975 100644
--- a/src/daemon/admin/index.ts
+++ b/src/daemon/admin/index.ts
@@ -1,14 +1,12 @@
-import NDK, { NDKEvent, NDKPrivateKeySigner, NDKRpcRequest, NDKRpcResponse, NDKUser } from '@nostr-dev-kit/ndk';
+import NDK, { NDKPrivateKeySigner, NDKRpcRequest, NDKRpcResponse, NDKUser } from '@nostr-dev-kit/ndk';
import { NDKNostrRpc } from '@nostr-dev-kit/ndk';
import { debug } from 'debug';
import { Key, KeyUser } from '../run';
-import {
- checkIfPubkeyAllowed,
- allowAllRequestsFromKey,
- rejectAllRequestsFromKey
-} from '../lib/acl/index.js';
+import { allowAllRequestsFromKey } from '../lib/acl/index.js';
import prisma from '../../db';
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';
export type IAdminOpts = {
@@ -89,13 +87,23 @@ class AdminInterface {
private async handleRequest(req: NDKRpcRequest) {
// await this.validateRequest(req);
- switch (req.method) {
- case 'get_keys': this.reqGetKeys(req); break;
- case 'get_key_users': this.reqGetKeyUsers(req); break;
- case 'create_new_key': createNewKey(this, req); break;
- case 'unlock_key': unlockKey(this, req); break;
- default:
- console.log(`Unknown method ${req.method}`);
+ try {
+ 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;
+
+ default:
+ console.log(`Unknown method ${req.method}`);
+ }
+ } catch (err: any) {
+ console.error(`Error handling request ${req.method}: ${err.message}`, req.params);
}
}
@@ -103,6 +111,84 @@ class AdminInterface {
// TODO validate pubkey, validate signature
}
+ /**
+ * Command to list tokens
+ */
+ private async reqGetKeyTokens(req: NDKRpcRequest) {
+ const keyName = req.params[0];
+ const tokens = await prisma.token.findMany({
+ where: { keyName },
+ include: {
+ policy: {
+ include: {
+ rules: true,
+ },
+ },
+ KeyUser: true,
+ },
+ });
+
+ const keys = await this.getKeys!();
+ const key = keys.find((k) => k.name === keyName);
+
+ if (!key || !key.npub) {
+ return this.rpc.sendResponse(req.id, req.pubkey, JSON.stringify([]), 24134);
+ }
+
+ const npub = key.npub;
+
+ const result = JSON.stringify(tokens.map((t) => {
+ return {
+ id: t.id,
+ key_name: t.keyName,
+ client_name: t.clientName,
+ token: [ npub, t.token ].join('#'),
+ policy_id: t.policyId,
+ policy_name: t.policy?.name,
+ created_at: t.createdAt,
+ updated_at: t.updatedAt,
+ expires_at: t.expiresAt,
+ redeemed_at: t.redeemedAt,
+ redeemed_by: t.KeyUser?.description,
+ time_until_expiration: t.expiresAt ? (t.expiresAt.getTime() - Date.now()) / 1000 : null,
+ };
+ }));
+
+ return this.rpc.sendResponse(req.id, req.pubkey, result, 24134);
+ }
+
+ /**
+ * Command to list policies
+ */
+ private async reqListPolicies(req: NDKRpcRequest) {
+ const policies = await prisma.policy.findMany({
+ include: {
+ rules: true,
+ },
+ });
+
+ const result = JSON.stringify(policies.map((p) => {
+ return {
+ id: p.id,
+ name: p.name,
+ description: p.description,
+ created_at: p.createdAt,
+ updated_at: p.updatedAt,
+ expires_at: p.expiresAt,
+ rules: p.rules.map((r) => {
+ return {
+ method: r.method,
+ kind: r.kind,
+ max_usage_count: r.maxUsageCount,
+ current_usage_count: r.currentUsageCount,
+ };
+ })
+ };
+ }));
+
+ return this.rpc.sendResponse(req.id, req.pubkey, result, 24134);
+ }
+
/**
* Command to fetch keys and their current state
*/
@@ -127,8 +213,6 @@ class AdminInterface {
return this.rpc.sendResponse(req.id, pubkey, result, 24134); // 24134
}
-
-
/**
* This function is called when a request is received from a remote user that needs
* to be approved by the admin interface.
diff --git a/src/daemon/backend/index.ts b/src/daemon/backend/index.ts
index 9baabb8..fa983be 100644
--- a/src/daemon/backend/index.ts
+++ b/src/daemon/backend/index.ts
@@ -1,9 +1,73 @@
import NDK, { NDKNip46Backend, Nip46PermitCallback } from '@nostr-dev-kit/ndk';
import PublishEventHandlingStrategy from './publish-event.js';
+import prisma from '../../db.js';
export class Backend extends NDKNip46Backend {
constructor(ndk: NDK, key: string, cb: Nip46PermitCallback) {
super(ndk, key, cb);
+
this.setStrategy('publish_event', new PublishEventHandlingStrategy());
}
+
+ private async validateToken(token: string) {
+ if (!token) throw new Error("Invalid token");
+
+ const tokenRecord = await prisma.token.findUnique({ where: {
+ token
+ }, include: { policy: { include: { rules: true } } } });
+
+ if (!tokenRecord) throw new Error("Token not found");
+ if (tokenRecord.redeemedAt) throw new Error("Token already redeemed");
+ if (!tokenRecord.policy) throw new Error("Policy not found");
+ if (tokenRecord.expiresAt && tokenRecord.expiresAt < new Date()) throw new Error("Token expired");
+
+ return tokenRecord;
+ }
+
+ async applyToken(userPubkey: string, token: string): Promise