mirror of
https://github.com/aljazceru/nsecbunkerd.git
synced 2025-12-17 06:04:22 +01:00
Policies and single-use tokens
This commit is contained in:
@@ -1,31 +1,25 @@
|
|||||||
# Security Model
|
# Security Model
|
||||||
|
|
||||||
The premise of nsecBunker is that you can store Nostr private keys (nsecs), use them remotely
|
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.
|
||||||
under certain policies, but these keys can never be exfiltrated from nsecBunker.
|
|
||||||
|
|
||||||
All communication with nsecBunker happens through encrypted, ephemeral nostr events.
|
All communication with nsecBunker happens through encrypted, ephemeral nostr events.
|
||||||
|
|
||||||
## Keys
|
## Keys
|
||||||
Within nsecBunker there are two distinct sets of 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
|
### User keys (aka target keys)
|
||||||
stores keys locally: every time you start nsecBunker, you must enter the passphrase to decrypt it.
|
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.
|
Without this passphrase, keys cannot be used.
|
||||||
|
|
||||||
### nsecBunker's key
|
### nsecBunker's key
|
||||||
nsecBunker generates it's own private key, which is used solely to communicate
|
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.
|
||||||
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
|
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.
|
||||||
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
|
Non-whitelisted keys simply cannot talk to nsecBunker's Administration UI.
|
||||||
the nsecBunker connection string that is created when you setup your nsecBunker is leaked, nothing
|
|
||||||
happens.
|
|
||||||
|
|
||||||
## Nostr Connect
|
## Nostr Connect
|
||||||
nsecBunker listens on certain relays (specified in the config file) for keys that are attempting to
|
nsecBunker listens on certain relays (specified in the config file) for keys that are attempting to sign with the target keys.
|
||||||
sign with the
|
|
||||||
78
package-lock.json
generated
78
package-lock.json
generated
@@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "nsecbunkerd",
|
"name": "nsecbunkerd",
|
||||||
"version": "0.5.7",
|
"version": "0.5.9",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "nsecbunkerd",
|
"name": "nsecbunkerd",
|
||||||
"version": "0.5.7",
|
"version": "0.5.9",
|
||||||
"license": "CC BY-NC-ND 4.0",
|
"license": "CC BY-NC-ND 4.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@inquirer/password": "^1.0.0",
|
"@inquirer/password": "^1.0.0",
|
||||||
"@inquirer/prompts": "^1.0.0",
|
"@inquirer/prompts": "^1.0.0",
|
||||||
"@nostr-dev-kit/ndk": "^0.3.26",
|
"@nostr-dev-kit/ndk": "^0.3.32",
|
||||||
"@prisma/client": "^4.14.1",
|
"@prisma/client": "^4.15.0",
|
||||||
"@scure/base": "^1.1.1",
|
"@scure/base": "^1.1.1",
|
||||||
"@types/yargs": "^17.0.24",
|
"@types/yargs": "^17.0.24",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.57.0",
|
"@typescript-eslint/eslint-plugin": "^5.57.0",
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/debug": "^4.1.7",
|
"@types/debug": "^4.1.7",
|
||||||
"@types/node": "^18.15.11",
|
"@types/node": "^18.15.11",
|
||||||
"prisma": "^4.14.1",
|
"prisma": "^4.15.0",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^5.0.3"
|
"typescript": "^5.0.3"
|
||||||
}
|
}
|
||||||
@@ -1117,9 +1117,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nostr-dev-kit/ndk": {
|
"node_modules/@nostr-dev-kit/ndk": {
|
||||||
"version": "0.3.26",
|
"version": "0.3.32",
|
||||||
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-0.3.26.tgz",
|
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-0.3.32.tgz",
|
||||||
"integrity": "sha512-Blr8T2G2CuhCK4JRqZxLE2ZsvXHrBTtJEfNN3Iw8uzMw5uhYM+oaNNVOZ3Gwf0Hth4nxTjsZSqYiUvqf5t7DRw==",
|
"integrity": "sha512-f6fttPt6rRFNM0JV+atNZudz3MUpS4cZtPHZv8DQOXgfEOzjJCG5yOl0n9AUTsPYQirsGq3glW/gZs/LkxrP8g==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/secp256k1": "^2.0.0",
|
"@noble/secp256k1": "^2.0.0",
|
||||||
"@scure/base": "^1.1.1",
|
"@scure/base": "^1.1.1",
|
||||||
@@ -1143,12 +1143,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/client": {
|
"node_modules/@prisma/client": {
|
||||||
"version": "4.14.1",
|
"version": "4.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.15.0.tgz",
|
||||||
"integrity": "sha512-TZIswkeX1ccsHG/eN2kICzg/csXll0osK3EHu1QKd8VJ3XLcXozbNELKkCNfsCUvKJAwPdDtFCzF+O+raIVldw==",
|
"integrity": "sha512-xnROvyABcGiwqRNdrObHVZkD9EjkJYHOmVdlKy1yGgI+XOzvMzJ4tRg3dz1pUlsyhKxXGCnjIQjWW+2ur+YXuw==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/engines-version": "4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c"
|
"@prisma/engines-version": "4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
@@ -1163,16 +1163,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines": {
|
"node_modules/@prisma/engines": {
|
||||||
"version": "4.14.1",
|
"version": "4.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.15.0.tgz",
|
||||||
"integrity": "sha512-APqFddPVHYmWNKqc+5J5SqrLFfOghKOLZxobmguDUacxOwdEutLsbXPVhNnpFDmuQWQFbXmrTTPoRrrF6B1MWA==",
|
"integrity": "sha512-FTaOCGs0LL0OW68juZlGxFtYviZa4xdQj/rQEdat2txw0s3Vu/saAPKjNVXfIgUsGXmQ72HPgNr6935/P8FNAA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true
|
"hasInstallScript": true
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines-version": {
|
"node_modules/@prisma/engines-version": {
|
||||||
"version": "4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c",
|
"version": "4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944.tgz",
|
||||||
"integrity": "sha512-3jum8/YSudeSN0zGW5qkpz+wAN2V/NYCQ+BPjvHYDfWatLWlQkqy99toX0GysDeaUoBIJg1vaz2yKqiA3CFcQw=="
|
"integrity": "sha512-sVOig4tjGxxlYaFcXgE71f/rtFhzyYrfyfNFUsxCIEJyVKU9rdOWIlIwQ2NQ7PntvGnn+x0XuFo4OC1jvPJKzg=="
|
||||||
},
|
},
|
||||||
"node_modules/@scure/base": {
|
"node_modules/@scure/base": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
@@ -4199,13 +4199,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prisma": {
|
"node_modules/prisma": {
|
||||||
"version": "4.14.1",
|
"version": "4.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-4.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/prisma/-/prisma-4.15.0.tgz",
|
||||||
"integrity": "sha512-z6hxzTMYqT9SIKlzD08dhzsLUpxjFKKsLpp5/kBDnSqiOjtUyyl/dC5tzxLcOa3jkEHQ8+RpB/fE3w8bgNP51g==",
|
"integrity": "sha512-iKZZpobPl48gTcSZVawLMQ3lEy6BnXwtoMj7hluoGFYu2kQ6F9LBuBrUyF95zRVnNo8/3KzLXJXJ5TEnLSJFiA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/engines": "4.14.1"
|
"@prisma/engines": "4.15.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"prisma": "build/index.js",
|
"prisma": "build/index.js",
|
||||||
@@ -5872,9 +5872,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@nostr-dev-kit/ndk": {
|
"@nostr-dev-kit/ndk": {
|
||||||
"version": "0.3.26",
|
"version": "0.3.32",
|
||||||
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-0.3.26.tgz",
|
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-0.3.32.tgz",
|
||||||
"integrity": "sha512-Blr8T2G2CuhCK4JRqZxLE2ZsvXHrBTtJEfNN3Iw8uzMw5uhYM+oaNNVOZ3Gwf0Hth4nxTjsZSqYiUvqf5t7DRw==",
|
"integrity": "sha512-f6fttPt6rRFNM0JV+atNZudz3MUpS4cZtPHZv8DQOXgfEOzjJCG5yOl0n9AUTsPYQirsGq3glW/gZs/LkxrP8g==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@noble/secp256k1": "^2.0.0",
|
"@noble/secp256k1": "^2.0.0",
|
||||||
"@scure/base": "^1.1.1",
|
"@scure/base": "^1.1.1",
|
||||||
@@ -5898,23 +5898,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@prisma/client": {
|
"@prisma/client": {
|
||||||
"version": "4.14.1",
|
"version": "4.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.15.0.tgz",
|
||||||
"integrity": "sha512-TZIswkeX1ccsHG/eN2kICzg/csXll0osK3EHu1QKd8VJ3XLcXozbNELKkCNfsCUvKJAwPdDtFCzF+O+raIVldw==",
|
"integrity": "sha512-xnROvyABcGiwqRNdrObHVZkD9EjkJYHOmVdlKy1yGgI+XOzvMzJ4tRg3dz1pUlsyhKxXGCnjIQjWW+2ur+YXuw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@prisma/engines-version": "4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c"
|
"@prisma/engines-version": "4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@prisma/engines": {
|
"@prisma/engines": {
|
||||||
"version": "4.14.1",
|
"version": "4.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.15.0.tgz",
|
||||||
"integrity": "sha512-APqFddPVHYmWNKqc+5J5SqrLFfOghKOLZxobmguDUacxOwdEutLsbXPVhNnpFDmuQWQFbXmrTTPoRrrF6B1MWA==",
|
"integrity": "sha512-FTaOCGs0LL0OW68juZlGxFtYviZa4xdQj/rQEdat2txw0s3Vu/saAPKjNVXfIgUsGXmQ72HPgNr6935/P8FNAA==",
|
||||||
"devOptional": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"@prisma/engines-version": {
|
"@prisma/engines-version": {
|
||||||
"version": "4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c",
|
"version": "4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944.tgz",
|
||||||
"integrity": "sha512-3jum8/YSudeSN0zGW5qkpz+wAN2V/NYCQ+BPjvHYDfWatLWlQkqy99toX0GysDeaUoBIJg1vaz2yKqiA3CFcQw=="
|
"integrity": "sha512-sVOig4tjGxxlYaFcXgE71f/rtFhzyYrfyfNFUsxCIEJyVKU9rdOWIlIwQ2NQ7PntvGnn+x0XuFo4OC1jvPJKzg=="
|
||||||
},
|
},
|
||||||
"@scure/base": {
|
"@scure/base": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
@@ -8073,12 +8073,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"prisma": {
|
"prisma": {
|
||||||
"version": "4.14.1",
|
"version": "4.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-4.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/prisma/-/prisma-4.15.0.tgz",
|
||||||
"integrity": "sha512-z6hxzTMYqT9SIKlzD08dhzsLUpxjFKKsLpp5/kBDnSqiOjtUyyl/dC5tzxLcOa3jkEHQ8+RpB/fE3w8bgNP51g==",
|
"integrity": "sha512-iKZZpobPl48gTcSZVawLMQ3lEy6BnXwtoMj7hluoGFYu2kQ6F9LBuBrUyF95zRVnNo8/3KzLXJXJ5TEnLSJFiA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@prisma/engines": "4.14.1"
|
"@prisma/engines": "4.15.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"punycode": {
|
"punycode": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nsecbunkerd",
|
"name": "nsecbunkerd",
|
||||||
"version": "0.5.8",
|
"version": "0.6.0",
|
||||||
"description": "nsecbunker daemon",
|
"description": "nsecbunker daemon",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -35,8 +35,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@inquirer/password": "^1.0.0",
|
"@inquirer/password": "^1.0.0",
|
||||||
"@inquirer/prompts": "^1.0.0",
|
"@inquirer/prompts": "^1.0.0",
|
||||||
"@nostr-dev-kit/ndk": "^0.3.26",
|
"@nostr-dev-kit/ndk": "^0.3.32",
|
||||||
"@prisma/client": "^4.14.1",
|
"@prisma/client": "^4.15.0",
|
||||||
"@scure/base": "^1.1.1",
|
"@scure/base": "^1.1.1",
|
||||||
"@types/yargs": "^17.0.24",
|
"@types/yargs": "^17.0.24",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.57.0",
|
"@typescript-eslint/eslint-plugin": "^5.57.0",
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/debug": "^4.1.7",
|
"@types/debug": "^4.1.7",
|
||||||
"@types/node": "^18.15.11",
|
"@types/node": "^18.15.11",
|
||||||
"prisma": "^4.14.1",
|
"prisma": "^4.15.0",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^5.0.3"
|
"typescript": "^5.0.3"
|
||||||
}
|
}
|
||||||
|
|||||||
19
prisma/migrations/20230603115339_policies/migration.sql
Normal file
19
prisma/migrations/20230603115339_policies/migration.sql
Normal file
@@ -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
|
||||||
|
);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Policy" ADD COLUMN "deletedAt" DATETIME;
|
||||||
2
prisma/migrations/20230603122649_desc/migration.sql
Normal file
2
prisma/migrations/20230603122649_desc/migration.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Policy" ADD COLUMN "description" TEXT;
|
||||||
19
prisma/migrations/20230603134135_tokens/migration.sql
Normal file
19
prisma/migrations/20230603134135_tokens/migration.sql
Normal file
@@ -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");
|
||||||
30
prisma/migrations/20230603145715_add_keyname/migration.sql
Normal file
30
prisma/migrations/20230603145715_add_keyname/migration.sql
Normal file
@@ -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;
|
||||||
@@ -20,6 +20,7 @@ model KeyUser {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
lastUsedAt DateTime?
|
lastUsedAt DateTime?
|
||||||
|
Token Token[]
|
||||||
|
|
||||||
@@unique([keyName, userPubkey], name: "unique_key_user")
|
@@unique([keyName, userPubkey], name: "unique_key_user")
|
||||||
}
|
}
|
||||||
@@ -45,3 +46,43 @@ model Log {
|
|||||||
KeyUser KeyUser? @relation(fields: [keyUserId], references: [id])
|
KeyUser KeyUser? @relation(fields: [keyUserId], references: [id])
|
||||||
keyUserId Int?
|
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?
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,8 +18,11 @@ export interface IConfig {
|
|||||||
const defaultConfig: IConfig = {
|
const defaultConfig: IConfig = {
|
||||||
nostr: {
|
nostr: {
|
||||||
relays: [
|
relays: [
|
||||||
|
'wss://relay.damus.io',
|
||||||
'wss://nos.lol',
|
'wss://nos.lol',
|
||||||
// 'wss://relay.damus.io'
|
'wss://relay.snort.social',
|
||||||
|
"wss://relay.nsecbunker.com",
|
||||||
|
"wss://nostr.vulpem.com",
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
|
|||||||
33
src/daemon/admin/commands/create_new_policy.ts
Normal file
33
src/daemon/admin/commands/create_new_policy.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
30
src/daemon/admin/commands/create_new_token.ts
Normal file
30
src/daemon/admin/commands/create_new_token.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
@@ -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 { NDKNostrRpc } from '@nostr-dev-kit/ndk';
|
||||||
import { debug } from 'debug';
|
import { debug } from 'debug';
|
||||||
import { Key, KeyUser } from '../run';
|
import { Key, KeyUser } from '../run';
|
||||||
import {
|
import { allowAllRequestsFromKey } from '../lib/acl/index.js';
|
||||||
checkIfPubkeyAllowed,
|
|
||||||
allowAllRequestsFromKey,
|
|
||||||
rejectAllRequestsFromKey
|
|
||||||
} from '../lib/acl/index.js';
|
|
||||||
import prisma from '../../db';
|
import prisma from '../../db';
|
||||||
import createNewKey from './commands/create_new_key';
|
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 unlockKey from './commands/unlock_key';
|
||||||
|
|
||||||
export type IAdminOpts = {
|
export type IAdminOpts = {
|
||||||
@@ -89,20 +87,108 @@ class AdminInterface {
|
|||||||
private async handleRequest(req: NDKRpcRequest) {
|
private async handleRequest(req: NDKRpcRequest) {
|
||||||
// await this.validateRequest(req);
|
// await this.validateRequest(req);
|
||||||
|
|
||||||
|
try {
|
||||||
switch (req.method) {
|
switch (req.method) {
|
||||||
case 'get_keys': this.reqGetKeys(req); break;
|
case 'get_keys': this.reqGetKeys(req); break;
|
||||||
case 'get_key_users': this.reqGetKeyUsers(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 'create_new_key': createNewKey(this, req); break;
|
||||||
case 'unlock_key': unlockKey(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:
|
default:
|
||||||
console.log(`Unknown method ${req.method}`);
|
console.log(`Unknown method ${req.method}`);
|
||||||
}
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`Error handling request ${req.method}: ${err.message}`, req.params);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async validateRequest(req: NDKRpcRequest) {
|
private async validateRequest(req: NDKRpcRequest) {
|
||||||
// TODO validate pubkey, validate signature
|
// 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
|
* Command to fetch keys and their current state
|
||||||
*/
|
*/
|
||||||
@@ -127,8 +213,6 @@ class AdminInterface {
|
|||||||
return this.rpc.sendResponse(req.id, pubkey, result, 24134); // 24134
|
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
|
* This function is called when a request is received from a remote user that needs
|
||||||
* to be approved by the admin interface.
|
* to be approved by the admin interface.
|
||||||
|
|||||||
@@ -1,9 +1,73 @@
|
|||||||
import NDK, { NDKNip46Backend, Nip46PermitCallback } from '@nostr-dev-kit/ndk';
|
import NDK, { NDKNip46Backend, Nip46PermitCallback } from '@nostr-dev-kit/ndk';
|
||||||
import PublishEventHandlingStrategy from './publish-event.js';
|
import PublishEventHandlingStrategy from './publish-event.js';
|
||||||
|
import prisma from '../../db.js';
|
||||||
|
|
||||||
export class Backend extends NDKNip46Backend {
|
export class Backend extends NDKNip46Backend {
|
||||||
constructor(ndk: NDK, key: string, cb: Nip46PermitCallback) {
|
constructor(ndk: NDK, key: string, cb: Nip46PermitCallback) {
|
||||||
super(ndk, key, cb);
|
super(ndk, key, cb);
|
||||||
|
|
||||||
this.setStrategy('publish_event', new PublishEventHandlingStrategy());
|
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<void> {
|
||||||
|
const tokenRecord = await this.validateToken(token);
|
||||||
|
const keyName = tokenRecord.keyName;
|
||||||
|
|
||||||
|
// Upsert the KeyUser with the given remotePubkey
|
||||||
|
const upsertedUser = await prisma.keyUser.upsert({
|
||||||
|
where: { unique_key_user: { keyName, userPubkey } },
|
||||||
|
update: { },
|
||||||
|
create: { keyName, userPubkey, description: tokenRecord.clientName },
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.signingCondition.create({
|
||||||
|
data: {
|
||||||
|
keyUserId: upsertedUser.id,
|
||||||
|
method: 'connect',
|
||||||
|
allowed: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Go through the rules of this policy and apply them to the user
|
||||||
|
for (const rule of tokenRecord!.policy!.rules) {
|
||||||
|
const signingConditionQuery: any = { method: rule.method };
|
||||||
|
|
||||||
|
if (rule && rule.kind) {
|
||||||
|
signingConditionQuery.kind = rule.kind.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.signingCondition.create({
|
||||||
|
data: {
|
||||||
|
keyUserId: upsertedUser.id,
|
||||||
|
method: rule.method,
|
||||||
|
allowed: true,
|
||||||
|
...signingConditionQuery,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.token.update({
|
||||||
|
where: { id: tokenRecord.id },
|
||||||
|
data: {
|
||||||
|
redeemedAt: new Date(),
|
||||||
|
keyUserId: upsertedUser.id,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export function requestToSigningConditionQuery(method: string, event?: NDKEvent)
|
|||||||
|
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case 'sign_event':
|
case 'sign_event':
|
||||||
signingConditionQuery.kind = event?.kind?.toString();
|
signingConditionQuery.kind = { in: [ event?.kind?.toString(), 'all' ] };
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,24 +97,16 @@ export async function allowAllRequestsFromKey(
|
|||||||
create: { keyName, userPubkey: remotePubkey, description },
|
create: { keyName, userPubkey: remotePubkey, description },
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log({ upsertedUser });
|
|
||||||
|
|
||||||
// Create a new SigningCondition for the given KeyUser and set allowed to true
|
// Create a new SigningCondition for the given KeyUser and set allowed to true
|
||||||
const signingConditionQuery = allowScopeToSigningConditionQuery(method, allowScope);
|
const signingConditionQuery = allowScopeToSigningConditionQuery(method, allowScope);
|
||||||
await prisma.signingCondition.create({
|
await prisma.signingCondition.create({
|
||||||
data: {
|
data: {
|
||||||
allowed: true,
|
allowed: true,
|
||||||
keyUserId: upsertedUser.id,
|
keyUserId: upsertedUser.id,
|
||||||
...signingConditionQuery
|
...signingConditionQuery,
|
||||||
|
kind: 'all'
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`create`, {
|
|
||||||
allowed: true,
|
|
||||||
keyUserId: upsertedUser.id,
|
|
||||||
...signingConditionQuery
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('allowAllRequestsFromKey', e);
|
console.log('allowAllRequestsFromKey', e);
|
||||||
}
|
}
|
||||||
@@ -128,8 +120,6 @@ export async function rejectAllRequestsFromKey(remotePubkey: string, keyName: st
|
|||||||
create: { keyName, userPubkey: remotePubkey },
|
create: { keyName, userPubkey: remotePubkey },
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log({ upsertedUser });
|
|
||||||
|
|
||||||
// Create a new SigningCondition for the given KeyUser and set allowed to false
|
// Create a new SigningCondition for the given KeyUser and set allowed to false
|
||||||
await prisma.signingCondition.create({
|
await prisma.signingCondition.create({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
Reference in New Issue
Block a user