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