diff --git a/plugins/.gitignore b/plugins/.gitignore index 389469cf0..3c340b67d 100644 --- a/plugins/.gitignore +++ b/plugins/.gitignore @@ -10,3 +10,4 @@ pay spenderp topology txprepare +chanbackup \ No newline at end of file diff --git a/plugins/Makefile b/plugins/Makefile index bae7bfa4e..c69297569 100644 --- a/plugins/Makefile +++ b/plugins/Makefile @@ -4,6 +4,9 @@ PLUGIN_PAY_OBJS := $(PLUGIN_PAY_SRC:.c=.o) PLUGIN_AUTOCLEAN_SRC := plugins/autoclean.c PLUGIN_AUTOCLEAN_OBJS := $(PLUGIN_AUTOCLEAN_SRC:.c=.o) +PLUGIN_chanbackup_SRC := plugins/chanbackup.c +PLUGIN_chanbackup_OBJS := $(PLUGIN_chanbackup_SRC:.c=.o) + PLUGIN_TOPOLOGY_SRC := plugins/topology.c PLUGIN_TOPOLOGY_OBJS := $(PLUGIN_TOPOLOGY_SRC:.c=.o) @@ -55,6 +58,7 @@ PLUGIN_FUNDER_OBJS := $(PLUGIN_FUNDER_SRC:.c=.o) PLUGIN_ALL_SRC := \ $(PLUGIN_AUTOCLEAN_SRC) \ + $(PLUGIN_chanbackup_SRC) \ $(PLUGIN_BCLI_SRC) \ $(PLUGIN_FETCHINVOICE_SRC) \ $(PLUGIN_FUNDER_SRC) \ @@ -77,6 +81,7 @@ PLUGIN_ALL_OBJS := $(PLUGIN_ALL_SRC:.c=.o) C_PLUGINS := \ plugins/autoclean \ + plugins/chanbackup \ plugins/bcli \ plugins/fetchinvoice \ plugins/funder \ @@ -140,6 +145,7 @@ PLUGIN_COMMON_OBJS := \ common/psbt_open.o \ common/pseudorand.o \ common/random_select.o \ + common/scb_wiregen.o \ common/setup.o \ common/status_levels.o \ common/type_to_string.o \ @@ -160,6 +166,8 @@ plugins/pay: bitcoin/chainparams.o $(PLUGIN_PAY_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGI plugins/autoclean: bitcoin/chainparams.o $(PLUGIN_AUTOCLEAN_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) $(JSMN_OBJS) $(CCAN_OBJS) +plugins/chanbackup: bitcoin/chainparams.o $(PLUGIN_chanbackup_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) $(JSMN_OBJS) $(CCAN_OBJS) + # Topology wants to decode node_announcement, and peer_wiregen which # pulls in some of bitcoin/. plugins/topology: common/route.o common/dijkstra.o common/gossmap.o common/fp16.o bitcoin/chainparams.o wire/peer$(EXP)_wiregen.o wire/channel_type_wiregen.o bitcoin/block.o bitcoin/preimage.o $(PLUGIN_TOPOLOGY_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) $(JSMN_OBJS) $(CCAN_OBJS) diff --git a/plugins/chanbackup.c b/plugins/chanbackup.c new file mode 100644 index 000000000..cd8376be4 --- /dev/null +++ b/plugins/chanbackup.c @@ -0,0 +1,419 @@ +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define HEADER_LEN crypto_secretstream_xchacha20poly1305_HEADERBYTES +#define ABYTES crypto_secretstream_xchacha20poly1305_ABYTES + +/* VERSION is the current version of the data encrypted in the file */ +#define VERSION ((u64)1) + +/* Global secret object to keep the derived encryption key for the SCB */ +static struct secret secret; + +/* Helper to fetch out SCB from the RPC call */ +static bool json_to_scb_chan(const char *buffer, + const jsmntok_t *tok, + struct scb_chan ***channels) +{ + size_t i; + const jsmntok_t *t; + *channels = tok->size ? tal_arr(tmpctx, + struct scb_chan *, + tok->size) : NULL; + + json_for_each_arr(i, t, tok) { + const u8 *scb_tmp = tal_hexdata(tmpctx, + json_strdup(tmpctx, + buffer, + t), + strlen(json_strdup(tmpctx, + buffer, + t))); + size_t scblen_tmp = tal_count(scb_tmp); + + (*channels)[i] = fromwire_scb_chan(tmpctx, + &scb_tmp, + &scblen_tmp); + } + + return true; +} + +/* This writes encrypted static backup in the recovery file */ +static void write_scb(struct plugin *p, + int fd, + struct scb_chan **scb_chan_arr) +{ + u32 timestamp = time_now().ts.tv_sec; + + u8 *decrypted_scb = towire_static_chan_backup(tmpctx, + VERSION, + timestamp, + cast_const2(const struct scb_chan **, + scb_chan_arr)); + + u8 *encrypted_scb = tal_arr(tmpctx, + u8, + tal_bytelen(decrypted_scb) + + ABYTES + + HEADER_LEN); + + crypto_secretstream_xchacha20poly1305_state crypto_state; + + if (crypto_secretstream_xchacha20poly1305_init_push(&crypto_state, + encrypted_scb, + (&secret)->data) != 0) + { + plugin_err(p, "Can't encrypt the data!"); + return; + } + + if (crypto_secretstream_xchacha20poly1305_push(&crypto_state, + encrypted_scb + + HEADER_LEN, + NULL, decrypted_scb, + tal_bytelen(decrypted_scb), + /* Additional data and tag */ + NULL, 0, 0)) { + plugin_err(p, "Can't encrypt the data!"); + return; + } + + if (!write_all(fd, encrypted_scb, tal_bytelen(encrypted_scb))) { + unlink_noerr("scb.tmp"); + plugin_err(p, "Writing encrypted SCB: %s", + strerror(errno)); + } + +} + +/* checks if the SCB file exists, creates a new one in case it doesn't. */ +static void maybe_create_new_scb(struct plugin *p, + struct scb_chan **channels) +{ + + /* Note that this is opened for write-only, even though the permissions + * are set to read-only. That's perfectly valid! */ + int fd = open("emergency.recover", O_CREAT|O_EXCL|O_WRONLY, 0400); + if (fd < 0) { + /* Don't do anything if the file already exists. */ + if (errno == EEXIST) + return; + plugin_err(p, "creating: %s", strerror(errno)); + } + + /* Comes here only if the file haven't existed before */ + unlink_noerr("emergency.recover"); + + /* This couldn't give EEXIST because we call unlink_noerr("scb.tmp") + * in INIT */ + fd = open("scb.tmp", O_CREAT|O_EXCL|O_WRONLY, 0400); + if (fd < 0) + plugin_err(p, "Opening: %s", strerror(errno)); + + plugin_log(p, LOG_INFORM, "Creating Emergency Recovery"); + + write_scb(p, fd, channels); + + /* fsync (mostly!) ensures that the file has reached the disk. */ + if (fsync(fd) != 0) { + unlink_noerr("scb.tmp"); + plugin_err(p, "fsync : %s", strerror(errno)); + } + + /* This should never fail if fsync succeeded. But paranoia good, and + * bugs exist. */ + if (close(fd) != 0) { + unlink_noerr("scb.tmp"); + plugin_err(p, "closing: %s", strerror(errno)); + } + + /* We actually need to sync the *directory itself* to make sure the + * file exists! You're only allowed to open directories read-only in + * modern Unix though. */ + fd = open(".", O_RDONLY); + if (fd < 0) + plugin_err(p, "Opening: %s", strerror(errno)); + + if (fsync(fd) != 0) { + unlink_noerr("scb.tmp"); + plugin_err(p, "closing: %s", strerror(errno)); + } + + /* This will never fail, if fsync worked! */ + close(fd); + + /* This will update the scb file */ + rename("scb.tmp", "emergency.recover"); +} + + +/* Returns decrypted SCB in form of a u8 array */ +static u8 *decrypt_scb(struct plugin *p) +{ + struct stat st; + int fd = open("emergency.recover", O_RDONLY); + + if (stat("emergency.recover", &st) != 0) + plugin_err(p, "SCB file is corrupted!: %s", + strerror(errno)); + + u8 final[st.st_size]; + + if (!read_all(fd, &final, st.st_size)) { + plugin_log(p, LOG_DBG, "SCB file is corrupted!: %s", + strerror(errno)); + return NULL; + } + + crypto_secretstream_xchacha20poly1305_state crypto_state; + + if (st.st_size < ABYTES + + HEADER_LEN) + plugin_err(p, "SCB file is corrupted!"); + + u8 *ans = tal_arr(tmpctx, u8, st.st_size - + ABYTES - + HEADER_LEN); + + /* The header part */ + if (crypto_secretstream_xchacha20poly1305_init_pull(&crypto_state, + final, + (&secret)->data) != 0) + { + plugin_err(p, "SCB file is corrupted!"); + } + + if (crypto_secretstream_xchacha20poly1305_pull(&crypto_state, ans, + NULL, 0, + final + + HEADER_LEN, + st.st_size - + HEADER_LEN, + NULL, 0) != 0) { + plugin_err(p, "SCB file is corrupted!"); + } + + if (close(fd) != 0) + plugin_err(p, "Closing: %s", strerror(errno)); + + return ans; +} + +static struct command_result *after_recover_rpc(struct command *cmd, + const char *buf, + const jsmntok_t *params, + void *cb_arg UNUSED) +{ + + size_t i; + const jsmntok_t *t; + struct json_stream *response; + + response = jsonrpc_stream_success(cmd); + + json_for_each_obj(i, t, params) + json_add_tok(response, json_strdup(tmpctx, buf, t), t+1, buf); + + return command_finished(cmd, response); +} + +/* Recovers the channels by making RPC to `recoverchannel` */ +static struct command_result *json_emergencyrecover(struct command *cmd, + const char *buf, + const jsmntok_t *params) +{ + struct out_req *req; + u64 version; + u32 timestamp; + struct scb_chan **scb; + + if (!param(cmd, buf, params, NULL)) + return command_param_failed(); + + u8 *res = decrypt_scb(cmd->plugin); + + if (!fromwire_static_chan_backup(cmd, + res, + &version, + ×tamp, + &scb)) { + plugin_err(cmd->plugin, "Corrupted SCB!"); + } + + if (version != VERSION) { + plugin_err(cmd->plugin, + "Incompatible version, Contact the admin!"); + } + + req = jsonrpc_request_start(cmd->plugin, cmd, "recoverchannel", + after_recover_rpc, + &forward_error, NULL); + + json_array_start(req->js, "scb"); + for (size_t i=0; ijs, NULL, scb_hex, tal_bytelen(scb_hex)); + } + json_array_end(req->js); + + return send_outreq(cmd->plugin, req); +} + +static void update_scb(struct plugin *p, struct scb_chan **channels) +{ + + /* If the temp file existed before, remove it */ + unlink_noerr("scb.tmp"); + + int fd = open("scb.tmp", O_CREAT|O_EXCL|O_WRONLY, 0400); + if (fd<0) + plugin_err(p, "Opening: %s", strerror(errno)); + + plugin_log(p, LOG_DBG, "Updating the SCB file..."); + + write_scb(p, fd, channels); + + /* fsync (mostly!) ensures that the file has reached the disk. */ + if (fsync(fd) != 0) { + unlink_noerr("scb.tmp"); + } + + /* This should never fail if fsync succeeded. But paranoia good, and + * bugs exist. */ + if (close(fd) != 0) { + unlink_noerr("scb.tmp"); + } + /* We actually need to sync the *directory itself* to make sure the + * file exists! You're only allowed to open directories read-only in + * modern Unix though. */ + fd = open(".", O_RDONLY); + if (fd < 0) { + plugin_log(p, LOG_DBG, "Opening: %s", strerror(errno)); + } + if (fsync(fd) != 0) { + unlink_noerr("scb.tmp"); + } + close(fd); + + /* This will atomically replace the main file */ + rename("scb.tmp", "emergency.recover"); +} + +static struct command_result *after_staticbackup(struct command *cmd, + const char *buf, + const jsmntok_t *params, + void *cb_arg UNUSED) +{ + struct scb_chan **scb_chan; + const jsmntok_t *scbs = json_get_member(buf, params, "scb"); + json_to_scb_chan(buf, scbs, &scb_chan); + plugin_log(cmd->plugin, LOG_INFORM, "Updating the SCB"); + + update_scb(cmd->plugin, scb_chan); + return notification_handled(cmd); +} + +static struct command_result *json_state_changed(struct command *cmd, + const char *buf, + const jsmntok_t *params) +{ + const jsmntok_t *notiftok = json_get_member(buf, + params, + "channel_state_changed"), + *statetok = json_get_member(buf, notiftok, "new_state"); + + /* FIXME: I wanted to update the file on CHANNELD_AWAITING_LOCKIN, + * But I don't get update for it, maybe because there is + * no previous_state, also apparently `channel_opened` gets published + * when *peer* funded a channel with us? + * So, is their no way to get a notif on CHANNELD_AWAITING_LOCKIN? */ + if (json_tok_streq(buf, statetok, "CLOSED") || + json_tok_streq(buf, statetok, "CHANNELD_NORMAL")) { + + struct out_req *req; + req = jsonrpc_request_start(cmd->plugin, + cmd, + "staticbackup", + after_staticbackup, + &forward_error, + NULL); + + return send_outreq(cmd->plugin, req); + } + + return notification_handled(cmd); +} + +static const char *init(struct plugin *p, + const char *buf UNUSED, + const jsmntok_t *config UNUSED) +{ + struct scb_chan **scb_chan; + const char *info = "scb secret"; + u8 *info_hex = tal_dup_arr(tmpctx, u8, (u8*)info, strlen(info), 0); + + rpc_scan(p, "staticbackup", + take(json_out_obj(NULL, NULL, NULL)), + "{scb:%}", JSON_SCAN(json_to_scb_chan, &scb_chan)); + + rpc_scan(p, "makesecret", + take(json_out_obj(NULL, "info_hex", + tal_hexstr(tmpctx, + info_hex, + tal_bytelen(info_hex)))), + "{secret:%}", JSON_SCAN(json_to_secret, &secret)); + + plugin_log(p, LOG_DBG, "Chanbackup Initialised!"); + + /* flush the tmp file, if exists */ + unlink_noerr("scb.tmp"); + + maybe_create_new_scb(p, scb_chan); + + return NULL; +} + +static const struct plugin_notification notifs[] = { + { + "channel_state_changed", + json_state_changed, + } +}; + +static const struct plugin_command commands[] = { { + "emergencyrecover", + "recovery", + "Populates the DB with stub channels", + "returns stub channel-id's on completion", + json_emergencyrecover, + } +}; + +int main(int argc, char *argv[]) +{ + setup_locale(); + plugin_main(argv, init, PLUGIN_RESTARTABLE, true, NULL, + commands, ARRAY_SIZE(commands), + notifs, ARRAY_SIZE(notifs), NULL, 0, + NULL, 0, /* Notification topics we publish */ + NULL); +}