updates
This commit is contained in:
parent
54de9cfa8e
commit
74cd9715ac
12 changed files with 3353 additions and 438 deletions
75
README.md
75
README.md
|
|
@ -1,15 +1,78 @@
|
|||
# nsecbunker
|
||||
# nsecbunkerd
|
||||
Daemon to remotely sign nostr events using keys.
|
||||
|
||||
## Installation
|
||||
## Easy setup via docker
|
||||
|
||||
To quickly install `nsecbunkerd` via Docker just run:
|
||||
|
||||
```
|
||||
npm i -g nsecbunker
|
||||
docker run -d --name nsecbunkerd pablof7z/nsecbunkerd start --admin <your-npub>
|
||||
```
|
||||
|
||||
## Usage
|
||||
nsecBunker will give you a connection string like:
|
||||
|
||||
```
|
||||
bunker://npub1tj2dmc4udvgafxxxxxxxrtgne8j8l6rgrnaykzc8sys9mzfcz@relay.nsecbunker.com
|
||||
```
|
||||
|
||||
You can visit https://app.nsecbunker.com/ to administrate your nsecBunker remotely.
|
||||
|
||||
## Hard setup:
|
||||
(If you installed via docker you don't need to do any of this, skip to the [Configure](#configure) section)
|
||||
|
||||
```
|
||||
git clone <nsecbunkerd-repo>
|
||||
npm i
|
||||
npm run build
|
||||
npx prisma migrate deploy
|
||||
```
|
||||
|
||||
## Configure
|
||||
|
||||
### Easy: Remote configuration
|
||||
|
||||
Using the connection string you saw before, you can go to https://app.nsecbunker.com and paste your connection string.
|
||||
|
||||
Note that ONLY the npub that you designated as an administrator when launching nsecBunker is able to control your nsecBunker. Even if someone sees your connection string, without access to your administrator keys, there's nothing they can do.
|
||||
|
||||
### Hard: manual configuration
|
||||
|
||||
(If you are using remote configuration you don't need to do any of this)
|
||||
|
||||
### Add your nsec to nsecBunker
|
||||
|
||||
Here you'll give nsecBunker your nsec. It will ask you for a passphrase to encrypt it on-disk.
|
||||
The name is an internal name you'll use to refer to this keypair. Choose anything that is useful to you.
|
||||
|
||||
```
|
||||
npm run nsecbunkerd -- add --name <your-key-name>
|
||||
```
|
||||
|
||||
#### Example
|
||||
```
|
||||
$ npm run nsecbunkerd -- add --name "Uncomfortable family"
|
||||
|
||||
nsecBunker uses a passphrase to encrypt your nsec when stored on-disk.
|
||||
Every time you restart it, you will need to type in this password.
|
||||
|
||||
Enter a passphrase: <enter-your-passphrase-here>
|
||||
Enter the nsec for Uncomfortable family: <copy-your-nsec-here>
|
||||
nsecBunker generated an admin password for you:
|
||||
|
||||
***************************
|
||||
|
||||
You will need this to manage users of your keys.
|
||||
````
|
||||
|
||||
## Start
|
||||
|
||||
```
|
||||
$ npm run nsecbunkerd start
|
||||
```
|
||||
|
||||
## Testing with `nsecbunker-client`
|
||||
|
||||
|
||||
nsecbunker -
|
||||
|
||||
# Authors
|
||||
|
||||
|
|
@ -18,5 +81,5 @@ nsecbunker -
|
|||
|
||||
# License
|
||||
|
||||
CC BY-NC-ND 3.0
|
||||
CC BY-NC-ND 4.0
|
||||
Contact @pablof7z for licensing.
|
||||
2977
package-lock.json
generated
2977
package-lock.json
generated
File diff suppressed because it is too large
Load diff
25
package.json
25
package.json
|
|
@ -1,14 +1,18 @@
|
|||
{
|
||||
"name": "nsecbunker",
|
||||
"version": "0.1.0",
|
||||
"description": "nsecbunker",
|
||||
"name": "nsecbunkerd",
|
||||
"version": "0.5.8",
|
||||
"description": "nsecbunker daemon",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"nsecbunkerd": "dist/index.js",
|
||||
"nsecbunker-client": "dist/client.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
"dist",
|
||||
"scripts/start.js",
|
||||
"prisma/schema.prisma",
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -16,7 +20,10 @@
|
|||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"prisma:generate": "npx prisma generate",
|
||||
"prisma:migrate": "npx prisma migrate deploy",
|
||||
"prisma:create": "npx prisma db push --preview-feature",
|
||||
"start": "node ./scripts/start.js",
|
||||
"nsecbunkerd": "node dist/index.js",
|
||||
"nsecbunker-client": "node dist/client.js"
|
||||
},
|
||||
|
|
@ -24,16 +31,16 @@
|
|||
"nostr"
|
||||
],
|
||||
"author": "pablof7z",
|
||||
"license": "MIT",
|
||||
"license": "CC BY-NC-ND 4.0",
|
||||
"dependencies": {
|
||||
"@inquirer/password": "^1.0.0",
|
||||
"@inquirer/prompts": "^1.0.0",
|
||||
"@prisma/client": "4.13.0",
|
||||
"@nostr-dev-kit/ndk": "^0.3.26",
|
||||
"@prisma/client": "^4.14.1",
|
||||
"@scure/base": "^1.1.1",
|
||||
"@types/yargs": "^17.0.24",
|
||||
"@typescript-eslint/eslint-plugin": "^5.57.0",
|
||||
"@typescript-eslint/parser": "^5.57.0",
|
||||
"crypto": "^1.0.1",
|
||||
"dotenv": "^16.0.3",
|
||||
"eslint": "^8.37.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
|
|
@ -45,7 +52,7 @@
|
|||
"devDependencies": {
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/node": "^18.15.11",
|
||||
"prisma": "^4.13.0",
|
||||
"prisma": "^4.14.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.0.3"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,15 +7,19 @@ generator client {
|
|||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
url = "file:./nsecbunker.db"
|
||||
}
|
||||
|
||||
model KeyUser {
|
||||
id Int @id @default(autoincrement())
|
||||
keyName String
|
||||
userPubkey String
|
||||
description String?
|
||||
signingConditions SigningCondition[]
|
||||
logs Log[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
lastUsedAt DateTime?
|
||||
|
||||
@@unique([keyName, userPubkey], name: "unique_key_user")
|
||||
}
|
||||
|
|
@ -24,7 +28,7 @@ model SigningCondition {
|
|||
id Int @id @default(autoincrement())
|
||||
|
||||
method String?
|
||||
kind Int?
|
||||
kind String?
|
||||
content String?
|
||||
keyUserKeyName String?
|
||||
allowed Boolean?
|
||||
|
|
|
|||
|
|
@ -1,37 +1,33 @@
|
|||
import NDK, { NDKEvent, NDKPrivateKeySigner, NDKNip46Signer, NostrEvent } from '@nostr-dev-kit/ndk';
|
||||
import NDK, { NDKUser, NDKEvent, NDKPrivateKeySigner, NDKNip46Signer, NostrEvent } from '@nostr-dev-kit/ndk';
|
||||
|
||||
const remotePubkey = process.env.PUBKEY;
|
||||
|
||||
if (!remotePubkey) {
|
||||
console.log('Usage: PUBKEY=<pubkey> node src/client.js <content>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const pubkey = process.argv[2];
|
||||
const remotePubkey = process.argv[2];
|
||||
const content = process.argv[3];
|
||||
|
||||
if (!content) {
|
||||
console.log('Usage: node src/client.js <remote-pubkey> <content>');
|
||||
console.log('Usage: node src/client.js <remote-npub> <content>');
|
||||
console.log('');
|
||||
console.log(`\t<remote-pubkey>: npub that should be published as`);
|
||||
console.log(`\t<remote-npub>: npub that should be published as`);
|
||||
console.log(`\t<content>: event JSON to sign | or kind:1 content string to sign`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function createNDK(): Promise<NDK> {
|
||||
const ndk = new NDK({
|
||||
explicitRelayUrls: ['wss://nos.lol'],
|
||||
explicitRelayUrls: ['wss://nostr.vulpem.com', "wss://67aee52897df.ngrok.app"],
|
||||
});
|
||||
await ndk.connect(2000);
|
||||
ndk.pool.on('connect', () => console.log('✅ connected'));
|
||||
ndk.pool.on('relay:connect', () => console.log('✅ connected'));
|
||||
ndk.pool.on('relay:disconnect', () => console.log('❌ disconnected'));
|
||||
await ndk.connect(5000);
|
||||
|
||||
return ndk;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const remoteUser = new NDKUser({npub: remotePubkey});
|
||||
const ndk = await createNDK();
|
||||
const localSigner = new NDKPrivateKeySigner('9ec8a4b2e1fac9eae616736f718f92ed30c57fc2fff36ef8139e27c31889e327');
|
||||
const signer = new NDKNip46Signer(ndk, remotePubkey, localSigner);
|
||||
const localSigner = NDKPrivateKeySigner.generate();
|
||||
// const localSigner = new NDKPrivateKeySigner('b8baad35c387d7cf84d52e0958d9a02aff214393a85b0703de4146c7a3697bb3');
|
||||
const signer = new NDKNip46Signer(ndk, remoteUser.hexpubkey(), localSigner);
|
||||
console.log(`local pubkey`, (await signer.user()).npub);
|
||||
console.log(`remote pubkey`, remotePubkey);
|
||||
ndk.signer = signer;
|
||||
|
|
@ -40,17 +36,17 @@ async function createNDK(): Promise<NDK> {
|
|||
await signer.blockUntilReady();
|
||||
console.log(`authorized to sign as`, remotePubkey);
|
||||
|
||||
const notPabloEvent = new NDKEvent(ndk, {
|
||||
pubkey: remotePubkey,
|
||||
const event = new NDKEvent(ndk, {
|
||||
pubkey: remoteUser.hexpubkey(),
|
||||
kind: 1,
|
||||
content,
|
||||
tags: [
|
||||
['t', 'grownostr'],
|
||||
['client', 'nsecbunker-client']
|
||||
],
|
||||
} as NostrEvent);
|
||||
|
||||
await notPabloEvent.sign();
|
||||
console.log('resulting event', JSON.stringify(await notPabloEvent.toNostrEvent()));
|
||||
// await notPabloEvent.publish();
|
||||
await event.sign();
|
||||
console.log('resulting event', JSON.stringify(await event.toNostrEvent()));
|
||||
await event.publish();
|
||||
}, 2000);
|
||||
})();
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import {nip19, getPublicKey} from 'nostr-tools';
|
||||
import {nip19} from 'nostr-tools';
|
||||
import readline from 'readline';
|
||||
import { getCurrentConfig, saveCurrentConfig } from '../config/index.js';
|
||||
import { encryptNsec } from '../config/keys.js';
|
||||
|
|
@ -8,9 +8,9 @@ interface IOpts {
|
|||
name: string;
|
||||
}
|
||||
|
||||
function saveEncrypted(config: string, nsec: string, passphrase: string, name: string) {
|
||||
export async function saveEncrypted(config: string, nsec: string, passphrase: string, name: string) {
|
||||
const { iv, data } = encryptNsec(nsec, passphrase);
|
||||
const currentConfig = getCurrentConfig(config);
|
||||
const currentConfig = await getCurrentConfig(config);
|
||||
|
||||
currentConfig.keys[name] = { iv, data };
|
||||
|
||||
|
|
@ -35,8 +35,8 @@ export async function addNsec(opts: IOpts) {
|
|||
let decoded;
|
||||
try {
|
||||
decoded = nip19.decode(nsec);
|
||||
const hexpubkey = getPublicKey(decoded.data as string);
|
||||
const npub = nip19.npubEncode(hexpubkey);
|
||||
// const hexpubkey = getPublicKey(decoded.data as string);
|
||||
// const npub = nip19.npubEncode(hexpubkey);
|
||||
saveEncrypted(config, nsec, passphrase, name);
|
||||
|
||||
rl.close();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,21 @@
|
|||
import readline from 'readline';
|
||||
import fs from 'fs';
|
||||
import crypto from 'crypto';
|
||||
import { getCurrentConfig, saveCurrentConfig } from '../config/index.js';
|
||||
|
||||
export function setup(config: string) {
|
||||
export async function setup(config: string) {
|
||||
const currentConfig = await getCurrentConfig(config);
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
console.log(`You need at least one administrator to remotely control nsecBunker. This should probably be your own npub.\n`);
|
||||
|
||||
rl.question(`Enter an administrator npub: `, (npub: string) => {
|
||||
currentConfig.admin.npubs.push(npub);
|
||||
|
||||
saveCurrentConfig(config, currentConfig);
|
||||
rl.close();
|
||||
|
||||
console.log(`Administrator npub added!`);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import readline from 'readline';
|
||||
import { getCurrentConfig } from '../config/index.js';
|
||||
import { getCurrentConfig, saveCurrentConfig } from '../config/index.js';
|
||||
import { decryptNsec } from '../config/keys.js';
|
||||
import { fork } from 'child_process';
|
||||
import { resolve } from 'path';
|
||||
|
|
@ -8,10 +8,21 @@ interface IOpts {
|
|||
keys: string[];
|
||||
verbose: boolean;
|
||||
config: string;
|
||||
adminNpubs: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* This command starts the nsecbunkerd process with an (optional)
|
||||
* admin interface over websockets or redis.
|
||||
*/
|
||||
export async function start(opts: IOpts) {
|
||||
const configData = getCurrentConfig(opts.config);
|
||||
const configData = await getCurrentConfig(opts.config);
|
||||
|
||||
if (opts.adminNpubs) {
|
||||
configData.admin.npubs = opts.adminNpubs;
|
||||
}
|
||||
|
||||
await saveCurrentConfig(opts.config, configData);
|
||||
|
||||
if (opts.verbose) {
|
||||
configData.verbose = opts.verbose;
|
||||
|
|
@ -19,11 +30,7 @@ export async function start(opts: IOpts) {
|
|||
|
||||
const keys: Record<string, string> = {};
|
||||
|
||||
let keysToStart = opts.keys;
|
||||
|
||||
if (!keysToStart) {
|
||||
keysToStart = Object.keys(configData.keys);
|
||||
}
|
||||
const keysToStart = opts.keys || [];
|
||||
|
||||
for (const keyName of keysToStart) {
|
||||
const nsec = await startKey(keyName, configData.keys[keyName], opts.verbose);
|
||||
|
|
@ -33,19 +40,13 @@ export async function start(opts: IOpts) {
|
|||
}
|
||||
}
|
||||
|
||||
if (Object.keys(keys).length === 0) {
|
||||
console.log(`No keys started.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`nsecBunker starting with keys:`, Object.keys(keys).join(', '));
|
||||
|
||||
configData.keys = keys;
|
||||
|
||||
const daemonProcess = fork(resolve(__dirname, '../daemon/index.js'));
|
||||
daemonProcess.send(configData);
|
||||
|
||||
// process.exit(0);
|
||||
daemonProcess.send({
|
||||
configFile: opts.config,
|
||||
allKeys: configData.keys,
|
||||
...configData,
|
||||
keys,
|
||||
});
|
||||
}
|
||||
|
||||
interface KeyData {
|
||||
|
|
|
|||
|
|
@ -1,21 +1,34 @@
|
|||
import { randomBytes } from 'crypto';
|
||||
import { readFileSync, writeFileSync } from 'fs';
|
||||
import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
|
||||
import { IAdminOpts } from '../daemon/admin';
|
||||
|
||||
function getPassphrase(): string {
|
||||
const passwordLength = 32;
|
||||
const passwordBytes = randomBytes(passwordLength);
|
||||
return passwordBytes.toString('base64').slice(0, passwordLength);
|
||||
const generatedKey = NDKPrivateKeySigner.generate();
|
||||
|
||||
export interface IConfig {
|
||||
nostr: {
|
||||
relays: string[];
|
||||
};
|
||||
admin: IAdminOpts;
|
||||
database: string;
|
||||
logs: string;
|
||||
keys: Record<string, any>;
|
||||
verbose: boolean;
|
||||
}
|
||||
|
||||
const defaultConfig = {
|
||||
const defaultConfig: IConfig = {
|
||||
nostr: {
|
||||
relays: [
|
||||
'wss://nos.lol',
|
||||
// 'wss://relay.damus.io'
|
||||
]
|
||||
},
|
||||
remote: {
|
||||
passphrase: getPassphrase(),
|
||||
admin: {
|
||||
npubs: [],
|
||||
adminRelays: [
|
||||
"wss://nostr.vulpem.com",
|
||||
"wss://relay.nsecbunker.com"
|
||||
],
|
||||
key: generatedKey.privateKey!
|
||||
},
|
||||
database: 'sqlite://nsecbunker.db',
|
||||
logs: './nsecbunker.log',
|
||||
|
|
@ -23,17 +36,13 @@ const defaultConfig = {
|
|||
verbose: false,
|
||||
};
|
||||
|
||||
export function getCurrentConfig(config: string) {
|
||||
async function getCurrentConfig(config: string): Promise<IConfig> {
|
||||
try {
|
||||
const configFileContents = readFileSync(config, 'utf8');
|
||||
return JSON.parse(configFileContents);
|
||||
} catch (err: any) {
|
||||
if (err.code === 'ENOENT') {
|
||||
const d = defaultConfig;
|
||||
|
||||
console.log(`nsecBunker generated an admin password for you:\n\n${d.remote.passphrase}\n\n` +
|
||||
`You will need this to manage users of your keys.\n\n`);
|
||||
|
||||
await saveCurrentConfig(config, defaultConfig);
|
||||
return defaultConfig;
|
||||
} else {
|
||||
console.error(`Error reading config file: ${err.message}`);
|
||||
|
|
@ -51,3 +60,5 @@ export function saveCurrentConfig(config: string, currentConfig: any) {
|
|||
process.exit(1); // Kill the process if there is an error parsing the JSON
|
||||
}
|
||||
}
|
||||
|
||||
export {getCurrentConfig};
|
||||
|
|
@ -1,6 +1,11 @@
|
|||
import run from './run';
|
||||
import type {IOpts} from './run';
|
||||
import type {IConfig} from '../config/index';
|
||||
|
||||
process.on('message', (configData: IOpts) => {
|
||||
run(configData);
|
||||
export type DaemonConfig = IConfig & {
|
||||
configFile: string;
|
||||
allKeys: Record<string, any>;
|
||||
};
|
||||
|
||||
process.on('message', (config: DaemonConfig) => {
|
||||
run(config);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,222 +1,104 @@
|
|||
import NDK, { Nip46PermitCallback } from '@nostr-dev-kit/ndk';
|
||||
import NDK, { NDKPrivateKeySigner, Nip46PermitCallback } from '@nostr-dev-kit/ndk';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { Backend } from './backend/index.js';
|
||||
import readline from 'readline';
|
||||
import {
|
||||
checkIfPubkeyAllowed,
|
||||
allowAllRequestsFromKey,
|
||||
rejectAllRequestsFromKey
|
||||
} from './lib/acl/index.js';
|
||||
import AdminInterface from './admin/index.js';
|
||||
import { askYNquestion } from '../utils/prompts/boolean.js';
|
||||
import { IConfig } from '../config/index.js';
|
||||
import { NDKRpcRequest } from '@nostr-dev-kit/ndk';
|
||||
import prisma from '../db.js';
|
||||
import { DaemonConfig } from './index.js';
|
||||
import { decryptNsec } from '../config/keys.js';
|
||||
|
||||
export interface IOpts {
|
||||
keys: Record<string, string>;
|
||||
nostr: {
|
||||
relays: string[],
|
||||
}
|
||||
verbose: boolean;
|
||||
}
|
||||
export type Key = {
|
||||
name: string;
|
||||
npub?: string;
|
||||
};
|
||||
|
||||
export type KeyUser = {
|
||||
name: string;
|
||||
pubkey: string;
|
||||
description?: string;
|
||||
createdAt: Date;
|
||||
lastUsedAt?: Date;
|
||||
};
|
||||
|
||||
export default async function run(opts: IOpts) {
|
||||
console.log(`nsecBunker daemon starting with PID ${process.pid}...`);
|
||||
console.log(`Connecting to ${opts.nostr.relays.length} relays...`);
|
||||
function getKeys(config: DaemonConfig) {
|
||||
return async (): Promise<Key[]> => {
|
||||
let lockedKeyNames = Object.keys(config.allKeys);
|
||||
const keys: Key[] = [];
|
||||
|
||||
const ndk = new NDK({
|
||||
explicitRelayUrls: opts.nostr.relays,
|
||||
});
|
||||
await ndk.pool.on('connect', (r) => { console.log(`✅ Connected to ${r.url}`); });
|
||||
await ndk.pool.on('notice', (n, r) => { console.log(`👀 Notice from ${r.url}`, n); });
|
||||
await ndk.connect(5000);
|
||||
|
||||
setTimeout(async () => {
|
||||
const promise = [];
|
||||
|
||||
for (const [name, nsec] of Object.entries(opts.keys)) {
|
||||
const cb = callbackForKey(name);
|
||||
for (const [name, nsec] of Object.entries(config.keys)) {
|
||||
const hexpk = nip19.decode(nsec).data as string;
|
||||
const backend = new Backend(ndk, hexpk, cb);
|
||||
promise.push(backend.start());
|
||||
const user = await new NDKPrivateKeySigner(hexpk).user();
|
||||
const key = {
|
||||
name,
|
||||
npub: user.npub,
|
||||
};
|
||||
|
||||
lockedKeyNames = lockedKeyNames.filter((keyName) => keyName !== name);
|
||||
keys.push(key);
|
||||
}
|
||||
|
||||
await Promise.all(promise);
|
||||
console.log({ lockedKeyNames });
|
||||
|
||||
console.log('✅ nsecBunker ready to serve requests.');
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
async function checkIfPubkeyAllowed(keyName: string, remotePubkey: string, method: string, param?: any): Promise<boolean | undefined> {
|
||||
// find KeyUser
|
||||
const keyUser = await prisma.keyUser.findUnique({
|
||||
where: { unique_key_user: { keyName, userPubkey: remotePubkey } },
|
||||
});
|
||||
|
||||
if (!keyUser) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// find SigningCondition
|
||||
const signingConditionQuery = requestToSigningConditionQuery(method, param);
|
||||
|
||||
const explicitReject = await prisma.signingCondition.findFirst({
|
||||
where: {
|
||||
keyUserId: keyUser.id,
|
||||
method: '*',
|
||||
allowed: false,
|
||||
for (const name of lockedKeyNames) {
|
||||
keys.push({ name });
|
||||
}
|
||||
});
|
||||
|
||||
if (explicitReject) {
|
||||
console.log(`explicit reject`, explicitReject);
|
||||
return false;
|
||||
}
|
||||
|
||||
const signingCondition = await prisma.signingCondition.findFirst({
|
||||
where: {
|
||||
keyUserId: keyUser.id,
|
||||
...signingConditionQuery,
|
||||
}
|
||||
});
|
||||
|
||||
// if no SigningCondition found, return undefined
|
||||
if (!signingCondition) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const allowed = signingCondition.allowed;
|
||||
|
||||
if (allowed === true || allowed === false) {
|
||||
console.log(`found signing condition`, signingCondition);
|
||||
return allowed;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return keys;
|
||||
};
|
||||
}
|
||||
|
||||
function requestToSigningConditionQuery(method: string, param?: any) {
|
||||
const signingConditionQuery: any = { method };
|
||||
function getKeyUsers(config: IConfig) {
|
||||
return async (req: NDKRpcRequest): Promise<KeyUser[]> => {
|
||||
const keyUsers: KeyUser[] = [];
|
||||
const keyName = req.params[0];
|
||||
|
||||
switch (method) {
|
||||
case 'sign_event':
|
||||
signingConditionQuery.kind = param.kind;
|
||||
break;
|
||||
}
|
||||
|
||||
return signingConditionQuery;
|
||||
}
|
||||
|
||||
async function allowAllRequestsFromKey(remotePubkey: string, keyName: string, method: string, param?: any): Promise<void> {
|
||||
try {
|
||||
|
||||
// Upsert the KeyUser with the given remotePubkey
|
||||
const upsertedUser = await prisma.keyUser.upsert({
|
||||
where: { unique_key_user: { keyName, userPubkey: remotePubkey } },
|
||||
update: { },
|
||||
create: { keyName, userPubkey: remotePubkey },
|
||||
});
|
||||
|
||||
console.log({ upsertedUser });
|
||||
|
||||
// Create a new SigningCondition for the given KeyUser and set allowed to true
|
||||
const signingConditionQuery = requestToSigningConditionQuery(method, param);
|
||||
await prisma.signingCondition.create({
|
||||
data: {
|
||||
allowed: true,
|
||||
keyUserId: upsertedUser.id,
|
||||
...signingConditionQuery
|
||||
const users = await prisma.keyUser.findMany({
|
||||
where: {
|
||||
keyName,
|
||||
},
|
||||
include: {
|
||||
signingConditions: true,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.log('allowAllRequestsFromKey', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function rejectAllRequestsFromKey(remotePubkey: string, keyName: string): Promise<void> {
|
||||
// Upsert the KeyUser with the given remotePubkey
|
||||
const upsertedUser = await prisma.keyUser.upsert({
|
||||
where: { unique_key_user: { keyName, userPubkey: remotePubkey } },
|
||||
update: { },
|
||||
create: { keyName, userPubkey: remotePubkey },
|
||||
});
|
||||
for (const user of users) {
|
||||
const keyUser = {
|
||||
name: user.keyName,
|
||||
pubkey: user.userPubkey,
|
||||
description: user.description || undefined,
|
||||
createdAt: user.createdAt,
|
||||
lastUsedAt: user.lastUsedAt || undefined,
|
||||
signingConditions: user.signingConditions, // Include signing conditions
|
||||
};
|
||||
|
||||
console.log({ upsertedUser });
|
||||
|
||||
// Create a new SigningCondition for the given KeyUser and set allowed to false
|
||||
await prisma.signingCondition.create({
|
||||
data: {
|
||||
allowed: false,
|
||||
keyUserId: upsertedUser.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
interface IAskYNquestionOpts {
|
||||
timeoutLength?: number;
|
||||
yes: any;
|
||||
no: any;
|
||||
always?: any;
|
||||
never?: any;
|
||||
response?: any;
|
||||
timeout?: any;
|
||||
}
|
||||
|
||||
async function askYNquestion(
|
||||
question: string,
|
||||
opts: IAskYNquestionOpts
|
||||
) {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
let timeout: NodeJS.Timeout | undefined;
|
||||
|
||||
if (opts.timeoutLength) {
|
||||
timeout = setTimeout(() => {
|
||||
rl.close();
|
||||
opts.timeout && opts.timeout();
|
||||
}, opts.timeoutLength);
|
||||
}
|
||||
|
||||
const prompts = ['y', 'n'];
|
||||
|
||||
if (opts.always) prompts.push('always');
|
||||
if (opts.never) prompts.push('never');
|
||||
|
||||
question += ` (${prompts.join('/')})`;
|
||||
|
||||
rl.question(question, (answer) => {
|
||||
timeout && clearTimeout(timeout);
|
||||
|
||||
switch (answer) {
|
||||
case 'y':
|
||||
case 'Y':
|
||||
opts.yes();
|
||||
opts.response && opts.response(answer);
|
||||
break;
|
||||
case 'n':
|
||||
case 'N':
|
||||
opts.no();
|
||||
opts.response && opts.response(answer);
|
||||
break;
|
||||
case 'always':
|
||||
opts.yes();
|
||||
opts.always();
|
||||
opts.response && opts.response(answer);
|
||||
break;
|
||||
case 'never':
|
||||
opts.no();
|
||||
opts.never();
|
||||
opts.response && opts.response(answer);
|
||||
break;
|
||||
default:
|
||||
console.log('Invalid answer');
|
||||
askYNquestion(question, opts);
|
||||
break;
|
||||
keyUsers.push(keyUser);
|
||||
}
|
||||
|
||||
rl.close();
|
||||
});
|
||||
|
||||
return rl;
|
||||
return keyUsers;
|
||||
};
|
||||
}
|
||||
|
||||
let requestPermissionMutex = false;
|
||||
|
||||
async function requestPermission(keyName: string, remotePubkey: string, method: string, param?: any): Promise<boolean> {
|
||||
if (requestPermissionMutex) {
|
||||
console.log(`can't process request ${method} because signer is busy`);
|
||||
return false;
|
||||
// setTimeout(() => {
|
||||
// requestPermission(keyName, remotePubkey, method, param);
|
||||
// }, 1000);
|
||||
// return;
|
||||
}
|
||||
|
||||
requestPermissionMutex = true;
|
||||
|
||||
const npub = nip19.npubEncode(remotePubkey);
|
||||
|
||||
const promise = new Promise<boolean>((resolve, reject) => {
|
||||
|
|
@ -245,12 +127,40 @@ async function requestPermission(keyName: string, remotePubkey: string, method:
|
|||
console.log('🚫 Denying this request and all future requests from this key.');
|
||||
await rejectAllRequestsFromKey(remotePubkey, keyName);
|
||||
},
|
||||
response: () => {
|
||||
requestPermissionMutex = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
function callbackForKeyAdminInterface(keyName: string, adminInterface: AdminInterface): Nip46PermitCallback {
|
||||
return async (remotePubkey: string, method: string, param?: any): Promise<boolean> => {
|
||||
console.log(`🔑 ${keyName} is being requested to ${method} by ${nip19.npubEncode(remotePubkey)}`);
|
||||
|
||||
if (!adminInterface.requestPermission) {
|
||||
throw new Error('adminInterface.requestPermission is not defined');
|
||||
}
|
||||
|
||||
try {
|
||||
const keyAllowed = await checkIfPubkeyAllowed(keyName, remotePubkey, method, param);
|
||||
|
||||
if (keyAllowed === true || keyAllowed === false) {
|
||||
console.log(`🔎 ${nip19.npubEncode(remotePubkey)} is ${keyAllowed ? 'allowed' : 'denied'} to ${method} with key ${keyName}`);
|
||||
return keyAllowed;
|
||||
}
|
||||
|
||||
return adminInterface.requestPermission(keyName, remotePubkey, method, param);
|
||||
} catch(e) {
|
||||
console.log('callbackForKey error:', e);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
function callbackForKey(keyName: string): Nip46PermitCallback {
|
||||
return async (remotePubkey: string, method: string, param?: any): Promise<boolean> => {
|
||||
try {
|
||||
|
|
@ -270,3 +180,78 @@ function callbackForKey(keyName: string): Nip46PermitCallback {
|
|||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
export default async function run(config: DaemonConfig) {
|
||||
const daemon = new Daemon(config);
|
||||
await daemon.start();
|
||||
}
|
||||
|
||||
class Daemon {
|
||||
private config: DaemonConfig;
|
||||
private activeKeys: Record<string, any>;
|
||||
private adminInterface: AdminInterface;
|
||||
private ndk: NDK;
|
||||
|
||||
constructor(config: DaemonConfig) {
|
||||
this.config = config;
|
||||
this.activeKeys = config.keys;
|
||||
this.adminInterface = new AdminInterface(config.admin, config.configFile);
|
||||
|
||||
this.adminInterface.getKeys = getKeys(config);
|
||||
this.adminInterface.getKeyUsers = getKeyUsers(config);
|
||||
this.adminInterface.unlockKey = this.unlockKey.bind(this);
|
||||
this.adminInterface.loadNsec = this.loadNsec.bind(this);
|
||||
|
||||
this.ndk = new NDK({
|
||||
explicitRelayUrls: config.nostr.relays,
|
||||
});
|
||||
this.ndk.pool.on('connect', (r) => { console.log(`✅ Connected to ${r.url}`); });
|
||||
this.ndk.pool.on('notice', (n, r) => { console.log(`👀 Notice from ${r.url}`, n); });
|
||||
}
|
||||
|
||||
async start() {
|
||||
await this.ndk.connect(5000);
|
||||
|
||||
setTimeout(async () => {
|
||||
const promise = [];
|
||||
|
||||
for (const [name, nsec] of Object.entries(this.config.keys)) {
|
||||
promise.push(this.startKey(name, nsec));
|
||||
}
|
||||
|
||||
// await Promise.all(promise);
|
||||
|
||||
console.log('✅ nsecBunker ready to serve requests.');
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to start a key's backend
|
||||
* @param name Name of the key
|
||||
* @param nsec NSec of the key
|
||||
*/
|
||||
async startKey(name: string, nsec: string) {
|
||||
const cb = callbackForKeyAdminInterface(name, this.adminInterface);
|
||||
const hexpk = nip19.decode(nsec).data as string;
|
||||
const backend = new Backend(this.ndk, hexpk, cb);
|
||||
await backend.start();
|
||||
}
|
||||
|
||||
async unlockKey(keyName: string, passphrase: string): Promise<boolean> {
|
||||
const keyData = this.config.allKeys[keyName];
|
||||
const { iv, data } = keyData;
|
||||
|
||||
const nsec = decryptNsec(iv, data, passphrase);
|
||||
this.activeKeys[keyName] = nsec;
|
||||
|
||||
this.startKey(keyName, nsec);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
loadNsec(keyName: string, nsec: string) {
|
||||
this.activeKeys[keyName] = nsec;
|
||||
|
||||
this.startKey(keyName, nsec);
|
||||
}
|
||||
}
|
||||
60
src/index.ts
60
src/index.ts
|
|
@ -7,14 +7,14 @@ import { setup } from './commands/setup.js';
|
|||
import { addNsec } from './commands/add.js';
|
||||
import { start } from './commands/start.js';
|
||||
|
||||
console.log(`nsecBunker licensed under CC BY-NC-ND 3.0:`);
|
||||
console.log(`nsecBunker licensed under CC BY-NC-ND 4.0:`);
|
||||
console.log(`free to use for non-commercial use`);
|
||||
console.log(`Copyright by pablof7z <npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft> 2023`);
|
||||
console.log(`Copyright by pablof7z <pablo@f7z.io> 2023`);
|
||||
console.log(`Contact for licensing`);
|
||||
console.log(``);
|
||||
|
||||
yargs(hideBin(process.argv))
|
||||
.command('setup', 'Setup nsecBunker', () => {}, (argv) => {
|
||||
.command('setup', 'Setup nsecBunker', {}, (argv) => {
|
||||
setup(argv.config as string);
|
||||
})
|
||||
|
||||
|
|
@ -30,13 +30,19 @@ yargs(hideBin(process.argv))
|
|||
.option('key <name>', {
|
||||
type: 'string',
|
||||
description: 'Name of key to enable',
|
||||
})
|
||||
.array('admin')
|
||||
.option('admin <npub>', {
|
||||
alias: 'a',
|
||||
type: 'string',
|
||||
description: 'Admin npub',
|
||||
});
|
||||
}, (argv) => {
|
||||
start({
|
||||
keys: argv.key as string[],
|
||||
verbose: argv.verbose as boolean,
|
||||
config: argv.config as string,
|
||||
|
||||
adminNpubs: argv.admin as string[],
|
||||
});
|
||||
})
|
||||
|
||||
|
|
@ -65,49 +71,3 @@ yargs(hideBin(process.argv))
|
|||
})
|
||||
.demandCommand(1)
|
||||
.parse();
|
||||
|
||||
|
||||
|
||||
// async function cb(pubkey: string, method: string, param?: any): Promise<boolean> {
|
||||
// // check if pubkey is in allowed list file
|
||||
// // if not, return false
|
||||
// // if yes, return true
|
||||
|
||||
// // read file allowed.json
|
||||
// try {
|
||||
// const data = fs.readFileSync('config.json', 'utf8');
|
||||
// const config = JSON.parse(data);
|
||||
// const allowedPubkeys = config.allowedPubkeys || {};
|
||||
// console.log('allowedPubkeys', allowedPubkeys, allowedPubkeys[pubkey]);
|
||||
// if (allowedPubkeys[pubkey] && allowedPubkeys[pubkey].methods[method]) {
|
||||
// console.log(`✅ ${pubkey} is allowed to ${method}`);
|
||||
// return true;
|
||||
// }
|
||||
// } catch(e) {
|
||||
// console.log('Error:', e);
|
||||
// }
|
||||
|
||||
// console.log(`🚫 ${pubkey} is not allowed to ${method}`);
|
||||
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// (async () => {
|
||||
// const ndk = await createNDK();
|
||||
|
||||
// console.log(`NSECBUNKER BOOTING UP`);
|
||||
|
||||
// if (!process.env.PKEY) {
|
||||
// console.error('PKEY not set');
|
||||
// process.exit(1);
|
||||
// }
|
||||
|
||||
// const backend = new Backend(ndk, process.env.PKEY, cb);
|
||||
// await backend.start();
|
||||
|
||||
// const npub = backend.localUser?.npub;
|
||||
// const hexpubkey = backend.localUser?.hexpubkey();
|
||||
// console.log(`NPUB: ${npub}`);
|
||||
// console.log(`PUBK: ${hexpubkey}`);
|
||||
// })();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue