Skip to content

Commit

Permalink
Merge pull request #25 from aaronbarnardsound/release/0.2.0
Browse files Browse the repository at this point in the history
Release 0.2.0
  • Loading branch information
lnbc1QWFyb24 authored Mar 22, 2023
2 parents 3d00360 + 59b3d3d commit 898ddf5
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 92 deletions.
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/[email protected]":
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

0 comments on commit 898ddf5

Please sign in to comment.