Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 0.2.0 #25

Merged
merged 5 commits into from
Mar 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lnmessage",
"version": "0.1.0",
"version": "0.2.0",
"description": "Talk to Lightning nodes from your browser",
"main": "dist/index.js",
"type": "module",
Expand All @@ -10,6 +10,7 @@
],
"author": "Aaron Barnard",
"license": "MIT",
"repository": "github:aaronbarnardsound/lnmessage",
"scripts": {
"build": "tsc"
},
Expand All @@ -26,9 +27,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"
}
}
20 changes: 12 additions & 8 deletions src/crypto.ts
Original file line number Diff line number Diff line change
@@ -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))
Expand All @@ -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))
}

/**
Expand Down Expand Up @@ -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) {
Expand Down
27 changes: 19 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -96,7 +100,8 @@ class LnMessage {
privateKey,
ip,
port = 9735,
logger
logger,
tcpSocket
} = options

this._ls = Buffer.from(privateKey || createRandomPrivateKey(), 'hex')
Expand All @@ -115,6 +120,7 @@ class LnMessage {
this.connected$ = new BehaviorSubject<boolean>(false)
this.connecting = false
this.Buffer = Buffer
this.tcpSocket = tcpSocket

this._handshakeState = HANDSHAKE_STATE.INITIATOR_INITIATING
this._decryptedMsgs$ = new Subject()
Expand Down Expand Up @@ -159,10 +165,15 @@ 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.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')
Expand Down Expand Up @@ -211,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 =
Expand Down
6 changes: 3 additions & 3 deletions src/noise-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
47 changes: 47 additions & 0 deletions src/socket-wrapper.ts
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Buffer } from 'buffer'
import type { Socket as TCPSocket } from 'net'

export type LnWebSocketOptions = {
/**
Expand All @@ -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
Expand Down
76 changes: 6 additions & 70 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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==
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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"
Expand Down