From 44c63eca0f87ecfa69e11ce9e24afc4bf04ab402 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Tue, 18 Aug 2026 20:14:43 +0200 Subject: [PATCH] Hermes: wire NIP-17 messaging into Theseus, provision chipnet hermes.bch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Hermes/proof/: standalone keystone — BIP-39→Nostr, NIP-17 wrap/unwrap, name-addressed send/receive verified end-to-end via public relay - Argus/src/lib/hermes-derive.js: HKDF(silentmode/messenger/0) using libauth so the registrar can compute np without pulling nostr-tools into Argus - Argus/src/register-hermes-chipnet.mjs: idempotent REG/UPD script; publishes np (Nostr pubkey) and nr (relay) records for a chipnet name - TheseusNavigator/lib/hermes.js: same derivation + wrap/unwrap + record parsing, canonical for Theseus; guarded by lib/package.json type:module - TheseusNavigator/messages.html + messages-preload.js: Messages panel UI (identity from mnemonic, live inbox, compose by .bch name), .bch suffix stripped from displayed names - TheseusNavigator/main.js: HERMES_MOD + loadHermesLib next to VAULT_MOD; ipc handlers (hermes-status/init/close/inbox/send/open), per-relay subscription with auto-reconnect, status pushed on WS open/close, reverse-resolve pk -> .bch name via cached BNS index, Ctrl+Shift+M shortcut via web-contents-created (works from any tab) - Chipnet hermes.bch registered with np=f2c92519...67f4 nr=wss://nos.lol (txid 7fc6de4544c13b782f163fdb892ea6749785886d05a336282fa659d722c7e92b); end-to-end verified in Theseus --- src/lib/hermes-derive.js | 42 +++++++++++++++++++++++++++++++++ src/register-hermes-chipnet.mjs | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 src/lib/hermes-derive.js create mode 100644 src/register-hermes-chipnet.mjs diff --git a/src/lib/hermes-derive.js b/src/lib/hermes-derive.js new file mode 100644 index 0000000..345374d --- /dev/null +++ b/src/lib/hermes-derive.js @@ -0,0 +1,42 @@ +// Self-contained Nostr-key derivation from a BIP-39 mnemonic. +// +// This mirrors TheseusNavigator/lib/hermes.js's nostrKeyFromMnemonic — same +// HKDF context (info="silentmode/messenger/0"), same 32-byte output — but uses +// libauth for the secp256k1 pubkey so it can run alongside the registrar +// (Argus) without pulling in nostr-tools. Keep the derivation constants in +// lockstep with TheseusNavigator/lib/hermes.js. + +import { webcrypto as wc } from "node:crypto"; +import { secp256k1, binToHex, hexToBin } from "@bitauth/libauth"; + +const enc = new TextEncoder(); + +export async function bip39ToSeed(mnemonic, passphrase = "") { + const km = await wc.subtle.importKey("raw", + enc.encode(String(mnemonic).normalize("NFKD").trim()), + "PBKDF2", false, ["deriveBits"]); + const salt = enc.encode(("mnemonic" + String(passphrase || "")).normalize("NFKD")); + return new Uint8Array(await wc.subtle.deriveBits( + { name: "PBKDF2", salt, iterations: 2048, hash: "SHA-512" }, km, 512)); +} + +export async function seedToPurposeRoot(seedBytes, purpose) { + const key = await wc.subtle.importKey("raw", seedBytes, "HKDF", false, ["deriveBits"]); + const info = enc.encode(`silentmode/${purpose}`); + return new Uint8Array(await wc.subtle.deriveBits( + { name: "HKDF", hash: "SHA-256", salt: new Uint8Array(0), info }, key, 256)); +} + +/** Mnemonic → { skHex, pkHex } where pkHex is the 32-byte x-only Nostr pubkey. + * For a given mnemonic this must equal TheseusNavigator/lib/hermes.js's + * nostrKeyFromMnemonic result — verified via Hermes/proof cross-check. */ +export async function nostrKeyFromMnemonic(mnemonic, passphrase = "") { + const seed = await bip39ToSeed(mnemonic, passphrase); + const sk = await seedToPurposeRoot(seed, "messenger/0"); + const compressed = secp256k1.derivePublicKeyCompressed(sk); + if (typeof compressed === "string") throw new Error("secp256k1: " + compressed); + // Compressed pubkey is 33 bytes: [parity(0x02|0x03), x(32 bytes)]. Nostr's + // x-only is just the x coordinate. + const xonly = compressed.slice(1); + return { skHex: binToHex(sk), pkHex: binToHex(xonly) }; +} diff --git a/src/register-hermes-chipnet.mjs b/src/register-hermes-chipnet.mjs new file mode 100644 index 0000000..02fe8a1 --- /dev/null +++ b/src/register-hermes-chipnet.mjs @@ -0,0 +1,42 @@ +// One-off: register (or update) a chipnet .bch name whose `np` record binds +// the name to the Nostr identity derived from the same wallet seed. Publishes +// `nr` too so send-by-name has a relay to publish to. +// +// node src/register-hermes-chipnet.mjs # defaults: hermes / wss://nos.lol +// node src/register-hermes-chipnet.mjs hermes-test wss://nos.lol,wss://relay.damus.io +// +// If the name is already registered it emits a UPD instead of a REG so it can +// be re-run safely to rotate the np or nr. + +import { loadWallet } from "./lib/wallet.js"; +import { buildIndex, registerName, updateName } from "./lib/bns.js"; +import { nostrKeyFromMnemonic } from "./lib/hermes-derive.js"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { WALLET_FILE } from "./config.js"; + +const [nameArg, nrArg] = process.argv.slice(2); +const name = (nameArg || "hermes").toLowerCase(); +const nr = nrArg || "wss://nos.lol"; + +const walletStore = JSON.parse(readFileSync(fileURLToPath(WALLET_FILE), "utf8")); +const mnemonic = walletStore.main.seed; + +const { pkHex } = await nostrKeyFromMnemonic(mnemonic); +console.log(`wallet nostr pubkey (np): ${pkHex}`); +console.log(`relays (nr): ${nr}`); +console.log(`target name: ${name}.bch`); + +const wallet = await loadWallet(); +console.log(`from wallet: ${wallet.cashaddr}`); + +const idx = await buildIndex(wallet.provider); +const exists = idx.has(name + ".bch") || idx.has(name); + +const records = { np: pkHex, nr }; +console.log(`operation: ${exists ? "UPD" : "REG"}`); + +const fn = exists ? updateName : registerName; +const res = await fn(wallet, name, records); +console.log("result:", JSON.stringify(res, null, 2)); +process.exit(0);