lnsocket

A minimal C library for connecting to the lightning network
git clone git://jb55.com/lnsocket
Log | Files | Refs | Submodules | README | LICENSE

commit 71edace76a72da7328d9036da41748f92237775a
parent 496407d29ddae7db3a6bcb33e87eb73801d50a7e
Author: William Casarin <jb55@jb55.com>
Date:   Mon,  7 Mar 2022 20:14:11 -0800

lnsocket working in the browser!

This adds a wasm build target for lnsocket. See lnsocket_example.js for
a demo

Diffstat:
M.gitignore | 4++++
MMakefile | 43++++++++++++++++++++++++++++++++++++++++---
Mcommando.c | 12+++++-------
Mcommando.h | 3++-
Aexport.h | 12++++++++++++
Mhandshake.c | 110++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mhandshake.h | 3++-
Aindex.htm | 18++++++++++++++++++
Mlnsocket.c | 160++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mlnsocket.h | 20+++++++++++++++-----
Alnsocket_example.js | 18++++++++++++++++++
Mlnsocket_internal.h | 8++++++++
Alnsocket_lib.js | 261+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alnsocket_pre.js | 12++++++++++++
Alnsocket_wasm.c | 41+++++++++++++++++++++++++++++++++++++++++
Mrpc.c | 2+-
Mtest.c | 2+-
Atools/secp-wasm.sh | 21+++++++++++++++++++++
Atools/sodium-wasm.sh | 18++++++++++++++++++
19 files changed, 670 insertions(+), 98 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -10,3 +10,7 @@ /test TODO.bak *.a +*.wasm +/node_modules +lnsocket_module.js +lnsocket.js diff --git a/Makefile b/Makefile @@ -1,5 +1,5 @@ -CFLAGS=-Wall -g -Og -Ideps/secp256k1/include -Ideps/libsodium/src/libsodium/include -Ideps +CFLAGS=-Wall -g -O2 -Ideps/secp256k1/include -Ideps/libsodium/src/libsodium/include -Ideps LDFLAGS= SUBMODULES=deps/libsodium deps/secp256k1 @@ -10,9 +10,11 @@ SIM_SDK=$(XCODEDIR)/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimu IOS_SDK=$(XCODEDIR)/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk ARS=libsecp256k1.a libsodium.a +WASM_ARS=target/wasm/libsecp256k1.a target/wasm/libsodium.a target/wasm/lnsocket.a OBJS=sha256.o hkdf.o hmac.o sha512.o lnsocket.o error.o handshake.o crypto.o bigsize.o commando.o ARM64_OBJS=$(OBJS:.o=-arm64.o) X86_64_OBJS=$(OBJS:.o=-x86_64.o) +WASM_OBJS=$(OBJS:.o=-wasm.o) lnsocket_wasm-wasm.o BINS=test lnrpc DEPS=$(OBJS) $(ARS) config.h @@ -21,6 +23,11 @@ all: $(BINS) lnsocket.a ios: target/ios/lnsocket.a target/ios/libsodium.a target/ios/libsecp256k1.a +wasm: lnsocket.js + +lnsocket.js: lnsocket_module.js lnsocket_lib.js + cat lnsocket_module.js lnsocket_lib.js > $@ + deps/libsodium/.git: @tools/refresh-submodules.sh $(SUBMODULES) @@ -38,6 +45,10 @@ target/x86_64/lnsocket.a: $(X86_64_OBJS) @mkdir -p target/x86_64 ar rcs $@ $^ +target/wasm/lnsocket.a: $(WASM_OBJS) + @mkdir -p target/wasm + emar rcs $@ $^ + target/ios/lnsocket.a: target/x86_64/lnsocket.a target/arm64/lnsocket.a @mkdir -p target/ios lipo -create $^ -output $@ @@ -46,6 +57,10 @@ target/ios/lnsocket.a: target/x86_64/lnsocket.a target/arm64/lnsocket.a @echo "cc $@" @$(CC) $(CFLAGS) -c $< -o $@ -arch arm64 -isysroot $(IOS_SDK) -target arm64-apple-ios -fembed-bitcode +%-wasm.o: %.c config.h + @echo "emcc $@" + @emcc $(CFLAGS) -c $< -o $@ + %-x86_64.o: %.c config.h @echo "cc $@" @$(CC) $(CFLAGS) -c $< -o $@ -arch x86_64 -isysroot $(SIM_SDK) -mios-simulator-version-min=6.0.0 -target x86_64-apple-ios-simulator @@ -88,9 +103,19 @@ libsodium.a: deps/libsodium/src/libsodium/.libs/libsodium.a cp $< $@ target/ios/libsodium.a: deps/libsodium/libsodium-ios/lib/libsodium.a + mkdir -p target/ios cp $< $@ target/ios/libsecp256k1.a: deps/secp256k1/libsecp256k1-ios/lib/libsecp256k1.a + mkdir -p target/ios + cp $< $@ + +target/wasm/libsecp256k1.a: deps/secp256k1/libsecp256k1-wasm/lib/libsecp256k1.a + mkdir -p target/wasm + cp $< $@ + +target/wasm/libsodium.a: deps/libsodium/libsodium-wasm/lib/libsodium.a + mkdir -p target/wasm cp $< $@ deps/libsodium/libsodium-ios/lib/libsodium.a: @@ -100,10 +125,19 @@ deps/libsodium/libsodium-ios/lib/libsodium.a: deps/secp256k1/libsecp256k1-ios/lib/libsecp256k1.a: ./tools/secp-ios.sh +deps/secp256k1/libsecp256k1-wasm/lib/libsecp256k1.a: + ./tools/secp-wasm.sh + +deps/libsodium/libsodium-wasm/lib/libsodium.a: + ./tools/sodium-wasm.sh + deps/libsodium/src/libsodium/.libs/libsodium.a: deps/libsodium/config.status cd deps/libsodium/src/libsodium; \ make -j2 libsodium.la +check: test + @./test + test: test.o $(DEPS) $(ARS) @echo "ld test" @$(CC) $(CFLAGS) test.o $(OBJS) $(ARS) $(LDFLAGS) -o $@ @@ -112,14 +146,17 @@ lnrpc: rpc.o $(DEPS) $(ARS) @echo "ld lnrpc" @$(CC) $(CFLAGS) rpc.o $(OBJS) $(ARS) $(LDFLAGS) -o $@ +lnsocket_module.js: $(WASM_ARS) lnsocket_pre.js + emcc --pre-js lnsocket_pre.js -s ENVIRONMENT=web -s MODULARIZE -s EXPORTED_RUNTIME_METHODS=ccall,cwrap $(CFLAGS) -Wl,-whole-archive $(WASM_ARS) -Wl,-no-whole-archive -o $@ + tags: fake find . -name '*.c' -or -name '*.h' | xargs ctags clean: fake - rm -rf $(BINS) config.h $(OBJS) $(ARM64_OBJS) $(X86_64_OBJS) target + rm -rf $(BINS) config.h $(OBJS) $(ARM64_OBJS) $(X86_64_OBJS) $(WASM_OBJS) target distclean: clean - rm -rf $(ARS) deps/secp256k1/src/libsecp256k1-config.h deps/libsodium/libsodium-ios deps/secp256k1/libsecp256k1-ios + rm -rf $(ARS) deps/secp256k1/src/libsecp256k1-config.h deps/libsodium/libsodium-ios deps/secp256k1/libsecp256k1-ios deps/libsodium/libsodium-wasm deps/secp256k1/libsecp256k1-wasm lnsocket.wasm cd deps/secp256k1; \ make distclean cd deps/libsodium; \ diff --git a/commando.c b/commando.c @@ -3,10 +3,10 @@ #include "cursor.h" #include "endian.h" #include "commando.h" +#include "export.h" -int commando_make_rpc_msg(const char *method, const char *params, - const char *rune, uint64_t req_id, unsigned char *buf, - int buflen, unsigned short *outlen) +int EXPORT commando_make_rpc_msg(const char *method, const char *params, + const char *rune, unsigned int req_id, unsigned char *buf, int buflen) { struct cursor msgbuf; int ok; @@ -18,7 +18,7 @@ int commando_make_rpc_msg(const char *method, const char *params, if (!cursor_push_u16(&msgbuf, COMMANDO_CMD)) return 0; - if (!cursor_push_u64(&msgbuf, req_id)) + if (!cursor_push_u64(&msgbuf, (u64)req_id)) return 0; ok = cursor_push_str(&msgbuf, "{\"method\":\"") && @@ -32,7 +32,5 @@ int commando_make_rpc_msg(const char *method, const char *params, if (!ok) return 0; - *outlen = msgbuf.p - msgbuf.start; - - return 1; + return msgbuf.p - msgbuf.start; } diff --git a/commando.h b/commando.h @@ -3,11 +3,12 @@ #define LNSOCKET_COMMANDO #include <inttypes.h> +#include "export.h" #define COMMANDO_CMD 0x4c4f #define COMMANDO_REPLY_CONTINUES 0x594b #define COMMANDO_REPLY_TERM 0x594d -int commando_make_rpc_msg(const char *method, const char *params, const char *rune, uint64_t req_id, unsigned char *buf, int buflen, unsigned short *outlen); +int EXPORT commando_make_rpc_msg(const char *method, const char *params, const char *rune, unsigned int req_id, unsigned char *buf, int buflen); #endif /* LNSOCKET_COMMANDO */ diff --git a/export.h b/export.h @@ -0,0 +1,12 @@ + +#ifndef LNSOCKET_COMMON_H +#define LNSOCKET_COMMON_H + +#ifdef __EMSCRIPTEN__ + #include <emscripten.h> + #define EXPORT EMSCRIPTEN_KEEPALIVE +#else + #define EXPORT +#endif + +#endif diff --git a/handshake.c b/handshake.c @@ -31,6 +31,7 @@ THE SOFTWARE. #include <secp256k1_ecdh.h> #include <unistd.h> #include <sodium/randombytes.h> +#include "export.h" struct keypair generate_key(secp256k1_context *ctx) { @@ -232,10 +233,11 @@ static int handshake_success(struct lnsocket *ln, struct handshake *h) return 1; } -static int act_three_initiator(struct lnsocket *ln, struct handshake *h) +static struct act_three *build_act_three(struct lnsocket *ln) { u8 spub[PUBKEY_CMPR_LEN]; size_t len = sizeof(spub); + struct handshake *h = &ln->handshake; /* BOLT #8: * 1. `c = encryptWithAD(temp_k2, 1, h, s.pub.serializeCompressed())` @@ -258,8 +260,10 @@ static int act_three_initiator(struct lnsocket *ln, struct handshake *h) * * where `re` is the ephemeral public key of the responder */ if (!secp256k1_ecdh(ln->secp, h->ss.data, &h->re.pubkey, - ln->key.priv.secret.data, NULL, NULL)) - return note_error(&ln->errs, "act3 ecdh handshake failed"); + ln->key.priv.secret.data, NULL, NULL)) { + note_error(&ln->errs, "act3 ecdh handshake failed"); + return NULL; + } /* BOLT #8: * @@ -284,40 +288,30 @@ static int act_three_initiator(struct lnsocket *ln, struct handshake *h) */ h->act3.v = 0; - if (write(ln->socket, &h->act3, ACT_THREE_SIZE) != ACT_THREE_SIZE) { - return note_error(&ln->errs, "handshake failed on initial send"); - } + handshake_success(ln, &ln->handshake); - return handshake_success(ln, h); + return &h->act3; } -// act2: read the response to the message sent in act1 -static int act_two_initiator(struct lnsocket *ln, struct handshake *h) -{ - /* BOLT #8: - * - * 1. Read _exactly_ 50 bytes from the network buffer. - * - * 2. Parse the read message (`m`) into `v`, `re`, and `c`: - * * where `v` is the _first_ byte of `m`, `re` is the next 33 - * bytes of `m`, and `c` is the last 16 bytes of `m`. - */ - ssize_t size; - if ((size = read(ln->socket, &h->act2, ACT_TWO_SIZE)) != ACT_TWO_SIZE) { - printf("read %ld bytes, expected %d\n", size, ACT_TWO_SIZE); - return note_error(&ln->errs, "%s", strerror(errno)); - } +// act2: handle the response to the message sent in act1 +struct act_three* EXPORT lnsocket_act_two(struct lnsocket *ln, struct act_two *act2) +{ + struct handshake *h = &ln->handshake; - //print_act_two(&h->act2); + //print_act_two(act2); /* BOLT #8: * * 3. If `v` is an unrecognized handshake version, then the responder * MUST abort the connection attempt. */ - if (h->act2.v != 0) - return note_error(&ln->errs, "unrecognized handshake version"); + if (act2->v != 0) { + note_error(&ln->errs, "unrecognized handshake version"); + return NULL; + } + + //print_hex() /* BOLT #8: * @@ -326,9 +320,10 @@ static int act_two_initiator(struct lnsocket *ln, struct handshake *h) * affine coordinates as encoded by the key's serialized * composed format. */ - if (secp256k1_ec_pubkey_parse(ln->secp, &h->re.pubkey, h->act2.pubkey, - sizeof(h->act2.pubkey)) != 1) { - return note_error(&ln->errs, "failed to parse remote pubkey"); + if (secp256k1_ec_pubkey_parse(ln->secp, &h->re.pubkey, act2->pubkey, + sizeof(act2->pubkey)) != 1) { + note_error(&ln->errs, "failed to parse remote pubkey"); + return NULL; } /* BOLT #8: @@ -343,7 +338,8 @@ static int act_two_initiator(struct lnsocket *ln, struct handshake *h) */ if (!secp256k1_ecdh(ln->secp, h->ss.data, &h->re.pubkey, h->e.priv.secret.data, NULL, NULL)) { - return note_error(&ln->errs, "act2 ecdh failed"); + note_error(&ln->errs, "act2 ecdh failed"); + return NULL; } /* BOLT #8: @@ -361,8 +357,9 @@ static int act_two_initiator(struct lnsocket *ln, struct handshake *h) * MUST terminate the connection without any further messages. */ if (!decrypt(&h->temp_k, 0, &h->h, sizeof(h->h), - h->act2.tag, sizeof(h->act2.tag), NULL, 0)) { - return note_error(&ln->errs, "handshake decrypt failed"); + act2->tag, sizeof(act2->tag), NULL, 0)) { + note_error(&ln->errs, "handshake decrypt failed"); + return NULL; } /* BOLT #8: @@ -372,15 +369,17 @@ static int act_two_initiator(struct lnsocket *ln, struct handshake *h) * This step serves to ensure the payload wasn't modified by a * MITM. */ - sha_mix_in(&h->h, h->act2.tag, sizeof(h->act2.tag)); + sha_mix_in(&h->h, act2->tag, sizeof(act2->tag)); - return act_three_initiator(ln, h); + return build_act_three(ln); } // Prepare the very first message and send it the connected node // Wait for a response in act2 -int act_one_initiator(struct lnsocket *ln, struct handshake *h) +int act_one_initiator_prep(struct lnsocket *ln) { + struct handshake *h = &ln->handshake; + h->e = generate_key(ln->secp); /* BOLT #8: @@ -437,9 +436,46 @@ int act_one_initiator(struct lnsocket *ln, struct handshake *h) check_act_one(&h->act1); - if (write(ln->socket, &h->act1, ACT_ONE_SIZE) != ACT_ONE_SIZE) { + return 1; +} + +// act2: read the response to the message sent in act1 +static int act_two_initiator(struct lnsocket *ln, struct handshake *h) +{ + /* BOLT #8: + * + * 1. Read _exactly_ 50 bytes from the network buffer. + * + * 2. Parse the read message (`m`) into `v`, `re`, and `c`: + * * where `v` is the _first_ byte of `m`, `re` is the next 33 + * bytes of `m`, and `c` is the last 16 bytes of `m`. + */ + ssize_t size; + + if ((size = read(ln->socket, &h->act2, ACT_TWO_SIZE)) != ACT_TWO_SIZE) { + printf("read %ld bytes, expected %d\n", size, ACT_TWO_SIZE); + return note_error(&ln->errs, "%s", strerror(errno)); + } + + struct act_three *act3 = lnsocket_act_two(ln, &h->act2); + if (act3 == NULL) + return 0; + + if (write(ln->socket, act3, ACT_THREE_SIZE) != ACT_THREE_SIZE) { + return note_error(&ln->errs, "handshake failed on initial send"); + } + + return 1; +} + +int act_one_initiator(struct lnsocket *ln) +{ + if (!act_one_initiator_prep(ln)) + return 0; + + if (write(ln->socket, &ln->handshake.act1, ACT_ONE_SIZE) != ACT_ONE_SIZE) { return note_error(&ln->errs, "handshake failed on initial send"); } - return act_two_initiator(ln, h); + return act_two_initiator(ln, &ln->handshake); } diff --git a/handshake.h b/handshake.h @@ -140,7 +140,8 @@ void new_handshake(secp256k1_context *secp, struct handshake *handshake, struct lnsocket; -int act_one_initiator(struct lnsocket *ln, struct handshake *h); +int act_one_initiator_prep(struct lnsocket *ln); +int act_one_initiator(struct lnsocket *ln); struct keypair generate_key(secp256k1_context *ctx); #endif /* LNLINK_HANDSHAKE_H */ diff --git a/index.htm b/index.htm @@ -0,0 +1,18 @@ + +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + + <title>Hello World</title> + </head> + <body> + <h1>Hello World</h1> + <script src="lnsocket.js"></script> + <script src="lnsocket_example.js"></script> + <script> + </script> + </body> +</html> + diff --git a/lnsocket.c b/lnsocket.c @@ -71,12 +71,12 @@ static int hex_decode(const char *str, size_t slen, void *buf, size_t bufsize) } -static int parse_node_id(const char *str, struct node_id *dest) +int parse_node_id(const char *str, struct node_id *dest) { return hex_decode(str, strlen(str), dest->k, sizeof(*dest)); } -static int pubkey_from_node_id(secp256k1_context *secp, struct pubkey *key, +int pubkey_from_node_id(secp256k1_context *secp, struct pubkey *key, const struct node_id *id) { return secp256k1_ec_pubkey_parse(secp, &key->pubkey, @@ -102,18 +102,13 @@ static int read_all(int fd, void *data, size_t size) return 1; } -int lnsocket_perform_init(struct lnsocket *ln) +int EXPORT lnsocket_make_default_initmsg(unsigned char *msgbuf, int buflen) { u8 tlvbuf[1024]; - u8 msgbuf[1024]; + struct tlv network_tlv; u8 global_features[2] = {0}; u8 features[5] = {0}; - struct tlv network_tlv; u16 len; - u8 *buf; - - if (!lnsocket_read(ln, &buf, &len)) - return 0; const u8 genesis_block[] = { 0x6f, 0xe2, 0x8c, 0x0a, 0xb6, 0xf1, 0xb3, 0x72, 0xc1, 0xa6, 0xa2, 0x46, @@ -129,13 +124,36 @@ int lnsocket_perform_init(struct lnsocket *ln) const struct tlv *init_tlvs[] = { &network_tlv } ; - if (!lnsocket_make_init_msg(msgbuf, sizeof(msgbuf), + if (!lnsocket_make_init_msg(msgbuf, buflen, global_features, sizeof(global_features), features, sizeof(features), init_tlvs, 1, &len)) return 0; + return (int)len; +} + +static void print_hex(u8 *bytes, int len) { + int i; + for (i = 0; i < len; ++i) { + printf("%02x", bytes[i]); + } +} + +int lnsocket_perform_init(struct lnsocket *ln) +{ + u8 msgbuf[1024]; + u16 len; + u8 *buf; + + // read the init message from the other side and ignore it + if (!lnsocket_read(ln, &buf, &len)) + return 0; + + if (!(len = lnsocket_make_default_initmsg(msgbuf, sizeof(msgbuf)))) + return 0; + if (!lnsocket_write(ln, msgbuf, len)) return 0; @@ -180,27 +198,24 @@ int lnsocket_recv(struct lnsocket *ln, u16 *msg_type, unsigned char **payload, u return 1; } -int lnsocket_read(struct lnsocket *ln, unsigned char **buf, unsigned short *len) +int EXPORT lnsocket_decrypt(struct lnsocket *ln, unsigned char *packet, int len) { - struct cursor enc, dec; + struct cursor read, enc, dec; u8 hdr[18]; u16 size; - reset_cursor(&ln->errs.cur); - reset_cursor(&ln->msgbuf); - - if (!read_all(ln->socket, hdr, sizeof(hdr))) - return note_error(&ln->errs,"Failed reading header: %s", - strerror(errno)); + make_cursor(packet, packet + len, &read); + if (!cursor_pull(&read, hdr, 18)) { + return note_error(&ln->errs, "not enough bytes in header, have %d, need 18", len); + } - if (!cryptomsg_decrypt_header(&ln->crypto_state, hdr, &size)) + if (!cryptomsg_decrypt_header(&ln->crypto_state, hdr, &size)) { return note_error(&ln->errs, "Failed hdr decrypt with rn=%"PRIu64, ln->crypto_state.rn-1); + } - if (!cursor_slice(&ln->msgbuf, &enc, size + 16)) - return note_error(&ln->errs, "out of memory"); - + reset_cursor(&ln->msgbuf); if (!cursor_slice(&ln->msgbuf, &dec, size)) return note_error(&ln->errs, "out of memory: %d + %d = %d > %d", ln->msgbuf.end - ln->msgbuf.p, size, @@ -208,19 +223,61 @@ int lnsocket_read(struct lnsocket *ln, unsigned char **buf, unsigned short *len) MSGBUF_MEM ); - if (!read_all(ln->socket, enc.p, enc.end - enc.start)) - return note_error(&ln->errs, "Failed reading body: %s", - strerror(errno)); + if (size + 16 != read.end - read.p) + return note_error(&ln->errs, "expected enc body size of %d, got %d", + size + 16, read.end - read.p); + + make_cursor(read.p, read.end, &enc); if (!cryptomsg_decrypt_body(&ln->crypto_state, enc.start, enc.end - enc.start, dec.start, dec.end - dec.start)) return note_error(&ln->errs, "error decrypting body"); - *buf = dec.start; - *len = dec.end - dec.start; + return dec.end - dec.start; +} - return 1; +int lnsocket_read(struct lnsocket *ln, unsigned char **buf, unsigned short *len) +{ + struct cursor enc, dec; + u8 hdr[18]; + u16 size; + + reset_cursor(&ln->errs.cur); + reset_cursor(&ln->msgbuf); + + if (!read_all(ln->socket, hdr, sizeof(hdr))) + return note_error(&ln->errs,"Failed reading header: %s", + strerror(errno)); + + if (!cryptomsg_decrypt_header(&ln->crypto_state, hdr, &size)) + return note_error(&ln->errs, + "Failed hdr decrypt with rn=%"PRIu64, + ln->crypto_state.rn-1); + + if (!cursor_slice(&ln->msgbuf, &enc, size + 16)) + return note_error(&ln->errs, "out of memory"); + + if (!cursor_slice(&ln->msgbuf, &dec, size)) + return note_error(&ln->errs, "out of memory: %d + %d = %d > %d", + ln->msgbuf.end - ln->msgbuf.p, size, + ln->msgbuf.end - ln->msgbuf.p + size, + MSGBUF_MEM + ); + + if (!read_all(ln->socket, enc.p, enc.end - enc.start)) + return note_error(&ln->errs, "Failed reading body: %s", + strerror(errno)); + + if (!cryptomsg_decrypt_body(&ln->crypto_state, + enc.start, enc.end - enc.start, + dec.start, dec.end - dec.start)) + return note_error(&ln->errs, "error decrypting body"); + + *buf = dec.start; + *len = dec.end - dec.start; + + return 1; } static int highest_byte(unsigned char *buf, int buflen) @@ -296,7 +353,7 @@ int lnsocket_make_network_tlv(unsigned char *buf, int buflen, return 1; } -int lnsocket_make_ping_msg(unsigned char *buf, int buflen, u16 num_pong_bytes, u16 ignored_bytes, u16 *outlen) +int lnsocket_make_ping_msg(unsigned char *buf, int buflen, u16 num_pong_bytes, u16 ignored_bytes) { struct cursor msg; int i; @@ -314,9 +371,7 @@ int lnsocket_make_ping_msg(unsigned char *buf, int buflen, u16 num_pong_bytes, u return 0; } - *outlen = msg.p - msg.start; - - return 1; + return msg.p - msg.start; } int lnsocket_make_init_msg(unsigned char *buf, int buflen, @@ -353,20 +408,26 @@ int lnsocket_make_init_msg(unsigned char *buf, int buflen, return 1; } -int lnsocket_write(struct lnsocket *ln, const u8 *msg, unsigned short msglen) +unsigned char* EXPORT lnsocket_msgbuf(struct lnsocket *ln) +{ + return ln->msgbuf.start; +} + +int EXPORT lnsocket_encrypt(struct lnsocket *ln, const u8 *msg, unsigned short msglen) { - ssize_t writelen, outcap; + ssize_t outcap; size_t outlen; - u8 *out; // this is just temporary so we don't need to move the memory cursor reset_cursor(&ln->msgbuf); - out = ln->msgbuf.start; + u8 *out = ln->msgbuf.start; outcap = ln->msgbuf.end - ln->msgbuf.start; +#ifndef __EMSCRIPTEN__ if (!ln->socket) return note_error(&ln->errs, "not connected"); +#endif if (outcap <= 0) return note_error(&ln->errs, "out of memory"); @@ -374,6 +435,17 @@ int lnsocket_write(struct lnsocket *ln, const u8 *msg, unsigned short msglen) if (!cryptomsg_encrypt_msg(&ln->crypto_state, msg, msglen, out, &outlen, (size_t)outcap)) return note_error(&ln->errs, "encrypt message failed, out of memory"); + return outlen; +} + +int lnsocket_write(struct lnsocket *ln, const u8 *msg, unsigned short msglen) +{ + ssize_t writelen, outlen; + u8 *out = ln->msgbuf.start; + + if (!(outlen = lnsocket_encrypt(ln, msg, msglen))) + return 0; + if ((writelen = write(ln->socket, out, outlen)) != outlen) return note_error(&ln->errs, "write failed. wrote %ld bytes, expected %ld %s", @@ -423,7 +495,7 @@ void lnsocket_destroy(struct lnsocket *lnsocket) free(lnsocket->mem.start); } -static int is_zero(void *vp, int size) +int is_zero(void *vp, int size) { u8 *p = (u8*)vp; const u8 *start = p; @@ -471,7 +543,6 @@ int lnsocket_connect_with(struct lnsocket *ln, const char *node_id, const char * { int ret; struct addrinfo *addrs = NULL; - struct handshake h; struct pubkey their_id; struct node_id their_node_id; struct timeval timeout = {0}; @@ -523,13 +594,13 @@ int lnsocket_connect_with(struct lnsocket *ln, const char *node_id, const char * } // prepare some data for ACT1 - new_handshake(ln->secp, &h, &their_id); + new_handshake(ln->secp, &ln->handshake, &their_id); - h.side = INITIATOR; - h.their_id = their_id; + ln->handshake.side = INITIATOR; + ln->handshake.their_id = their_id; // let's do this! - return act_one_initiator(ln, &h); + return act_one_initiator(ln); } int lnsocket_connect(struct lnsocket *ln, const char *node_id, const char *host) @@ -543,6 +614,11 @@ int lnsocket_fd(struct lnsocket *ln, int *fd) return 1; } +void * lnsocket_secp(struct lnsocket *ln) +{ + return ln->secp; +} + void lnsocket_genkey(struct lnsocket *ln) { ln->key = generate_key(ln->secp); diff --git a/lnsocket.h b/lnsocket.h @@ -6,6 +6,13 @@ struct lnsocket; +#ifdef __EMSCRIPTEN__ + #include <emscripten.h> + #define EXPORT EMSCRIPTEN_KEEPALIVE +#else + #define EXPORT +#endif + enum peer_wire { WIRE_INIT = 16, WIRE_ERROR = 17, @@ -58,12 +65,12 @@ struct tlv { unsigned char *value; }; -struct lnsocket *lnsocket_create(); +struct lnsocket EXPORT *lnsocket_create(); /* messages */ int lnsocket_make_network_tlv(unsigned char *buf, int buflen, const unsigned char **blockids, int num_blockids, struct tlv *tlv_out); -int lnsocket_make_ping_msg(unsigned char *buf, int buflen, unsigned short num_pong_bytes, unsigned short ignored_bytes, unsigned short *outlen); +int EXPORT lnsocket_make_ping_msg(unsigned char *buf, int buflen, unsigned short num_pong_bytes, unsigned short ignored_bytes); int lnsocket_make_init_msg(unsigned char *buf, int buflen, const unsigned char *globalfeatures, unsigned short gflen, const unsigned char *features, unsigned short flen, const struct tlv **tlvs, unsigned short num_tlvs, unsigned short *outlen); int lnsocket_perform_init(struct lnsocket *ln); @@ -77,8 +84,11 @@ int lnsocket_read(struct lnsocket *, unsigned char **buf, unsigned short *len); int lnsocket_send(struct lnsocket *, unsigned short msg_type, const unsigned char *payload, unsigned short payload_len); int lnsocket_recv(struct lnsocket *, unsigned short *msg_type, unsigned char **payload, unsigned short *payload_len); -void lnsocket_genkey(struct lnsocket *); -void lnsocket_destroy(struct lnsocket *); -void lnsocket_print_errors(struct lnsocket *); +void* EXPORT lnsocket_secp(struct lnsocket *); +void EXPORT lnsocket_genkey(struct lnsocket *); +void EXPORT lnsocket_destroy(struct lnsocket *); +void EXPORT lnsocket_print_errors(struct lnsocket *); +int EXPORT lnsocket_make_default_initmsg(unsigned char *msgbuf, int buflen); +int EXPORT lnsocket_encrypt(struct lnsocket *ln, const unsigned char *msg, unsigned short msglen); #endif /* LNSOCKET_H */ diff --git a/lnsocket_example.js b/lnsocket_example.js @@ -0,0 +1,18 @@ + +async function go() { + const LNSocket = await lnsocket_init() + const ln = LNSocket() + + ln.genkey() + const their_init = await ln.connect_and_init("03f3c108ccd536b8526841f0a5c58212bb9e6584a1eb493080e7c1cc34f82dad71", "cln.jb55.com:443") + + const pong = await ln.ping_pong() + console.log("pong", pong) + + const rune = "CVFjq11ZtQ-V14SOeQyV4V2AicYZnHfbBNY8lqYvf8c9NjcmbWV0aG9kPWdldGluZm8mdGltZTwxNjQ2ODc2MTU2" + const res = await ln.rpc({ method: "getinfo", rune }) + + document.body.innerHTML = `<pre>${JSON.stringify(res.result, undefined, 2)}</pre>` +} + +go() diff --git a/lnsocket_internal.h b/lnsocket_internal.h @@ -2,6 +2,8 @@ #define LNSOCKET_INTERNAL_H #include "crypto.h" +#include "error.h" +#include "handshake.h" struct lnsocket { const char *errors[8]; @@ -11,8 +13,14 @@ struct lnsocket { int num_errors; int socket; struct keypair key; + struct pubkey responder_id; + struct handshake handshake; struct crypto_state crypto_state; secp256k1_context *secp; }; +int parse_node_id(const char *str, struct node_id *dest); +int pubkey_from_node_id(secp256k1_context *secp, struct pubkey *key, const struct node_id *id); +int is_zero(void *vp, int size); + #endif /* LNSOCKET_INTERNAL_H */ diff --git a/lnsocket_lib.js b/lnsocket_lib.js @@ -0,0 +1,261 @@ + +async function lnsocket_init() { + const module = await Module() + + const ACT_ONE_SIZE = 50 + const ACT_TWO_SIZE = 50 + const ACT_THREE_SIZE = 66 + const DEFAULT_TIMEOUT = 3000 + + const COMMANDO_REPLY_CONTINUES = 0x594b + const COMMANDO_REPLY_TERM = 0x594d + + const lnsocket_create = module.cwrap("lnsocket_create", "number") + const lnsocket_encrypt = module.cwrap("lnsocket_encrypt", "number", ["int", "array", "int", "int"]) + const lnsocket_decrypt = module.cwrap("lnsocket_decrypt", "number", ["int", "array", "int"]) + const lnsocket_msgbuf = module.cwrap("lnsocket_msgbuf", "number", ["int"]) + const lnsocket_act_one = module.cwrap("lnsocket_act_one", "number", ["number", "string"]) + const lnsocket_act_two = module.cwrap("lnsocket_act_two", "number", ["number", "array"]) + const lnsocket_print_errors = module.cwrap("lnsocket_print_errors", "int") + const lnsocket_genkey = module.cwrap("lnsocket_genkey", "int") + const lnsocket_make_default_initmsg = module.cwrap("lnsocket_make_default_initmsg", "int", ["int", "int"]) + const lnsocket_make_ping_msg = module.cwrap("lnsocket_make_ping_msg", "int", ["int", "int", "int", "int"]) + const commando_make_rpc_msg = module.cwrap("commando_make_rpc_msg", "int", ["string", "string", "string", "number", "int", "int"]) + + function concat_u8_arrays(arrays) { + // sum of individual array lengths + let totalLength = arrays.reduce((acc, value) => acc + value.length, 0); + + if (!arrays.length) return null; + + let result = new Uint8Array(totalLength); + + let length = 0; + for (let array of arrays) { + result.set(array, length); + length += array.length; + } + + return result; + } + + function queue_recv(queue) { + return new Promise((resolve, reject) => { + const checker = setInterval(() => { + const val = queue.shift() + if (val) { + clearInterval(checker) + resolve(val) + } + }, 5); + }) + } + + function parse_msgtype(buf) { + return buf[0] << 8 | buf[1] + } + + function wasm_mem(ptr, size) { + return new Uint8Array(module.HEAPU8.buffer, ptr, size); + } + + function LNSocket(opts) { + if (!(this instanceof LNSocket)) + return new LNSocket(opts) + + this.opts = opts || { + timeout: DEFAULT_TIMEOUT + } + this.queue = [] + this.ln = lnsocket_create() + } + + LNSocket.prototype.print_errors = function _lnsocket_print_errors() { + lnsocket_print_errors(this.ln) + } + + LNSocket.prototype.genkey = function _lnsocket_genkey() { + lnsocket_genkey(this.ln) + } + + LNSocket.prototype.act_one_data = function _lnsocket_act_one(node_id) { + const act_one_ptr = lnsocket_act_one(this.ln, node_id) + if (act_one_ptr === 0) + return null + return wasm_mem(act_one_ptr, ACT_ONE_SIZE) + } + + LNSocket.prototype.act_two = function _lnsocket_act_two(act2) { + const act_three_ptr = lnsocket_act_two(this.ln, new Uint8Array(act2)) + if (act_three_ptr === 0) { + this.print_errors() + return null + } + return wasm_mem(act_three_ptr, ACT_THREE_SIZE) + } + + LNSocket.prototype.connect = async function lnsocket_connect(node_id, host) { + await handle_connect(this, node_id, host) + + const act1 = this.act_one_data(node_id) + this.ws.send(act1) + const act2 = await this.read_clear() + if (act2.byteLength != ACT_TWO_SIZE) { + throw new Error(`expected act2 to be ${ACT_TWO_SIZE} long, got ${act2.length}`) + } + const act3 = this.act_two(act2) + this.ws.send(act3) + } + + LNSocket.prototype.connect_and_init = async function _connect_and_init(node_id, host) { + await this.connect(node_id, host) + await this.perform_init() + } + + LNSocket.prototype.rpc = async function lnsocket_rpc(opts) { + const msg = this.make_commando_msg(opts) + this.write(msg) + return JSON.parse(await this.read_all_rpc()) + } + + LNSocket.prototype.recv = async function lnsocket_recv() { + const msg = await this.read() + console.log("recv", msg) + const msgtype = parse_msgtype(msg.slice(0,2)) + return [msgtype, msg.slice(2)] + } + + LNSocket.prototype.read_all_rpc = async function read_all_rpc() { + let chunks = [] + while (true) { + const [typ, msg] = await this.recv() + switch (typ) { + case COMMANDO_REPLY_TERM: + chunks.push(msg.slice(8)) + return new TextDecoder().decode(concat_u8_arrays(chunks)); + case COMMANDO_REPLY_CONTINUES: + chunks.push(msg.slice(8)) + break + default: + console.log("got unknown type", typ) + continue + } + } + } + + LNSocket.prototype.make_commando_msg = function _lnsocket_make_commando_msg(opts) { + const buflen = 4096 + let len = 0; + const buf = module._malloc(buflen); + module.HEAPU8.set(Uint8Array, buf); + + const params = JSON.stringify(opts.params||{}) + if (!(len = commando_make_rpc_msg(opts.method, params, opts.rune, + 0, buf, buflen))) { + throw new Error("couldn't make commando msg"); + } + + const dat = wasm_mem(buf, len) + module._free(buf); + return dat + } + + LNSocket.prototype.make_ping_msg = function _lnsocket_make_ping_msg(num_pong_bytes=1, ignored_bytes=1) { + const buflen = 32 + let len = 0; + const buf = module._malloc(buflen); + module.HEAPU8.set(Uint8Array, buf); + + if (!(len = lnsocket_make_ping_msg(buf, buflen, num_pong_bytes, ignored_bytes))) + throw new Error("couldn't make ping msg"); + + const dat = wasm_mem(buf, len) + module._free(buf); + return dat + } + + LNSocket.prototype.encrypt = function _lnsocket_encrypt(dat) { + const len = lnsocket_encrypt(this.ln, dat, dat.length) + if (len === 0) { + this.print_errors() + throw new Error("encrypt error") + } + const enc = wasm_mem(lnsocket_msgbuf(this.ln), len) + return enc + } + + LNSocket.prototype.decrypt = function _lnsocket_decrypt(dat) { + const len = lnsocket_decrypt(this.ln, dat, dat.length) + if (len === 0) { + this.print_errors() + throw new Error("decrypt error") + } + return wasm_mem(lnsocket_msgbuf(this.ln), len) + } + + LNSocket.prototype.write = function _lnsocket_write(dat) { + this.ws.send(this.encrypt(dat)) + } + + LNSocket.prototype.read_clear = async function _lnsocket_read() { + return (await queue_recv(this.queue)) + } + + LNSocket.prototype.read = async function _lnsocket_read() { + const enc = await this.read_clear() + return this.decrypt(new Uint8Array(enc)) + } + + LNSocket.prototype.make_default_initmsg = function _lnsocket_make_default_initmsg() { + const buflen = 1024 + let len = 0; + const buf = module._malloc(buflen); + module.HEAPU8.set(Uint8Array, buf); + + if (!(len = lnsocket_make_default_initmsg(buf, buflen))) + throw new Error("couldn't make initmsg"); + + const dat = wasm_mem(buf, len) + module._free(buf); + return dat + } + + LNSocket.prototype.perform_init = async function lnsocket_connect() { + await this.read() + const our_init = this.make_default_initmsg() + console.log("our_init", our_init) + this.write(our_init) + } + + LNSocket.prototype.ping_pong = async function lnsocket_ping_pong() { + const pingmsg = this.make_ping_msg() + console.log("ping", pingmsg) + this.write(pingmsg) + return await this.read() + } + + function handle_connect(ln, node_id, host) { + const ws = new WebSocket(`wss://${host}`) + return new Promise((resolve, reject) => { + ws.onmessage = (v) => { + ln.queue.push(v.data.arrayBuffer()) + } + + ws.addEventListener('open', function(ev) { + ln.ws = ws + resolve(ws) + }); + + ws.addEventListener('close', function(ev) { + reject() + }); + + const timeout = ln.opts.timeout || DEFAULT_TIMEOUT + setTimeout(reject, timeout); + }) + } + + return LNSocket +} + +Module.init = lnsocket_init diff --git a/lnsocket_pre.js b/lnsocket_pre.js @@ -0,0 +1,12 @@ +Module.getRandomValue = (function() { + const window_ = "object" === typeof window ? window : self + const crypto_ = typeof window_.crypto !== "undefined" ? window_.crypto : window_.msCrypto; + + function randomValuesStandard() { + var buf = new Uint32Array(1); + crypto_.getRandomValues(buf); + return buf[0] >>> 0; + }; + + return randomValuesStandard +})() diff --git a/lnsocket_wasm.c b/lnsocket_wasm.c @@ -0,0 +1,41 @@ + +#include "handshake.h" +#include "lnsocket_internal.h" +#include <emscripten.h> + +struct act_three* lnsocket_act_two(struct lnsocket *ln, struct act_two *act2); + +void* EMSCRIPTEN_KEEPALIVE lnsocket_act_one(struct lnsocket *ln, const char *node_id) +{ + struct pubkey their_id; + struct node_id their_node_id; + + if (is_zero(&ln->key, sizeof(ln->key))) { + note_error(&ln->errs, "key not initialized, use lnsocket_set_key() or lnsocket_genkey()"); + return NULL; + } + + if (!parse_node_id(node_id, &their_node_id)) { + note_error(&ln->errs, "failed to parse node id"); + return NULL; + } + + if (!pubkey_from_node_id(ln->secp, &their_id, &their_node_id)) { + note_error(&ln->errs, "failed to convert node_id to pubkey"); + return NULL; + } + + new_handshake(ln->secp, &ln->handshake, &their_id); + + printf("their_id node_id %s\n", node_id); + + ln->handshake.side = INITIATOR; + ln->handshake.their_id = their_id; + + if (!act_one_initiator_prep(ln)) { + note_error(&ln->errs, "failed to build initial handshake data (act1)"); + return NULL; + } + + return &ln->handshake.act1; +} diff --git a/rpc.c b/rpc.c @@ -69,7 +69,7 @@ int main(int argc, const char *argv[]) if (verbose) fprintf(stderr, "init success\n"); - if (!(ok = commando_make_rpc_msg(method, params, rune, 1, msgbuf, sizeof(msgbuf), &len))) + if (!(ok = len = commando_make_rpc_msg(method, params, rune, 1, msgbuf, sizeof(msgbuf)))) goto done; if (!(ok = lnsocket_write(ln, msgbuf, len))) diff --git a/test.c b/test.c @@ -38,7 +38,7 @@ int main(int argc, const char *argv[]) printf("init ok!\n"); - if (!(ok = lnsocket_make_ping_msg(msgbuf, sizeof(msgbuf), 1, 1, &len))) + if (!(ok = len = lnsocket_make_ping_msg(msgbuf, sizeof(msgbuf), 1, 1))) goto done; if (!(ok = lnsocket_write(ln, msgbuf, len))) diff --git a/tools/secp-wasm.sh b/tools/secp-wasm.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +cd deps/secp256k1 + +export CC=emcc +export AR=emar + +export PREFIX="$(pwd)/libsecp256k1-wasm" + +mkdir -p $PREFIX || exit 1 + +make distclean > /dev/null + +./configure --disable-shared \ + --enable-module-ecdh \ + --prefix="$PREFIX" + +make -j3 install || exit 1 + +rm -rf -- "$PREFIX/tmp" +make distclean > /dev/null diff --git a/tools/sodium-wasm.sh b/tools/sodium-wasm.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +cd deps/libsodium + +export CC=emcc +export AR=emar + +export PREFIX="$(pwd)/libsodium-wasm" + +mkdir -p $PREFIX || exit 1 + +make distclean > /dev/null + +./configure --disable-shared --enable-minimal --disable-ssp --prefix="$PREFIX" + +make -j3 install || exit 1 + +make distclean > /dev/null