From 5469c11d9ab8685fd0bc38e9c4b37a5289f92d9f Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Tue, 7 Mar 2023 08:02:13 +0530 Subject: [PATCH 1/4] chore: replace secp256k1 to @noble/secp256k1 (#22) * chore: replace secp256k1 to @noble/secp256k1 * Formatting * Version bump --------- Co-authored-by: Aaron --- package.json | 4 +-- src/crypto.ts | 20 ++++++++------ yarn.lock | 76 ++++----------------------------------------------- 3 files changed, 20 insertions(+), 80 deletions(-) diff --git a/package.json b/package.json index 0cf4f25..44a8a6b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lnmessage", - "version": "0.1.0", + "version": "0.1.0-0.1.0", "description": "Talk to Lightning nodes from your browser", "main": "dist/index.js", "type": "module", @@ -26,9 +26,9 @@ }, "dependencies": { "@noble/hashes": "^1.2.0", + "@noble/secp256k1": "^1.7.1", "buffer": "^6.0.3", "rxjs": "^7.5.7", - "secp256k1": "^5.0.0", "ws": "^8.12.1" } } diff --git a/src/crypto.ts b/src/crypto.ts index ec93056..f707927 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -1,16 +1,17 @@ import { Buffer } from 'buffer' -import secp256k1 from 'secp256k1' +import * as secp256k1 from '@noble/secp256k1' import { createCipher, createDecipher } from './chacha/index.js' import { hmac } from '@noble/hashes/hmac' import { sha256 as sha256Array } from '@noble/hashes/sha256' import { bytesToHex, randomBytes } from '@noble/hashes/utils' -export function sha256(input: Buffer): Buffer { +export function sha256(input: Uint8Array): Buffer { return Buffer.from(sha256Array(input)) } export function ecdh(pubkey: Uint8Array, privkey: Uint8Array) { - return Buffer.from(secp256k1.ecdh(pubkey, privkey)) + const point = secp256k1.Point.fromHex(secp256k1.getSharedSecret(privkey, pubkey)) + return Buffer.from(sha256(point.toRawBytes(true))) } export function hmacHash(key: Buffer, input: Buffer) { return Buffer.from(hmac(sha256Array, key, input)) @@ -36,7 +37,7 @@ export function hkdf(ikm: Buffer, len: number, salt = Buffer.alloc(0), info = Bu } export function getPublicKey(privKey: Buffer, compressed = true) { - return Buffer.from(secp256k1.publicKeyCreate(privKey, compressed)) + return Buffer.from(secp256k1.getPublicKey(privKey, compressed)) } /** @@ -101,13 +102,16 @@ export function createRandomPrivateKey(): string { } export function validPublicKey(publicKey: string): boolean { - return secp256k1.publicKeyVerify(Buffer.from(publicKey, 'hex')) + try { + secp256k1.Point.fromHex(publicKey) + return true + } catch (e) { + return false + } } export function validPrivateKey(privateKey: string | Buffer): boolean { - return secp256k1.privateKeyVerify( - typeof privateKey === 'string' ? Buffer.from(privateKey, 'hex') : privateKey - ) + return secp256k1.utils.isValidPrivateKey(privateKey) } export function createRandomBytes(length: number) { diff --git a/yarn.lock b/yarn.lock index e9cebe3..7c9b559 100644 --- a/yarn.lock +++ b/yarn.lock @@ -46,6 +46,11 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.2.0.tgz#a3150eeb09cc7ab207ebf6d7b9ad311a9bdbed12" integrity sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ== +"@noble/secp256k1@^1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c" + integrity sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -223,11 +228,6 @@ base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== -bn.js@^4.11.9: - version "4.12.0" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" - integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -243,11 +243,6 @@ braces@^3.0.2: dependencies: fill-range "^7.0.1" -brorand@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== - buffer@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" @@ -321,19 +316,6 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -elliptic@^6.5.4: - version "6.5.4" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" - integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== - dependencies: - bn.js "^4.11.9" - brorand "^1.1.0" - hash.js "^1.0.0" - hmac-drbg "^1.0.1" - inherits "^2.0.4" - minimalistic-assert "^1.0.1" - minimalistic-crypto-utils "^1.0.1" - escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" @@ -588,23 +570,6 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" - integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.1" - -hmac-drbg@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -636,7 +601,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3, inherits@^2.0.4: +inherits@2: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -725,16 +690,6 @@ micromatch@^4.0.4: braces "^3.0.2" picomatch "^2.3.1" -minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== - minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -752,16 +707,6 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -node-addon-api@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762" - integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA== - -node-gyp-build@^4.2.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" - integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ== - once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -883,15 +828,6 @@ rxjs@^7.5.7: dependencies: tslib "^2.1.0" -secp256k1@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-5.0.0.tgz#be6f0c8c7722e2481e9773336d351de8cddd12f7" - integrity sha512-TKWX8xvoGHrxVdqbYeZM9w+izTF4b9z3NhSaDkdn81btvuh+ivbIMGT/zQvDtTFWhRlThpoz6LEYTr7n8A5GcA== - dependencies: - elliptic "^6.5.4" - node-addon-api "^5.0.0" - node-gyp-build "^4.2.0" - semver@^7.3.7: version "7.3.7" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" From e910827696c01e1c68519cfcb1913686fbd8d723 Mon Sep 17 00:00:00 2001 From: escapedcat Date: Tue, 21 Mar 2023 20:44:18 +0100 Subject: [PATCH 2/4] fix: support SW usage (#23) * fix: support SW usage * refactor: use globalThis Co-authored-by: Michael Bumann --------- Co-authored-by: Aaron Co-authored-by: Michael Bumann --- src/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 23d848c..7042901 100644 --- a/src/index.ts +++ b/src/index.ts @@ -159,9 +159,7 @@ class LnMessage { this.connectionStatus$.next('connecting') this._attemptReconnect = attemptReconnect - this.socket = new ( - typeof window === 'undefined' ? (await import('ws')).default : window.WebSocket - )(this.wsUrl) + this.socket = new (typeof globalThis.WebSocket === 'undefined' ? (await import('ws')).default : globalThis.WebSocket)(this.wsUrl) this.socket.binaryType = 'arraybuffer' this.socket.onopen = async () => { From 22b84ee007ae53fbfdb9e0f2543b27f29da7cf03 Mon Sep 17 00:00:00 2001 From: Aaron Date: Wed, 22 Mar 2023 07:08:45 +1100 Subject: [PATCH 3/4] 0.1.0-0.3.0 - Feature: TCP Socket (#24) * Add tcpSocket option for direct connections * Dynamically import socket wrapper * Increment version * Update README --- README.md | 4 +++- package.json | 3 ++- src/index.ts | 25 +++++++++++++++++------ src/noise-state.ts | 6 +++--- src/socket-wrapper.ts | 47 +++++++++++++++++++++++++++++++++++++++++++ src/types.ts | 3 +++ 6 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 src/socket-wrapper.ts diff --git a/README.md b/README.md index d25e17e..2490022 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Talk to Lightning nodes from the Browser and NodeJS apps. ## Features -- Connect to a lightning node via a WebSocket connection. +- Connect to a lightning node via a WebSocket or TCP Socket connection. - Works in the Browser and Node without any polyfilling. - Initialise with a session secret to have a persistent node public key for the browser. - Control a Core Lightning node via [Commando](https://lightning.readthedocs.io/lightning-commando.7.html) RPC calls. @@ -84,6 +84,8 @@ type LnWebSocketOptions = { * When connecting directly to a node and not using a proxy, the protocol to use. Defaults to 'wss://' */ wsProtocol?: 'ws:' | 'wss:' + /**In Nodejs or React Native you can connect directly via a TCP socket */ + tcpSocket?: TCPSocket /** * 32 byte hex encoded private key to be used as the local node secret. * Use this to ensure a consistent local node identity across connection sessions diff --git a/package.json b/package.json index 44a8a6b..8865879 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lnmessage", - "version": "0.1.0-0.1.0", + "version": "0.1.0-0.3.0", "description": "Talk to Lightning nodes from your browser", "main": "dist/index.js", "type": "module", @@ -10,6 +10,7 @@ ], "author": "Aaron Barnard", "license": "MIT", + "repository": "github:aaronbarnardsound/lnmessage", "scripts": { "build": "tsc" }, diff --git a/src/index.ts b/src/index.ts index 7042901..52cb156 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,8 @@ import { CommandoMessage } from './messages/CommandoMessage.js' import { PongMessage } from './messages/PongMessage.js' import { PingMessage } from './messages/PingMessage.js' import type { WebSocket as NodeWebSocket } from 'ws' +import type { Socket as TCPSocket } from 'net' +import type SocketWrapper from './socket-wrapper.js' import { LnWebSocketOptions, @@ -47,7 +49,9 @@ class LnMessage { */ public wsUrl: string /**The WebSocket instance*/ - public socket: WebSocket | NodeWebSocket | null + public socket: WebSocket | NodeWebSocket | null | SocketWrapper + /**TCP socket instance*/ + public tcpSocket?: TCPSocket /** * @deprecated Use connectionStatus$ instead */ @@ -96,7 +100,8 @@ class LnMessage { privateKey, ip, port = 9735, - logger + logger, + tcpSocket } = options this._ls = Buffer.from(privateKey || createRandomPrivateKey(), 'hex') @@ -115,6 +120,7 @@ class LnMessage { this.connected$ = new BehaviorSubject(false) this.connecting = false this.Buffer = Buffer + this.tcpSocket = tcpSocket this._handshakeState = HANDSHAKE_STATE.INITIATOR_INITIATING this._decryptedMsgs$ = new Subject() @@ -159,8 +165,15 @@ class LnMessage { this.connectionStatus$.next('connecting') this._attemptReconnect = attemptReconnect - this.socket = new (typeof globalThis.WebSocket === 'undefined' ? (await import('ws')).default : globalThis.WebSocket)(this.wsUrl) - this.socket.binaryType = 'arraybuffer' + this.socket = this.tcpSocket + ? new (await import('./socket-wrapper.js')).default(this.wsUrl, this.tcpSocket) + : typeof globalThis.WebSocket === 'undefined' + ? new (await import('ws')).default(this.wsUrl) + : new globalThis.WebSocket(this.wsUrl) + + if ((this.socket as WebSocket | NodeWebSocket).binaryType) { + ;(this.socket as WebSocket | NodeWebSocket).binaryType = 'arraybuffer' + } this.socket.onopen = async () => { this._log('info', 'WebSocket is connected') @@ -209,8 +222,8 @@ class LnMessage { ) } - private queueMessage(event: MessageEvent) { - const { data } = event as { data: ArrayBuffer } + private queueMessage(event: { data: ArrayBuffer }) { + const { data } = event const message = Buffer.from(data) const currentData = diff --git a/src/noise-state.ts b/src/noise-state.ts index 3c9dfbb..0f7f7e9 100644 --- a/src/noise-state.ts +++ b/src/noise-state.ts @@ -120,7 +120,7 @@ export class NoiseState { // 4. ck, temp_k1 = HKDF(ck, es) const tempK1 = hkdf(ss, 64, this.ck) this.ck = tempK1.subarray(0, 32) - this.tempK1 = tempK1.subarray(32) + this.tempK1 = Buffer.from(tempK1.subarray(32)) // 5. c = encryptWithAD(temp_k1, 0, h, zero) const c = ccpEncrypt(this.tempK1, Buffer.alloc(12), this.h, Buffer.alloc(0)) @@ -183,7 +183,7 @@ export class NoiseState { // 4. ck, temp_k3 = HKDF(ck, ss) const tempK3 = hkdf(ss, 64, this.ck) this.ck = tempK3.subarray(0, 32) - this.tempK3 = tempK3.subarray(32) + this.tempK3 = Buffer.from(tempK3.subarray(32)) // 5. t = encryptWithAD(temp_k3, 0, h, zero) const t = ccpEncrypt(this.tempK3, Buffer.alloc(12), this.h, Buffer.alloc(0)) // 6. sk, rk = hkdf(ck, zero) @@ -239,7 +239,7 @@ export class NoiseState { // 4. ck, temp_k2 = hkdf(ck, ss) const tempK2 = hkdf(ss, 64, this.ck) this.ck = tempK2.subarray(0, 32) - this.tempK2 = tempK2.subarray(32) + this.tempK2 = Buffer.from(tempK2.subarray(32)) // 5. c = encryptWithAd(temp_k2, 0, h, zero) const c = ccpEncrypt(this.tempK2, Buffer.alloc(12), this.h, Buffer.alloc(0)) // 6. h = sha256(h || c) diff --git a/src/socket-wrapper.ts b/src/socket-wrapper.ts new file mode 100644 index 0000000..adfd840 --- /dev/null +++ b/src/socket-wrapper.ts @@ -0,0 +1,47 @@ +import type { Socket } from 'net' +import type { Buffer } from 'buffer' + +//**Wraps a TCP socket with the WebSocket API */ +class SocketWrapper { + public onopen?: () => void + public onclose?: () => void + public onerror?: (error: { message: string }) => void + public onmessage?: (event: { data: ArrayBuffer }) => void + public send: (message: Buffer) => void + public close: () => void + + constructor(connection: string, socket: Socket) { + socket.on('connect', () => { + this.onopen && this.onopen() + }) + + socket.on('close', () => { + this.onclose && this.onclose() + }) + + socket.on('error', (error) => { + this.onerror && this.onerror(error) + }) + + socket.on('data', (data) => { + this.onmessage && this.onmessage({ data }) + }) + + this.send = (message: Buffer) => { + socket.write(message) + } + + this.close = () => { + socket.removeAllListeners() + socket.destroy() + } + + const url = new URL(connection) + const { host } = url + const [nodeIP, port] = host.split(':') + + socket.connect(parseInt(port), nodeIP) + } +} + +export default SocketWrapper diff --git a/src/types.ts b/src/types.ts index 65f2c22..4745fd0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,5 @@ import type { Buffer } from 'buffer' +import type { Socket as TCPSocket } from 'net' export type LnWebSocketOptions = { /** @@ -25,6 +26,8 @@ export type LnWebSocketOptions = { * When connecting directly to a node, the protocol to use. Defaults to 'wss://' */ wsProtocol?: 'ws:' | 'wss:' + /**In nodejs or react native you can connect directly via a TCP socket */ + tcpSocket?: TCPSocket /** * 32 byte hex encoded private key to be used as the local node secret. * Use this to ensure a consistent local node identity across connection sessions From 59b3d3d755ca367ef1a558327a38b37992ac4db7 Mon Sep 17 00:00:00 2001 From: Aaron Date: Thu, 23 Mar 2023 06:34:45 +1100 Subject: [PATCH 4/4] Release 0.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8865879..9b6ecec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lnmessage", - "version": "0.1.0-0.3.0", + "version": "0.2.0", "description": "Talk to Lightning nodes from your browser", "main": "dist/index.js", "type": "module",