Policies and single-use tokens

This commit is contained in:
pablof7z 2023-06-04 10:03:02 +02:00
parent 28f4788aec
commit c43f1cc95e
15 changed files with 402 additions and 91 deletions

View File

@ -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
nsecBunker listens on certain relays (specified in the config file) for keys that are attempting to sign with the target keys.

78
package-lock.json generated
View File

@ -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": {

View File

@ -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"
}

View 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
);

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Policy" ADD COLUMN "deletedAt" DATETIME;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Policy" ADD COLUMN "description" TEXT;

View 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");

View 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;

View File

@ -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?
}

View File

@ -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: {

View 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);
}

View 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);
}

View File

@ -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.

View File

@ -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<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,
}
});
}
}

View File

@ -63,7 +63,7 @@ export function requestToSigningConditionQuery(method: string, event?: NDKEvent)
switch (method) {
case 'sign_event':
signingConditionQuery.kind = event?.kind?.toString();
signingConditionQuery.kind = { in: [ event?.kind?.toString(), 'all' ] };
break;
}
@ -97,24 +97,16 @@ export async function allowAllRequestsFromKey(
create: { keyName, userPubkey: remotePubkey, description },
});
console.log({ upsertedUser });
// Create a new SigningCondition for the given KeyUser and set allowed to true
const signingConditionQuery = allowScopeToSigningConditionQuery(method, allowScope);
await prisma.signingCondition.create({
data: {
allowed: true,
keyUserId: upsertedUser.id,
...signingConditionQuery
...signingConditionQuery,
kind: 'all'
},
});
console.log(`create`, {
allowed: true,
keyUserId: upsertedUser.id,
...signingConditionQuery
});
} catch (e) {
console.log('allowAllRequestsFromKey', e);
}
@ -128,8 +120,6 @@ export async function rejectAllRequestsFromKey(remotePubkey: string, keyName: st
create: { keyName, userPubkey: remotePubkey },
});
console.log({ upsertedUser });
// Create a new SigningCondition for the given KeyUser and set allowed to false
await prisma.signingCondition.create({
data: {