#!/usr/bin/env node /** * demo_javascript.js — comprehensive JavaScript demo for connecting to a * running n_signer via Qubes qrexec and performing all three core operations: * * 1. get_public_key — retrieve a Nostr public key by nostr_index * 2. sign_event — sign a Nostr event (kind 1 text note) * 3. nip44_encrypt — encrypt a message to a peer (and decrypt it back) * * Uses qrexec-client-vm (Qubes OS inter-qube IPC). No auth envelope needed — * identity comes from QREXEC_REMOTE_DOMAIN on the server side. * * Prerequisites: * - n_signer running in the target qube with --bridge-source-trusted * - qubes.NsignerRpc service installed in the target qube * - dom0 qrexec policy allowing this qube to call the service * - nostr-tools and @noble/secp256k1 npm packages installed * * Install dependencies (from n_signer repo root): * npm install nostr-tools @noble/secp256k1 * * Usage: * node client/demo_javascript.js [nostr_index] * node client/demo_javascript.js nostr_signer 1 * * If no nostr_index is given, defaults to 0. */ const { spawn } = require("child_process"); const crypto = require("crypto"); const secp = require("@noble/secp256k1"); const { nip19 } = require("nostr-tools"); // @noble/secp256k1 v3 requires sync sha256/hmacSha256 secp.hashes.sha256 = (msg) => new Uint8Array(crypto.createHash("sha256").update(msg).digest()); secp.hashes.hmacSha256 = (key, msg) => new Uint8Array(crypto.createHmac("sha256", key).update(msg).digest()); /** * Call n_signer via qrexec. Sends one framed JSON-RPC request, receives one * framed response. Each call spawns a fresh qrexec-client-vm process. * * Framing: 4-byte big-endian length prefix + JSON payload. * No auth envelope needed for qrexec (identity from QREXEC_REMOTE_DOMAIN). */ function callNsigner(targetQube, request) { return new Promise((resolve, reject) => { const payload = Buffer.from(JSON.stringify(request), "utf8"); const header = Buffer.alloc(4); header.writeUInt32BE(payload.length, 0); const framed = Buffer.concat([header, payload]); const proc = spawn("qrexec-client-vm", [targetQube, "qubes.NsignerRpc"], { stdio: ["pipe", "pipe", "pipe"], }); const stdoutChunks = []; const stderrChunks = []; proc.stdout.on("data", (chunk) => stdoutChunks.push(chunk)); proc.stderr.on("data", (chunk) => stderrChunks.push(chunk)); proc.on("error", (err) => { reject(new Error(`failed to spawn qrexec-client-vm: ${err.message}`)); }); proc.on("close", (code) => { if (code !== 0) { const stderr = Buffer.concat(stderrChunks).toString("utf8"); reject(new Error(`qrexec-client-vm exited with code ${code}: ${stderr.trim()}`)); return; } const buf = Buffer.concat(stdoutChunks); if (buf.length < 4) { reject(new Error("short response (missing frame header)")); return; } const len = buf.readUInt32BE(0); const body = buf.subarray(4, 4 + len); if (body.length !== len) { reject(new Error(`short response payload: expected ${len}, got ${body.length}`)); return; } try { resolve(JSON.parse(body.toString("utf8"))); } catch (e) { reject(new Error(`failed to parse response: ${e.message}`)); } }); proc.stdin.write(framed); proc.stdin.end(); }); } /** * Demo 1: Get a public key by nostr_index. */ async function demoGetPublicKey(targetQube, nostrIndex) { console.log(`\n=== Demo 1: get_public_key (nostr_index=${nostrIndex}) ===`); const response = await callNsigner(targetQube, { id: "1", method: "get_public_key", params: [{ nostr_index: nostrIndex }], }); if (response.error) { throw new Error(`get_public_key failed: ${JSON.stringify(response.error)}`); } const pubkeyHex = response.result; const npub = nip19.npubEncode(pubkeyHex); console.log(` pubkey hex: ${pubkeyHex}`); console.log(` npub: ${npub}`); return pubkeyHex; } /** * Demo 2: Sign a Nostr event (kind 1 text note). */ async function demoSignEvent(targetQube, nostrIndex, pubkeyHex) { console.log("\n=== Demo 2: sign_event (kind 1 text note) ==="); const unsignedEvent = { kind: 1, content: "Hello from n_signer JavaScript demo!", created_at: Math.floor(Date.now() / 1000), tags: [], pubkey: pubkeyHex, }; console.log(" Unsigned event:"); console.log(` ${JSON.stringify(unsignedEvent)}`); const response = await callNsigner(targetQube, { id: "2", method: "sign_event", params: [JSON.stringify(unsignedEvent), { nostr_index: nostrIndex }], }); if (response.error) { throw new Error(`sign_event failed: ${JSON.stringify(response.error)}`); } const signedEvent = JSON.parse(response.result); console.log(" Signed event:"); console.log(` ${JSON.stringify(signedEvent)}`); console.log(` event id: ${signedEvent.id}`); console.log(` signature: ${signedEvent.sig}`); return signedEvent; } /** * Demo 3: NIP-44 encrypt and decrypt. * Encrypts a message to ourselves (using our own pubkey as the peer), * then decrypts it to verify round-trip. */ async function demoNip44(targetQube, nostrIndex, pubkeyHex) { const plaintext = "Secret message from n_signer JavaScript demo!"; console.log("\n=== Demo 3: nip44_encrypt / nip44_decrypt ==="); console.log(` plaintext: "${plaintext}"`); console.log(` peer pubkey: ${pubkeyHex} (self)`); // Encrypt const encResponse = await callNsigner(targetQube, { id: "3", method: "nip44_encrypt", params: [pubkeyHex, plaintext, { nostr_index: nostrIndex }], }); if (encResponse.error) { throw new Error(`nip44_encrypt failed: ${JSON.stringify(encResponse.error)}`); } const ciphertext = encResponse.result; console.log(` ciphertext: ${ciphertext}`); // Decrypt const decResponse = await callNsigner(targetQube, { id: "4", method: "nip44_decrypt", params: [pubkeyHex, ciphertext, { nostr_index: nostrIndex }], }); if (decResponse.error) { throw new Error(`nip44_decrypt failed: ${JSON.stringify(decResponse.error)}`); } const decrypted = decResponse.result; console.log(` decrypted: "${decrypted}"`); if (plaintext === decrypted) { console.log(" ✓ Round-trip verified: plaintext matches decrypted"); } else { throw new Error("Round-trip FAILED: plaintext does not match decrypted"); } } async function main() { const targetQube = process.argv[2] || "nostr_signer"; const nostrIndex = parseInt(process.argv[3] || "0", 10); console.log("=== n_signer JavaScript Demo ==="); console.log(`Target qube: ${targetQube}`); console.log(`Service: qubes.NsignerRpc`); console.log(`nostr_index: ${nostrIndex}`); console.log("\nConnecting to n_signer via qrexec..."); try { const pubkeyHex = await demoGetPublicKey(targetQube, nostrIndex); await demoSignEvent(targetQube, nostrIndex, pubkeyHex); await demoNip44(targetQube, nostrIndex, pubkeyHex); console.log("\n=== Summary ==="); console.log("All demos completed successfully."); } catch (e) { console.error("\n=== Summary ==="); console.error(`Demo failed: ${e.message}`); process.exit(1); } } main();