UNPKG

nostr-nsec-seedphrase

Version:

A comprehensive TypeScript library for Nostr key management with BIP39 seed phrases, supporting both ESM and CommonJS. Implements NIP-01, NIP-06, NIP-19, and NIP-26 with key generation, event signing, bech32 encoding/decoding, and secure cryptographic ope

654 lines (653 loc) 25.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.nip19 = void 0; exports.generateSeedPhrase = generateSeedPhrase; exports.getEntropyFromSeedPhrase = getEntropyFromSeedPhrase; exports.validateSeedPhrase = validateSeedPhrase; exports.seedPhraseToKeyPair = seedPhraseToKeyPair; exports.generateKeyPairWithSeed = generateKeyPairWithSeed; exports.fromHex = fromHex; exports.getPublicKey = getPublicKey; exports.signEvent = signEvent; exports.verifyEvent = verifyEvent; exports.configureHMAC = configureHMAC; exports.createEvent = createEvent; exports.seedPhraseToPrivateKey = seedPhraseToPrivateKey; exports.privateKeyToNsec = privateKeyToNsec; exports.privateKeyToNpub = privateKeyToNpub; exports.nsecToHex = nsecToHex; exports.npubToHex = npubToHex; exports.hexToNpub = hexToNpub; exports.hexToNsec = hexToNsec; exports.signMessage = signMessage; exports.verifySignature = verifySignature; exports.nsecToPrivateKey = nsecToPrivateKey; exports.toNcryptsec = toNcryptsec; exports.fromNcryptsec = fromNcryptsec; const bip39_1 = require("bip39"); const secp256k1_js_1 = require("@noble/curves/secp256k1.js"); const utils_js_1 = require("@noble/hashes/utils.js"); const sha2_js_1 = require("@noble/hashes/sha2.js"); const hmac_js_1 = require("@noble/hashes/hmac.js"); const bech32_1 = require("bech32"); const nostr_crypto_utils_1 = require("nostr-crypto-utils"); const logger_1 = require("./utils/logger"); /** * Generates a new BIP39 seed phrase * @returns {string} A random 12-word BIP39 mnemonic seed phrase * @example * const seedPhrase = generateSeedPhrase(); * console.log(seedPhrase); // "witch collapse practice feed shame open despair creek road again ice least" */ function generateSeedPhrase() { return (0, bip39_1.generateMnemonic)(); } /** * Converts a BIP39 seed phrase to its entropy value * @param {string} seedPhrase - The BIP39 seed phrase to convert * @returns {Uint8Array} The entropy value * @throws {Error} If the seed phrase is invalid * @example * const entropy = getEntropyFromSeedPhrase("witch collapse practice feed shame open despair creek road again ice least"); * console.log(entropy); // Uint8Array */ function getEntropyFromSeedPhrase(seedPhrase) { if (!(0, bip39_1.validateMnemonic)(seedPhrase)) { throw new Error("Invalid seed phrase"); } // bip39.mnemonicToEntropy returns a hex string, convert it to Uint8Array const entropyHex = (0, bip39_1.mnemonicToEntropy)(seedPhrase); return (0, utils_js_1.hexToBytes)(entropyHex); } /** * Validates a BIP39 seed phrase * @param {string} seedPhrase - The seed phrase to validate * @returns {boolean} True if the seed phrase is valid, false otherwise * @example * const isValid = validateSeedPhrase("witch collapse practice feed shame open despair creek road again ice least"); * console.log(isValid); // true */ function validateSeedPhrase(seedPhrase) { logger_1.logger.log("Validating seed phrase"); logger_1.logger.log("Input being validated"); const isValid = (0, bip39_1.validateMnemonic)(seedPhrase); logger_1.logger.log({ isValid }, "Validated seed phrase"); return Boolean(isValid); } /** * Converts a BIP39 seed phrase to a Nostr key pair * @param {string} seedPhrase - The BIP39 seed phrase to convert * @returns {KeyPair} A key pair containing private and public keys in various formats * @throws {Error} If the seed phrase is invalid or key generation fails * @example * const keyPair = seedPhraseToKeyPair("witch collapse practice feed shame open despair creek road again ice least"); * console.log(keyPair.privateKey); // hex private key * console.log(keyPair.publicKey); // hex public key * console.log(keyPair.nsec); // bech32 nsec private key * console.log(keyPair.npub); // bech32 npub public key */ function seedPhraseToKeyPair(seedPhrase) { try { const entropy = getEntropyFromSeedPhrase(seedPhrase); // Hash the entropy to generate a proper private key const privateKeyBytes = (0, sha2_js_1.sha256)(entropy); entropy.fill(0); // zero sensitive material const privateKeyHex = (0, utils_js_1.bytesToHex)(privateKeyBytes); privateKeyBytes.fill(0); // zero sensitive material // Derive the public key const publicKeyBytes = secp256k1_js_1.secp256k1.getPublicKey((0, utils_js_1.hexToBytes)(privateKeyHex), true); // Force compressed format const publicKey = (0, utils_js_1.bytesToHex)(publicKeyBytes); // Generate the nsec and npub formats const nsec = exports.nip19.nsecEncode(privateKeyHex); const npub = exports.nip19.npubEncode(publicKey); return { privateKey: privateKeyHex, publicKey, nsec, npub, seedPhrase, }; } catch (error) { logger_1.logger.error("Failed to create key pair from seed phrase:", error?.toString()); throw error; } } /** * Generates a new key pair with a random seed phrase * @returns {KeyPair} A new key pair containing private and public keys in various formats * @example * const keyPair = generateKeyPairWithSeed(); * console.log(keyPair.seedPhrase); // random seed phrase * console.log(keyPair.privateKey); // hex private key * console.log(keyPair.publicKey); // hex public key */ function generateKeyPairWithSeed() { const seedPhrase = (0, bip39_1.generateMnemonic)(); return seedPhraseToKeyPair(seedPhrase); } /** * Creates a key pair from a hex private key * @param {string} privateKeyHex - The hex-encoded private key * @returns {KeyPair} A key pair containing private and public keys in various formats * @throws {Error} If the private key is invalid * @example * const keyPair = fromHex("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"); * console.log(keyPair.publicKey); // corresponding public key * console.log(keyPair.nsec); // bech32 nsec private key * console.log(keyPair.npub); // bech32 npub public key */ function fromHex(privateKeyHex) { try { // Validate the private key const privateKeyBytes = (0, utils_js_1.hexToBytes)(privateKeyHex); if (!secp256k1_js_1.secp256k1.utils.isValidSecretKey(privateKeyBytes)) { privateKeyBytes.fill(0); // zero sensitive material throw new Error("Invalid private key"); } // Derive the public key const publicKeyBytes = secp256k1_js_1.secp256k1.getPublicKey(privateKeyBytes, true); // Force compressed format privateKeyBytes.fill(0); // zero sensitive material const publicKey = (0, utils_js_1.bytesToHex)(publicKeyBytes); // Generate the nsec and npub formats const nsec = exports.nip19.nsecEncode(privateKeyHex); const npub = exports.nip19.npubEncode(publicKey); return { privateKey: privateKeyHex, publicKey, nsec, npub, seedPhrase: "", // No seed phrase for hex-imported keys }; } catch (error) { logger_1.logger.error("Failed to create key pair from hex:", error?.toString()); throw error; } } /** * Derives a public key from a private key * @param {string} privateKey - The hex-encoded private key * @returns {string} The hex-encoded public key * @example * const pubkey = getPublicKey("1234567890abcdef..."); * console.log(pubkey); // hex public key */ function getPublicKey(privateKey) { try { const privateKeyBytes = (0, utils_js_1.hexToBytes)(privateKey); const publicKeyBytes = secp256k1_js_1.secp256k1.getPublicKey(privateKeyBytes, true); // Force compressed format privateKeyBytes.fill(0); // zero sensitive material return (0, utils_js_1.bytesToHex)(publicKeyBytes); } catch (error) { logger_1.logger.error("Failed to get public key:", error?.toString()); throw error; } } /** * NIP-19 encoding and decoding functions * @namespace */ exports.nip19 = { /** * Encodes a public key into npub format * @param {string} pubkey - The hex-encoded public key * @returns {string} The bech32-encoded npub string */ npubEncode(pubkey) { const data = (0, utils_js_1.hexToBytes)(pubkey); const words = bech32_1.bech32.toWords(Uint8Array.from(data)); return bech32_1.bech32.encode("npub", words, 1000); }, /** * Decodes an npub string to hex format * @param {string} npub - The bech32-encoded npub string * @returns {string} The hex-encoded public key */ npubDecode(npub) { const { prefix, words } = bech32_1.bech32.decode(npub, 1000); if (prefix !== "npub") throw new Error("Invalid npub: wrong prefix"); const data = bech32_1.bech32.fromWords(words); return (0, utils_js_1.bytesToHex)(data instanceof Uint8Array ? data : Uint8Array.from(data)); }, /** * Encodes a private key into nsec format * @param {string} privkey - The hex-encoded private key * @returns {string} The bech32-encoded nsec string */ nsecEncode(privkey) { const data = (0, utils_js_1.hexToBytes)(privkey); const words = bech32_1.bech32.toWords(Uint8Array.from(data)); return bech32_1.bech32.encode("nsec", words, 1000); }, /** * Decodes an nsec string to hex format * @param {string} nsec - The bech32-encoded nsec string * @returns {string} The hex-encoded private key */ nsecDecode(nsec) { const { prefix, words } = bech32_1.bech32.decode(nsec, 1000); if (prefix !== "nsec") throw new Error("Invalid nsec: wrong prefix"); const data = bech32_1.bech32.fromWords(words); return (0, utils_js_1.bytesToHex)(data instanceof Uint8Array ? data : Uint8Array.from(data)); }, /** * Encodes an event ID into note format * @param {string} eventId - The hex-encoded event ID * @returns {string} The bech32-encoded note string */ noteEncode(eventId) { const data = (0, utils_js_1.hexToBytes)(eventId); const words = bech32_1.bech32.toWords(Uint8Array.from(data)); return bech32_1.bech32.encode("note", words, 1000); }, /** * Decodes a note string to hex format * @param {string} note - The bech32-encoded note string * @returns {string} The hex-encoded event ID */ noteDecode(note) { const { prefix, words } = bech32_1.bech32.decode(note, 1000); if (prefix !== "note") throw new Error("Invalid note: wrong prefix"); const data = bech32_1.bech32.fromWords(words); return (0, utils_js_1.bytesToHex)(data instanceof Uint8Array ? data : Uint8Array.from(data)); }, /** * Decodes any bech32-encoded Nostr entity * @param {string} bech32str - The bech32-encoded string * @returns {{ type: string; data: Uint8Array }} Object containing the decoded type and data * @property {string} type - The type of the decoded entity (npub, nsec, or note) * @property {Uint8Array} data - The raw decoded data */ decode(bech32str) { const { prefix, words } = bech32_1.bech32.decode(bech32str, 1000); const data = bech32_1.bech32.fromWords(words); return { type: prefix, data: data instanceof Uint8Array ? data : Uint8Array.from(data), }; }, }; /** * Calculates the event hash/ID according to the Nostr protocol * @param {UnsignedEvent} event - The event to hash * @returns {string} The hex-encoded event hash */ function getEventHash(event) { const serialized = JSON.stringify([ 0, event.pubkey, event.created_at, event.kind, event.tags, event.content, ]); return (0, utils_js_1.bytesToHex)((0, sha2_js_1.sha256)(new TextEncoder().encode(serialized))); } /** * Signs a Nostr event * @param {UnsignedEvent} event - The event to sign * @param {string} privateKey - The hex-encoded private key to sign with * @returns {Promise<string>} The hex-encoded signature * @throws {Error} If signing fails * @example * const signature = await signEvent({ * pubkey: "...", * created_at: Math.floor(Date.now() / 1000), * kind: 1, * tags: [], * content: "Hello Nostr!" * }, privateKey); */ async function signEvent(event, privateKey) { try { const eventHash = getEventHash(event); const privateKeyBytes = (0, utils_js_1.hexToBytes)(privateKey); const signature = secp256k1_js_1.schnorr.sign((0, utils_js_1.hexToBytes)(eventHash), privateKeyBytes); privateKeyBytes.fill(0); // zero sensitive material logger_1.logger.log("Event signed successfully"); return (0, utils_js_1.bytesToHex)(signature); } catch (error) { logger_1.logger.error("Failed to sign event:", error?.toString()); throw error; } } /** * Verifies a Nostr event signature * @param {NostrEvent} event - The event to verify * @returns {Promise<boolean>} True if the signature is valid, false otherwise * @example * const isValid = await verifyEvent(event); * console.log(isValid); // true or false */ async function verifyEvent(event) { try { if (!event.id || !event.pubkey || !event.sig) { logger_1.logger.log("Invalid event: missing required fields"); return false; } const hash = getEventHash(event); if (hash !== event.id) { logger_1.logger.log("Event hash mismatch"); return false; } logger_1.logger.log("Verifying event signature"); return secp256k1_js_1.schnorr.verify((0, utils_js_1.hexToBytes)(event.sig), (0, utils_js_1.hexToBytes)(hash), (0, utils_js_1.hexToBytes)(event.pubkey)); } catch (error) { logger_1.logger.error("Failed to verify event:", error?.toString()); throw error; } } /** * Configures secp256k1 with HMAC for WebSocket utilities * This is required for some WebSocket implementations * @example * configureHMAC(); */ function configureHMAC() { const hmacFunction = (key, ...messages) => { const h = hmac_js_1.hmac.create(sha2_js_1.sha256, key); messages.forEach((msg) => h.update(msg)); return h.digest(); }; const hmacSyncFunction = (key, ...messages) => { const h = hmac_js_1.hmac.create(sha2_js_1.sha256, key); messages.forEach((msg) => h.update(msg)); return h.digest(); }; // Safety check: only patch if utils exists and hmacSha256Sync is a known property if ("utils" in secp256k1_js_1.secp256k1 && typeof secp256k1_js_1.secp256k1.utils?.hmacSha256Sync !== "undefined") { secp256k1_js_1.secp256k1.utils.hmacSha256 = hmacFunction; secp256k1_js_1.secp256k1.utils.hmacSha256Sync = hmacSyncFunction; } else { logger_1.logger.log("secp256k1.utils.hmacSha256Sync not found; HMAC configuration skipped (library may handle HMAC internally)"); } logger_1.logger.log("Configured HMAC for secp256k1"); } /** * Creates a new signed Nostr event * @param {string} content - The event content * @param {number} kind - The event kind (1 for text note, etc.) * @param {string} privateKey - The hex-encoded private key to sign with * @param {string[][]} [tags=[]] - Optional event tags * @returns {Promise<NostrEvent>} The signed event * @throws {Error} If event creation or signing fails * @example * const event = await createEvent( * "Hello Nostr!", * 1, * privateKey, * [["t", "nostr"]] * ); * console.log(event); // complete signed event */ async function createEvent(content, kind, privateKey, tags = []) { const publicKey = getPublicKey(privateKey); const event = { pubkey: publicKey, created_at: Math.floor(Date.now() / 1000), kind, tags, content, }; const id = getEventHash(event); const sig = await signEvent(event, privateKey); logger_1.logger.log("Created new Nostr event"); return { ...event, id, sig, }; } /** * Converts a BIP39 seed phrase to a private key * @param {string} seedPhrase - The BIP39 seed phrase to convert * @returns {string} The hex-encoded private key * @throws {Error} If the seed phrase is invalid * @example * const privateKey = seedPhraseToPrivateKey("witch collapse practice feed shame open despair creek road again ice least"); * console.log(privateKey); // hex private key */ function seedPhraseToPrivateKey(seedPhrase) { return seedPhraseToKeyPair(seedPhrase).privateKey; } /** * Converts a private key to bech32 nsec format * @param {string} privateKey - The hex-encoded private key * @returns {string} The bech32-encoded nsec private key * @throws {Error} If the private key is invalid * @example * const nsec = privateKeyToNsec("1234567890abcdef..."); * console.log(nsec); // "nsec1..." */ function privateKeyToNsec(privateKey) { try { return exports.nip19.nsecEncode(privateKey); } catch (error) { logger_1.logger.error("Failed to encode nsec:", error?.toString()); throw error; } } /** * Converts a private key to bech32 npub format * @param {string} privateKey - The hex-encoded private key * @returns {string} The bech32-encoded npub public key * @throws {Error} If the private key is invalid * @example * const npub = privateKeyToNpub("1234567890abcdef..."); * console.log(npub); // "npub1..." */ function privateKeyToNpub(privateKey) { try { const privateKeyBytes = (0, utils_js_1.hexToBytes)(privateKey); const publicKey = secp256k1_js_1.secp256k1.getPublicKey(privateKeyBytes, true); privateKeyBytes.fill(0); // zero sensitive material return exports.nip19.npubEncode((0, utils_js_1.bytesToHex)(publicKey)); } catch (error) { logger_1.logger.error("Failed to encode npub:", error?.toString()); throw error; } } /** * Converts a bech32 nsec private key to hex format * @param {string} nsec - The bech32-encoded nsec private key * @returns {string} The hex-encoded private key * @throws {Error} If the nsec key is invalid * @example * const hex = nsecToHex("nsec1..."); * console.log(hex); // "1234567890abcdef..." */ function nsecToHex(nsec) { try { const hexPrivateKey = exports.nip19.nsecDecode(nsec); logger_1.logger.log("Converted nsec to hex"); return hexPrivateKey; } catch (error) { logger_1.logger.error("Failed to decode nsec:", error?.toString()); throw error; } } /** * Converts a bech32 npub public key to hex format * @param {string} npub - The bech32-encoded npub public key * @returns {string} The hex-encoded public key * @throws {Error} If the npub key is invalid * @example * const hex = npubToHex("npub1..."); * console.log(hex); // "1234567890abcdef..." */ function npubToHex(npub) { try { const { type, data } = exports.nip19.decode(npub); if (type !== "npub") { throw new Error("Invalid npub format"); } logger_1.logger.log("Converted npub to hex"); return (0, utils_js_1.bytesToHex)(data); } catch (error) { logger_1.logger.error("Failed to decode npub:", error?.toString()); throw error; } } /** * Converts a hex public key to bech32 npub format * @param {string} publicKeyHex - The hex-encoded public key * @returns {string} The bech32-encoded npub public key * @throws {Error} If the public key is invalid * @example * const npub = hexToNpub("1234567890abcdef..."); * console.log(npub); // "npub1..." */ function hexToNpub(publicKeyHex) { try { logger_1.logger.log("Converting hex to npub"); return exports.nip19.npubEncode(publicKeyHex); } catch (error) { logger_1.logger.error("Failed to encode npub:", error?.toString()); throw error; } } /** * Converts a hex private key to bech32 nsec format * @param {string} privateKeyHex - The hex-encoded private key * @returns {string} The bech32-encoded nsec private key * @throws {Error} If the private key is invalid * @example * const nsec = hexToNsec("1234567890abcdef..."); * console.log(nsec); // "nsec1..." */ function hexToNsec(privateKeyHex) { try { logger_1.logger.log("Converting hex to nsec"); return exports.nip19.nsecEncode(privateKeyHex); } catch (error) { logger_1.logger.error("Failed to encode nsec:", error?.toString()); throw error; } } /** * Signs a message with a private key * @param {string} message - The message to sign * @param {string} privateKey - The hex-encoded private key to sign with * @returns {Promise<string>} The hex-encoded signature * @throws {Error} If signing fails * @example * const signature = await signMessage("Hello Nostr!", privateKey); * console.log(signature); // hex-encoded signature */ async function signMessage(message, privateKey) { try { const messageBytes = new TextEncoder().encode(message); const messageHash = (0, sha2_js_1.sha256)(messageBytes); const messageHashHex = (0, utils_js_1.bytesToHex)(messageHash); const privateKeyBytes = (0, utils_js_1.hexToBytes)(privateKey); const signature = secp256k1_js_1.schnorr.sign((0, utils_js_1.hexToBytes)(messageHashHex), privateKeyBytes); privateKeyBytes.fill(0); // zero sensitive material logger_1.logger.log("Message signed successfully"); return (0, utils_js_1.bytesToHex)(signature); } catch (error) { logger_1.logger.error("Failed to sign message:", error?.toString()); throw error; } } /** * Verifies a message signature * @param {string} message - The original message * @param {string} signature - The hex-encoded signature to verify * @param {string} publicKey - The hex-encoded public key to verify against * @returns {Promise<boolean>} True if the signature is valid, false otherwise * @example * const isValid = await verifySignature("Hello Nostr!", signature, publicKey); * console.log(isValid); // true or false */ async function verifySignature(message, signature, publicKey) { try { const messageBytes = new TextEncoder().encode(message); const messageHash = (0, sha2_js_1.sha256)(messageBytes); const messageHashHex = (0, utils_js_1.bytesToHex)(messageHash); logger_1.logger.log("Verifying message signature"); return secp256k1_js_1.schnorr.verify((0, utils_js_1.hexToBytes)(signature), (0, utils_js_1.hexToBytes)(messageHashHex), (0, utils_js_1.hexToBytes)(publicKey)); } catch (error) { logger_1.logger.error("Failed to verify signature:", error?.toString()); throw error; } } /** * Converts a bech32 nsec private key to hex format * @param {string} nsec - The bech32-encoded nsec private key * @returns {string} The hex-encoded private key * @throws {Error} If the nsec key is invalid * @example * const hex = nsecToPrivateKey("nsec1..."); * console.log(hex); // "1234567890abcdef..." */ function nsecToPrivateKey(nsec) { try { return exports.nip19.nsecDecode(nsec); } catch (error) { logger_1.logger.error("Failed to decode nsec:", error?.toString()); throw error; } } /** * Encrypts a hex private key into an ncryptsec bech32 string (NIP-49) * @param {string} privateKeyHex - The hex-encoded private key (64 hex chars / 32 bytes) * @param {string} password - The password to encrypt with * @param {number} [logn=16] - Scrypt log2(N) parameter (higher = slower but more secure) * @returns {string} The bech32-encoded ncryptsec string * @throws {Error} If encryption fails * @example * const ncryptsec = toNcryptsec("1234567890abcdef...", "my-strong-password"); * console.log(ncryptsec); // "ncryptsec1..." */ function toNcryptsec(privateKeyHex, password, logn = 16) { try { const secretBytes = (0, utils_js_1.hexToBytes)(privateKeyHex); const result = nostr_crypto_utils_1.nip49.encrypt(secretBytes, password, logn); secretBytes.fill(0); // zero sensitive material return result; } catch (error) { logger_1.logger.error("Failed to encrypt private key to ncryptsec:", error?.toString()); throw error; } } /** * Decrypts an ncryptsec bech32 string back to a hex private key (NIP-49) * @param {string} ncryptsec - The bech32-encoded ncryptsec string * @param {string} password - The password used for encryption * @returns {string} The hex-encoded private key * @throws {Error} If decryption fails (wrong password, invalid format, etc.) * @example * const privateKeyHex = fromNcryptsec("ncryptsec1...", "my-strong-password"); * console.log(privateKeyHex); // "1234567890abcdef..." */ function fromNcryptsec(ncryptsec, password) { try { const secretBytes = nostr_crypto_utils_1.nip49.decrypt(ncryptsec, password); const hex = (0, utils_js_1.bytesToHex)(secretBytes); secretBytes.fill(0); // zero sensitive material return hex; } catch (error) { logger_1.logger.error("Failed to decrypt ncryptsec:", error?.toString()); throw error; } }