From 4239ba093d2d499e9ab9a55f2d2d3c66415a7ca8 Mon Sep 17 00:00:00 2001 From: Alex Bosworth Date: Fri, 3 Jul 2026 16:56:16 -0700 Subject: [PATCH] remove ecpair dependency --- CHANGELOG.md | 2 +- lnurl/auth.js | 29 ++++++++++++++--------------- lnurl/sign_auth_challenge.js | 31 ++++++++++++++++++++++--------- offchain/sign_payment_request.js | 32 ++++++++++++++++++++++++-------- package-lock.json | 1 - package.json | 3 +-- 6 files changed, 62 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f199119..c4a47b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Versions -## 22.1.6 +## 22.1.7 - Add support for LND 0.21.1 diff --git a/lnurl/auth.js b/lnurl/auth.js index ee609a8..211d2b2 100644 --- a/lnurl/auth.js +++ b/lnurl/auth.js @@ -2,7 +2,6 @@ const asyncAuto = require('async/auto'); const {bech32} = require('bech32'); const {returnResult} = require('asyncjs-util'); const {signMessage} = require('ln-service'); -const tinysecp = require('tiny-secp256k1'); const signAuthChallenge = require('./sign_auth_challenge'); @@ -35,9 +34,6 @@ const wordsAsUtf8 = n => Buffer.from(bech32.fromWords(n)).toString('utf8'); module.exports = (args, cbk) => { return new Promise((resolve, reject) => { return asyncAuto({ - // Import the ECPair library - ecp: async () => (await import('ecpair')).ECPairFactory(tinysecp), - // Check arguments validate: cbk => { if (!args.ask) { @@ -112,18 +108,21 @@ module.exports = (args, cbk) => { }], // Derive keys and get signatures - sign: ['ecp', 'parse', 'seed', ({ecp, parse, seed}, cbk) => { - const sign = signAuthChallenge({ - ecp, - hostname: parse.hostname, - k1: parse.k1, - seed: seed.signature, - }); + sign: ['parse', 'seed', ({parse, seed}, cbk) => { + try { + const sign = signAuthChallenge({ + hostname: parse.hostname, + k1: parse.k1, + seed: seed.signature, + }); - return cbk(null, { - public_key: sign.public_key, - signature: sign.signature, - }); + return cbk(null, { + public_key: sign.public_key, + signature: sign.signature, + }); + } catch (err) { + return cbk([500, 'UnexpectedErrorSigningAuthChallenge', {err}]); + } }], // Display confirmation dialog with domain name and action diff --git a/lnurl/sign_auth_challenge.js b/lnurl/sign_auth_challenge.js index a9d5ebe..99171b1 100644 --- a/lnurl/sign_auth_challenge.js +++ b/lnurl/sign_auth_challenge.js @@ -1,46 +1,59 @@ const {createHash} = require('crypto'); const {createHmac} = require('crypto'); +const tinysecp256k1 = require('tiny-secp256k1'); + const derEncodeSignature = require('./der_encode_signature'); -const asDer = n => (n[0]&128)?Buffer.concat([Buffer.alloc(1),n],1+n.length):n; -const bufferAsHex = buffer => buffer.toString('hex'); -const {from} = Buffer; +const bufferAsHex = buffer => Buffer.from(buffer).toString('hex'); +const derivePublicKey = key => tinysecp256k1.pointFromScalar(key, true); const hexAsBuffer = hex => Buffer.from(hex, 'hex'); const hmacSha256 = (pk, url) => createHmac('sha256', pk).update(url).digest(); +const {isPrivate} = tinysecp256k1; const sha256 = n => createHash('sha256').update(n).digest(); +const {sign} = tinysecp256k1; const utf8AsBuffer = utf8 => Buffer.from(utf8, 'utf8'); /** Sign an authentication challenge for LNURL Auth { - ecp: hostname: k1: seed: } + @throws + + @returns { public_key: signature: } */ -module.exports = ({ecp, hostname, k1, seed}) => { +module.exports = ({hostname, k1, seed}) => { // LUD-13: LN wallet defines hashingKey as sha256(signature) const hashingKey = sha256(utf8AsBuffer(seed)); // LUD-13: linkingPrivKey is defined as hmacSha256(hashingKey, domain) const linkingPrivKey = hmacSha256(hashingKey, utf8AsBuffer(hostname)); - // Instantiate the key pair from this derived private key - const linkingKey = ecp.fromPrivateKey(linkingPrivKey); + // Validate the private key + if (!isPrivate(linkingPrivKey)) { + throw new Error('ExpectedValidLinkingPrivateKey'); + } + + const publicKey = derivePublicKey(linkingPrivKey); + + if (!publicKey) { + throw new Error('ExpectedPublicKeyFromLinkingPrivateKey'); + } // Using the host-specific linking key, sign the challenge k1 value - const signature = bufferAsHex(from(linkingKey.sign(hexAsBuffer(k1)))); + const signature = bufferAsHex(sign(hexAsBuffer(k1), linkingPrivKey)); return { - public_key: bufferAsHex(linkingKey.publicKey), + public_key: bufferAsHex(publicKey), signature: derEncodeSignature({signature}).encoded, }; }; diff --git a/offchain/sign_payment_request.js b/offchain/sign_payment_request.js index 04be8ed..d4886f1 100644 --- a/offchain/sign_payment_request.js +++ b/offchain/sign_payment_request.js @@ -1,6 +1,7 @@ const {randomBytes} = require('crypto'); const asyncAuto = require('async/auto'); +const asyncRetry = require('async/retry'); const {createSignedRequest} = require('ln-service'); const {createUnsignedRequest} = require('ln-service'); const {decode} = require('bip66'); @@ -8,13 +9,15 @@ const {returnResult} = require('asyncjs-util'); const {signBytes} = require('ln-service'); const tinysecp256k1 = require('tiny-secp256k1'); -const bufferAsHex = buffer => buffer.toString('hex'); +const bufferAsHex = buffer => Buffer.from(buffer).toString('hex'); const {concat} = Buffer; const defaultBaseFee = '1000'; const defaultCltvDelta = 144; const defaultFeeRate = '1'; +const derivePublicKey = key => tinysecp256k1.pointFromScalar(key, true); const hexAsBuffer = hex => Buffer.from(hex, 'hex'); const {isArray} = Array; +const {isPrivate} = tinysecp256k1; const keyFamilyIdentity = 6; const keyIndexIdentity = 0; const makePrivateKey = () => randomBytes(32); @@ -103,20 +106,33 @@ module.exports = (args, cbk) => { return cbk(); }, - // Create a key pair for a virtual channel invoice - getKeyPair: ['validate', async ({}) => { + // Generate key pair + getKeyPair: ['validate', ({}, cbk) => { // Exit early when not using a virtual channel if (!args.is_virtual) { - return; + return cbk(); } - const ecp = (await import('ecpair')).ECPairFactory(tinysecp256k1); + return asyncRetry({}, cbk => { + const privateKey = makePrivateKey(); - const key = ecp.fromPrivateKey(makePrivateKey()); + // Very rarely random bytes are not a valid private key + if (!isPrivate(privateKey)) { + return cbk([503, 'ExpectedValidPrivateKeyFromRandomBytes']); + } - const publicKey = unit8AsHex(key.publicKey); + const publicKey = derivePublicKey(privateKey); - return {private_key: key.privateKey, public_key: publicKey}; + if (!publicKey) { + return cbk([503, 'ExpectedDerivedPublicKeyFromPrivateKey']); + } + + return cbk(null, { + private_key: privateKey, + public_key: bufferAsHex(publicKey), + }); + }, + cbk); }], // Assemble the hop hints from the chosen hint channels diff --git a/package-lock.json b/package-lock.json index 3411da7..053d554 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,6 @@ "colorette": "2.0.20", "crypto-js": "4.2.0", "csv-parse": "7.0.1", - "ecpair": "2.1.0", "goldengate": "16.0.3", "grammy": "1.44.0", "hot-formula-parser": "4.0.0", diff --git a/package.json b/package.json index d16134f..f9e19ac 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "colorette": "2.0.20", "crypto-js": "4.2.0", "csv-parse": "7.0.1", - "ecpair": "2.1.0", "goldengate": "16.0.3", "grammy": "1.44.0", "hot-formula-parser": "4.0.0", @@ -81,5 +80,5 @@ "postpublish": "docker buildx build --platform linux/amd64,linux/arm64 -t alexbosworth/balanceofsatoshis -t alexbosworth/balanceofsatoshis:$npm_package_version --push .", "test": "npx nyc@17.1.0 node --experimental-test-coverage --test test/arrays/*.js test/balances/*.js test/chain/*.js test/display/*.js test/encryption/*.js test/lnd/*.js test/network/*.js test/nodes/*.js test/peers/*.js test/responses/*.js test/routing/*.js test/services/*.js test/swaps/*.js test/tags/*.js test/telegram/*.js test/wallets/*.js" }, - "version": "22.1.6" + "version": "22.1.7" }