This commit is contained in:
pablof7z 2023-05-31 20:26:29 +02:00
commit 74cd9715ac
12 changed files with 3353 additions and 438 deletions

View file

@ -1,15 +1,78 @@
# nsecbunker # nsecbunkerd
Daemon to remotely sign nostr events using keys. 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 # Authors
@ -18,5 +81,5 @@ nsecbunker -
# License # License
CC BY-NC-ND 3.0 CC BY-NC-ND 4.0
Contact @pablof7z for licensing. Contact @pablof7z for licensing.

2977
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,14 +1,18 @@
{ {
"name": "nsecbunker", "name": "nsecbunkerd",
"version": "0.1.0", "version": "0.5.8",
"description": "nsecbunker", "description": "nsecbunker daemon",
"main": "dist/index.js", "main": "dist/index.js",
"bin": { "bin": {
"nsecbunkerd": "dist/index.js", "nsecbunkerd": "dist/index.js",
"nsecbunker-client": "dist/client.js" "nsecbunker-client": "dist/client.js"
}, },
"files": [ "files": [
"dist" "dist",
"scripts/start.js",
"prisma/schema.prisma",
"LICENSE",
"README.md"
], ],
"repository": { "repository": {
"type": "git", "type": "git",
@ -16,7 +20,10 @@
}, },
"scripts": { "scripts": {
"build": "tsc", "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", "nsecbunkerd": "node dist/index.js",
"nsecbunker-client": "node dist/client.js" "nsecbunker-client": "node dist/client.js"
}, },
@ -24,16 +31,16 @@
"nostr" "nostr"
], ],
"author": "pablof7z", "author": "pablof7z",
"license": "MIT", "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",
"@prisma/client": "4.13.0", "@nostr-dev-kit/ndk": "^0.3.26",
"@prisma/client": "^4.14.1",
"@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",
"@typescript-eslint/parser": "^5.57.0", "@typescript-eslint/parser": "^5.57.0",
"crypto": "^1.0.1",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"eslint": "^8.37.0", "eslint": "^8.37.0",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.8.0",
@ -45,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.13.0", "prisma": "^4.14.1",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.0.3" "typescript": "^5.0.3"
} }

View file

@ -7,15 +7,19 @@ generator client {
datasource db { datasource db {
provider = "sqlite" provider = "sqlite"
url = env("DATABASE_URL") url = "file:./nsecbunker.db"
} }
model KeyUser { model KeyUser {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
keyName String keyName String
userPubkey String userPubkey String
description String?
signingConditions SigningCondition[] signingConditions SigningCondition[]
logs Log[] logs Log[]
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
lastUsedAt DateTime?
@@unique([keyName, userPubkey], name: "unique_key_user") @@unique([keyName, userPubkey], name: "unique_key_user")
} }
@ -24,7 +28,7 @@ model SigningCondition {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
method String? method String?
kind Int? kind String?
content String? content String?
keyUserKeyName String? keyUserKeyName String?
allowed Boolean? allowed Boolean?

View file

@ -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; const remotePubkey = process.argv[2];
if (!remotePubkey) {
console.log('Usage: PUBKEY=<pubkey> node src/client.js <content>');
process.exit(1);
}
const pubkey = process.argv[2];
const content = process.argv[3]; const content = process.argv[3];
if (!content) { 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('');
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`); console.log(`\t<content>: event JSON to sign | or kind:1 content string to sign`);
process.exit(1); process.exit(1);
} }
async function createNDK(): Promise<NDK> { async function createNDK(): Promise<NDK> {
const ndk = new 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('relay:connect', () => console.log('✅ connected'));
ndk.pool.on('connect', () => console.log('✅ connected')); ndk.pool.on('relay:disconnect', () => console.log('❌ disconnected'));
await ndk.connect(5000);
return ndk; return ndk;
} }
(async () => { (async () => {
const remoteUser = new NDKUser({npub: remotePubkey});
const ndk = await createNDK(); const ndk = await createNDK();
const localSigner = new NDKPrivateKeySigner('9ec8a4b2e1fac9eae616736f718f92ed30c57fc2fff36ef8139e27c31889e327'); const localSigner = NDKPrivateKeySigner.generate();
const signer = new NDKNip46Signer(ndk, remotePubkey, localSigner); // const localSigner = new NDKPrivateKeySigner('b8baad35c387d7cf84d52e0958d9a02aff214393a85b0703de4146c7a3697bb3');
const signer = new NDKNip46Signer(ndk, remoteUser.hexpubkey(), localSigner);
console.log(`local pubkey`, (await signer.user()).npub); console.log(`local pubkey`, (await signer.user()).npub);
console.log(`remote pubkey`, remotePubkey); console.log(`remote pubkey`, remotePubkey);
ndk.signer = signer; ndk.signer = signer;
@ -40,17 +36,17 @@ async function createNDK(): Promise<NDK> {
await signer.blockUntilReady(); await signer.blockUntilReady();
console.log(`authorized to sign as`, remotePubkey); console.log(`authorized to sign as`, remotePubkey);
const notPabloEvent = new NDKEvent(ndk, { const event = new NDKEvent(ndk, {
pubkey: remotePubkey, pubkey: remoteUser.hexpubkey(),
kind: 1, kind: 1,
content, content,
tags: [ tags: [
['t', 'grownostr'], ['client', 'nsecbunker-client']
], ],
} as NostrEvent); } as NostrEvent);
await notPabloEvent.sign(); await event.sign();
console.log('resulting event', JSON.stringify(await notPabloEvent.toNostrEvent())); console.log('resulting event', JSON.stringify(await event.toNostrEvent()));
// await notPabloEvent.publish(); await event.publish();
}, 2000); }, 2000);
})(); })();

View file

@ -1,4 +1,4 @@
import {nip19, getPublicKey} from 'nostr-tools'; import {nip19} from 'nostr-tools';
import readline from 'readline'; import readline from 'readline';
import { getCurrentConfig, saveCurrentConfig } from '../config/index.js'; import { getCurrentConfig, saveCurrentConfig } from '../config/index.js';
import { encryptNsec } from '../config/keys.js'; import { encryptNsec } from '../config/keys.js';
@ -8,9 +8,9 @@ interface IOpts {
name: string; 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 { iv, data } = encryptNsec(nsec, passphrase);
const currentConfig = getCurrentConfig(config); const currentConfig = await getCurrentConfig(config);
currentConfig.keys[name] = { iv, data }; currentConfig.keys[name] = { iv, data };
@ -35,8 +35,8 @@ export async function addNsec(opts: IOpts) {
let decoded; let decoded;
try { try {
decoded = nip19.decode(nsec); decoded = nip19.decode(nsec);
const hexpubkey = getPublicKey(decoded.data as string); // const hexpubkey = getPublicKey(decoded.data as string);
const npub = nip19.npubEncode(hexpubkey); // const npub = nip19.npubEncode(hexpubkey);
saveEncrypted(config, nsec, passphrase, name); saveEncrypted(config, nsec, passphrase, name);
rl.close(); rl.close();

View file

@ -1,7 +1,21 @@
import readline from 'readline'; import readline from 'readline';
import fs from 'fs'; import { getCurrentConfig, saveCurrentConfig } from '../config/index.js';
import crypto from 'crypto';
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!`);
});
} }

View file

@ -1,5 +1,5 @@
import readline from 'readline'; import readline from 'readline';
import { getCurrentConfig } from '../config/index.js'; import { getCurrentConfig, saveCurrentConfig } from '../config/index.js';
import { decryptNsec } from '../config/keys.js'; import { decryptNsec } from '../config/keys.js';
import { fork } from 'child_process'; import { fork } from 'child_process';
import { resolve } from 'path'; import { resolve } from 'path';
@ -8,10 +8,21 @@ interface IOpts {
keys: string[]; keys: string[];
verbose: boolean; verbose: boolean;
config: string; 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) { 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) { if (opts.verbose) {
configData.verbose = opts.verbose; configData.verbose = opts.verbose;
@ -19,11 +30,7 @@ export async function start(opts: IOpts) {
const keys: Record<string, string> = {}; const keys: Record<string, string> = {};
let keysToStart = opts.keys; const keysToStart = opts.keys || [];
if (!keysToStart) {
keysToStart = Object.keys(configData.keys);
}
for (const keyName of keysToStart) { for (const keyName of keysToStart) {
const nsec = await startKey(keyName, configData.keys[keyName], opts.verbose); 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')); const daemonProcess = fork(resolve(__dirname, '../daemon/index.js'));
daemonProcess.send(configData); daemonProcess.send({
configFile: opts.config,
// process.exit(0); allKeys: configData.keys,
...configData,
keys,
});
} }
interface KeyData { interface KeyData {

View file

@ -1,21 +1,34 @@
import { randomBytes } from 'crypto';
import { readFileSync, writeFileSync } from 'fs'; import { readFileSync, writeFileSync } from 'fs';
import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
import { IAdminOpts } from '../daemon/admin';
function getPassphrase(): string { const generatedKey = NDKPrivateKeySigner.generate();
const passwordLength = 32;
const passwordBytes = randomBytes(passwordLength); export interface IConfig {
return passwordBytes.toString('base64').slice(0, passwordLength); nostr: {
relays: string[];
};
admin: IAdminOpts;
database: string;
logs: string;
keys: Record<string, any>;
verbose: boolean;
} }
const defaultConfig = { const defaultConfig: IConfig = {
nostr: { nostr: {
relays: [ relays: [
'wss://nos.lol', 'wss://nos.lol',
// 'wss://relay.damus.io' // 'wss://relay.damus.io'
] ]
}, },
remote: { admin: {
passphrase: getPassphrase(), npubs: [],
adminRelays: [
"wss://nostr.vulpem.com",
"wss://relay.nsecbunker.com"
],
key: generatedKey.privateKey!
}, },
database: 'sqlite://nsecbunker.db', database: 'sqlite://nsecbunker.db',
logs: './nsecbunker.log', logs: './nsecbunker.log',
@ -23,17 +36,13 @@ const defaultConfig = {
verbose: false, verbose: false,
}; };
export function getCurrentConfig(config: string) { async function getCurrentConfig(config: string): Promise<IConfig> {
try { try {
const configFileContents = readFileSync(config, 'utf8'); const configFileContents = readFileSync(config, 'utf8');
return JSON.parse(configFileContents); return JSON.parse(configFileContents);
} catch (err: any) { } catch (err: any) {
if (err.code === 'ENOENT') { if (err.code === 'ENOENT') {
const d = defaultConfig; await saveCurrentConfig(config, 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`);
return defaultConfig; return defaultConfig;
} else { } else {
console.error(`Error reading config file: ${err.message}`); 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 process.exit(1); // Kill the process if there is an error parsing the JSON
} }
} }
export {getCurrentConfig};

View file

@ -1,6 +1,11 @@
import run from './run'; import run from './run';
import type {IOpts} from './run'; import type {IConfig} from '../config/index';
process.on('message', (configData: IOpts) => { export type DaemonConfig = IConfig & {
run(configData); configFile: string;
allKeys: Record<string, any>;
};
process.on('message', (config: DaemonConfig) => {
run(config);
}); });

View file

@ -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 { nip19 } from 'nostr-tools';
import { Backend } from './backend/index.js'; 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 prisma from '../db.js';
import { DaemonConfig } from './index.js';
import { decryptNsec } from '../config/keys.js';
export interface IOpts { export type Key = {
keys: Record<string, string>; name: string;
nostr: { npub?: string;
relays: string[], };
}
verbose: boolean;
}
export type KeyUser = {
name: string;
pubkey: string;
description?: string;
createdAt: Date;
lastUsedAt?: Date;
};
export default async function run(opts: IOpts) { function getKeys(config: DaemonConfig) {
console.log(`nsecBunker daemon starting with PID ${process.pid}...`); return async (): Promise<Key[]> => {
console.log(`Connecting to ${opts.nostr.relays.length} relays...`); let lockedKeyNames = Object.keys(config.allKeys);
const keys: Key[] = [];
const ndk = new NDK({ for (const [name, nsec] of Object.entries(config.keys)) {
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);
const hexpk = nip19.decode(nsec).data as string; const hexpk = nip19.decode(nsec).data as string;
const backend = new Backend(ndk, hexpk, cb); const user = await new NDKPrivateKeySigner(hexpk).user();
promise.push(backend.start()); 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.'); for (const name of lockedKeyNames) {
}, 1000); keys.push({ name });
} }
async function checkIfPubkeyAllowed(keyName: string, remotePubkey: string, method: string, param?: any): Promise<boolean | undefined> { return keys;
// find KeyUser };
const keyUser = await prisma.keyUser.findUnique({
where: { unique_key_user: { keyName, userPubkey: remotePubkey } },
});
if (!keyUser) {
return undefined;
} }
// find SigningCondition function getKeyUsers(config: IConfig) {
const signingConditionQuery = requestToSigningConditionQuery(method, param); return async (req: NDKRpcRequest): Promise<KeyUser[]> => {
const keyUsers: KeyUser[] = [];
const keyName = req.params[0];
const explicitReject = await prisma.signingCondition.findFirst({ const users = await prisma.keyUser.findMany({
where: { where: {
keyUserId: keyUser.id, keyName,
method: '*', },
allowed: false, include: {
} signingConditions: true,
});
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;
}
function requestToSigningConditionQuery(method: string, param?: any) {
const signingConditionQuery: any = { method };
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
}, },
}); });
} catch (e) {
console.log('allowAllRequestsFromKey', e); 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
};
keyUsers.push(keyUser);
} }
async function rejectAllRequestsFromKey(remotePubkey: string, keyName: string): Promise<void> { return keyUsers;
// 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 false
await prisma.signingCondition.create({
data: {
allowed: false,
keyUserId: upsertedUser.id,
},
});
} }
let requestPermissionMutex = false;
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;
}
rl.close();
});
return rl;
}
async function requestPermission(keyName: string, remotePubkey: string, method: string, param?: any): Promise<boolean> { 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 npub = nip19.npubEncode(remotePubkey);
const promise = new Promise<boolean>((resolve, reject) => { 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.'); console.log('🚫 Denying this request and all future requests from this key.');
await rejectAllRequestsFromKey(remotePubkey, keyName); await rejectAllRequestsFromKey(remotePubkey, keyName);
}, },
response: () => {
requestPermissionMutex = false;
}
}); });
}); });
return promise; 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 { function callbackForKey(keyName: string): Nip46PermitCallback {
return async (remotePubkey: string, method: string, param?: any): Promise<boolean> => { return async (remotePubkey: string, method: string, param?: any): Promise<boolean> => {
try { try {
@ -270,3 +180,78 @@ function callbackForKey(keyName: string): Nip46PermitCallback {
return false; 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);
}
}

View file

@ -7,14 +7,14 @@ import { setup } from './commands/setup.js';
import { addNsec } from './commands/add.js'; import { addNsec } from './commands/add.js';
import { start } from './commands/start.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(`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(`Contact for licensing`);
console.log(``); console.log(``);
yargs(hideBin(process.argv)) yargs(hideBin(process.argv))
.command('setup', 'Setup nsecBunker', () => {}, (argv) => { .command('setup', 'Setup nsecBunker', {}, (argv) => {
setup(argv.config as string); setup(argv.config as string);
}) })
@ -30,13 +30,19 @@ yargs(hideBin(process.argv))
.option('key <name>', { .option('key <name>', {
type: 'string', type: 'string',
description: 'Name of key to enable', description: 'Name of key to enable',
})
.array('admin')
.option('admin <npub>', {
alias: 'a',
type: 'string',
description: 'Admin npub',
}); });
}, (argv) => { }, (argv) => {
start({ start({
keys: argv.key as string[], keys: argv.key as string[],
verbose: argv.verbose as boolean, verbose: argv.verbose as boolean,
config: argv.config as string, config: argv.config as string,
adminNpubs: argv.admin as string[],
}); });
}) })
@ -65,49 +71,3 @@ yargs(hideBin(process.argv))
}) })
.demandCommand(1) .demandCommand(1)
.parse(); .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}`);
// })();