Skip to content

Commit

Permalink
Merge pull request #19 from aaronbarnardsound/release/0.1.0
Browse files Browse the repository at this point in the history
Release 0.1.0
  • Loading branch information
lnbc1QWFyb24 authored Feb 24, 2023
2 parents 466fae7 + 9fba1ca commit 3d00360
Show file tree
Hide file tree
Showing 10 changed files with 121 additions and 144 deletions.
46 changes: 5 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
# lnmessage

Talk to Lightning nodes from the Browser or NodeJS(after polyfill) apps.
Talk to Lightning nodes from the Browser and NodeJS apps.

## Features

- Connect to a lightning node via a WebSocket connection.
- Works in the Browser without any polyfilling.
- Works in NodeJS with a simple polyfill
- 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.
- Automatic handling of ping messages to ensure constant connection to the node.
Expand Down Expand Up @@ -82,7 +81,7 @@ type LnWebSocketOptions = {
*/
wsProxy?: string
/**
* When connecting directly to a node, the protocol to use. Defaults to 'wss://'
* When connecting directly to a node and not using a proxy, the protocol to use. Defaults to 'wss://'
*/
wsProtocol?: 'ws:' | 'wss:'
/**
Expand Down Expand Up @@ -230,50 +229,15 @@ class Lnmessage {
}
```

## NodeJS Polyfill

Lnmessage is designed for the browser, but can be adapted to work in NodeJS apps with a simple polyfill:

1. Install `ws` dependency

**Yarn**
`yarn add ws`
`yarn add -D @types/ws`

**NPM**

`npm i ws`
`npm install --save-dev @types/ws`

2. Create a `polyfills.ts` file

```typescript
import WebSocket from 'ws'
import crypto from 'crypto'

if (!(<any>global).crypto) {
;(<any>global).crypto = crypto
}

if (!(<any>global).WebSocket) {
;(<any>global).WebSocket = WebSocket
}
```

3. Include polyfills.ts file in your tsconfig.json's includes section.
4. Import and initialise polyfills.ts at the start of your project.

## WebSocket Proxy

There are some limitations to connecting to Lightning nodes within a browser. Core Lightning nodes can be directly connected to if the [`experimental-websocket-port`](https://lightning.readthedocs.io/lightningd-config.5.html#experimental-options) option is set in the config. This will allow a direct connection to the node, but if you are running a browser app on https, then it will not allow a connection to a non SSL WebSocket endpoint, so you would need to setup SSL for your node. As far as I know LND nodes do not accept connections via WebSocket at this time.

So to simplify connecting to any Lightning node, you can go through a WebSocket proxy (see [Clams](https://github.com/clams-tech/lnsocket-proxy) and [jb55](https://github.com/jb55/ln-ws-proxy)'s WebSocket proxy server repos). Going through a proxy like this requires no trust in the server. The WebSocket connection is initated with the proxy, which then creates a regular TCP socket connection to the node. Then all messages are fully encrypted via the noise protocol, so the server only sees encrypted binary traffic that is simply proxied between the browser and the node. Currently only clearnet is supported, but I believe that the WebSocket proxy code could be modified to create a socket connection to a TOR only node to make this work.

## Current Limitations
## TOR

- For Commando calls, `lnmessage` will handle matching requests with responses and in most cases this just works. For RPC calls where a large response is expected (`listinvoices`, `listpays` etc) it is recommended to `await` these calls without making other calls simultaneously. A proxy server socket connection may split the response in to multiple parts. This leads to the messages possibly getting scrambled if multiple requests are made at the same time.
- Most connections will need to be made via a WebSocket proxy server. See the WebSocket proxy section.
- Clearnet only. I am pretty sure that this will not work out of the box with TOR connections, but I still need to try it in a TOR browser to see if it works.
Connecting to a node over TOR requires the proxy server to support it.

## Running Locally

Expand Down
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lnmessage",
"version": "0.0.19",
"version": "0.1.0",
"description": "Talk to Lightning nodes from your browser",
"main": "dist/index.js",
"type": "module",
Expand All @@ -14,8 +14,9 @@
"build": "tsc"
},
"devDependencies": {
"@types/crypto-js": "^4.1.1",
"@types/node": "^18.14.0",
"@types/secp256k1": "^4.0.3",
"@types/ws": "^8.5.4",
"@typescript-eslint/eslint-plugin": "^5.27.0",
"@typescript-eslint/parser": "^5.27.0",
"eslint": "^8.16.0",
Expand All @@ -24,9 +25,10 @@
"typescript": "^4.8.2"
},
"dependencies": {
"@noble/hashes": "^1.2.0",
"buffer": "^6.0.3",
"crypto-js": "^4.1.1",
"rxjs": "^7.5.7",
"secp256k1": "^4.0.3"
"secp256k1": "^5.0.0",
"ws": "^8.12.1"
}
}
4 changes: 2 additions & 2 deletions src/chacha/chacha20.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class Chacha20 {

this.cachePos = 64
this.buffer = new Uint32Array(16)
this.output = new Buffer(64)
this.output = Buffer.alloc(64)
}

quarterRound(a: number, b: number, c: number, d: number) {
Expand Down Expand Up @@ -91,7 +91,7 @@ class Chacha20 {

getBytes(len: number) {
let dpos = 0
const dst = new Buffer(len)
const dst = Buffer.alloc(len)
const cacheLen = 64 - this.cachePos

if (cacheLen) {
Expand Down
8 changes: 4 additions & 4 deletions src/chacha/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class Cipher {
}
this.alen = aad.length
this.poly.update(aad)
const padding = new Buffer(padAmount(this.alen))
const padding = Buffer.alloc(padAmount(this.alen))
if (padding.length) {
padding.fill(0)
this.poly.update(padding)
Expand Down Expand Up @@ -83,14 +83,14 @@ class Cipher {
throw new Error('Unsupported state or unable to authenticate data')
}

const padding = new Buffer(padAmount(this.clen))
const padding = Buffer.alloc(padAmount(this.clen))

if (padding.length) {
padding.fill(0)
this.poly.update(padding)
}

const lens = new Buffer(16)
const lens = Buffer.alloc(16)
lens.fill(0)
lens.writeUInt32LE(this.alen, 0)
lens.writeUInt32LE(this.clen, 8)
Expand All @@ -110,7 +110,7 @@ class Cipher {

getAuthTag() {
if (this._decrypt || this.tag === null) {
return new Buffer('')
return Buffer.from('')
}
return this.tag
}
Expand Down
4 changes: 2 additions & 2 deletions src/chacha/poly1305.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class Poly1305 {
public finished: number

constructor(key: Buffer) {
this.buffer = new Buffer(16)
this.buffer = Buffer.alloc(16)
this.leftover = 0
this.r = new Uint16Array(10)
this.h = new Uint16Array(10)
Expand Down Expand Up @@ -126,7 +126,7 @@ class Poly1305 {
}

finish() {
let mac = new Buffer(16),
let mac = Buffer.alloc(16),
g = new Uint16Array(10),
c = 0,
mask = 0,
Expand Down
32 changes: 15 additions & 17 deletions src/crypto.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,19 @@
import CryptoJS from 'crypto-js'
import { Buffer } from 'buffer'
import secp256k1 from '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 {
return Buffer.from(sha256Array(input))
}

export function ecdh(pubkey: Uint8Array, privkey: Uint8Array) {
return Buffer.from(secp256k1.ecdh(pubkey, privkey))
}

export function hmacHash(key: Buffer, input: Buffer) {
const words = CryptoJS.HmacSHA256(
CryptoJS.enc.Hex.parse(input.toString('hex')),
CryptoJS.enc.Hex.parse(key.toString('hex'))
)

return Buffer.from(CryptoJS.enc.Hex.stringify(words), 'hex')
}

export async function sha256(input: Buffer): Promise<Buffer> {
const res = await crypto.subtle.digest('SHA-256', input)
return Buffer.from(res)
return Buffer.from(hmac(sha256Array, key, input))
}

export function hkdf(ikm: Buffer, len: number, salt = Buffer.alloc(0), info = Buffer.alloc(0)) {
Expand Down Expand Up @@ -99,11 +94,10 @@ export function ccpDecrypt(k: Buffer, n: Buffer, ad: Buffer, ciphertext: Buffer)
export function createRandomPrivateKey(): string {
let privKey
do {
const bytes = Buffer.allocUnsafe(32)
privKey = crypto.getRandomValues(bytes)
} while (!validPrivateKey(privKey))
privKey = randomBytes(32)
} while (!validPrivateKey(Buffer.from(privKey)))

return privKey.toString('hex')
return bytesToHex(privKey)
}

export function validPublicKey(publicKey: string): boolean {
Expand All @@ -115,3 +109,7 @@ export function validPrivateKey(privateKey: string | Buffer): boolean {
typeof privateKey === 'string' ? Buffer.from(privateKey, 'hex') : privateKey
)
}

export function createRandomBytes(length: number) {
return randomBytes(length)
}
Loading

0 comments on commit 3d00360

Please sign in to comment.