diff --git a/common/Makefile b/common/Makefile index 8c2d6e87b5df..71dc75e91a70 100644 --- a/common/Makefile +++ b/common/Makefile @@ -32,6 +32,7 @@ COMMON_SRC_NOGEN := \ common/gossip_store.c \ common/hash_u5.c \ common/hmac.c \ + common/hsm_encryption.c \ common/htlc_state.c \ common/htlc_trim.c \ common/htlc_tx.c \ diff --git a/common/hsm_encryption.c b/common/hsm_encryption.c new file mode 100644 index 000000000000..d33c0ff56ba6 --- /dev/null +++ b/common/hsm_encryption.c @@ -0,0 +1,115 @@ +#include +#include +#include +#include + +char *hsm_secret_encryption_key(const char *pass, struct secret *key) +{ + u8 salt[16] = "c-lightning\0\0\0\0\0"; + + /* Don't swap the encryption key ! */ + if (sodium_mlock(key->data, sizeof(key->data)) != 0) + return "Could not lock hsm_secret encryption key memory."; + + /* Check bounds. */ + if (strlen(pass) < crypto_pwhash_argon2id_PASSWD_MIN) + return "Password too short to be able to derive a key from it."; + if (strlen(pass) > crypto_pwhash_argon2id_PASSWD_MAX) + return "Password too long to be able to derive a key from it."; + + /* Now derive the key. */ + if (crypto_pwhash(key->data, sizeof(key->data), pass, strlen(pass), salt, + /* INTERACTIVE needs 64 MiB of RAM, MODERATE needs 256, + * and SENSITIVE needs 1024. */ + crypto_pwhash_argon2id_OPSLIMIT_MODERATE, + crypto_pwhash_argon2id_MEMLIMIT_MODERATE, + crypto_pwhash_ALG_ARGON2ID13) != 0) + return "Could not derive a key from the password."; + + return NULL; +} + +bool encrypt_hsm_secret(const struct secret *encryption_key, + const struct secret *hsm_secret, + struct encrypted_hsm_secret *output) +{ + crypto_secretstream_xchacha20poly1305_state crypto_state; + + if (crypto_secretstream_xchacha20poly1305_init_push(&crypto_state, output->data, + encryption_key->data) != 0) + return false; + if (crypto_secretstream_xchacha20poly1305_push(&crypto_state, + output->data + HS_HEADER_LEN, + NULL, hsm_secret->data, + sizeof(hsm_secret->data), + /* Additional data and tag */ + NULL, 0, 0)) + return false; + + return true; +} + +bool decrypt_hsm_secret(const struct secret *encryption_key, + const struct encrypted_hsm_secret *cipher, + struct secret *output) +{ + crypto_secretstream_xchacha20poly1305_state crypto_state; + + /* The header part */ + if (crypto_secretstream_xchacha20poly1305_init_pull(&crypto_state, cipher->data, + encryption_key->data) != 0) + return false; + /* The ciphertext part */ + if (crypto_secretstream_xchacha20poly1305_pull(&crypto_state, output->data, + NULL, 0, + cipher->data + HS_HEADER_LEN, + HS_CIPHERTEXT_LEN, + NULL, 0) != 0) + return false; + + return true; +} + +void discard_key(struct secret *key TAKES) +{ + /* sodium_munlock() also zeroes the memory. */ + sodium_munlock(key->data, sizeof(key->data)); + if (taken(key)) + tal_free(key); +} + +char *read_stdin_pass(char **reason) +{ + struct termios current_term, temp_term; + char *passwd = NULL; + size_t passwd_size = 0; + + /* Set a temporary term, same as current but with ECHO disabled. */ + if (tcgetattr(fileno(stdin), ¤t_term) != 0) { + *reason = "Could not get current terminal options."; + return NULL; + } + temp_term = current_term; + temp_term.c_lflag &= ~ECHO; + if (tcsetattr(fileno(stdin), TCSAFLUSH, &temp_term) != 0) { + *reason = "Could not disable pass echoing."; + return NULL; + } + + /* Read the password, do not take the newline character into account. */ + if (getline(&passwd, &passwd_size, stdin) < 0) { + *reason = "Could not read pass from stdin."; + return NULL; + } + if (passwd[strlen(passwd) - 1] == '\n') + passwd[strlen(passwd) - 1] = '\0'; + + /* Restore the original terminal */ + if (tcsetattr(fileno(stdin), TCSAFLUSH, ¤t_term) != 0) { + *reason = "Could not restore terminal options."; + free(passwd); + return NULL; + } + + return passwd; +} diff --git a/common/hsm_encryption.h b/common/hsm_encryption.h new file mode 100644 index 000000000000..e620e1d6c06c --- /dev/null +++ b/common/hsm_encryption.h @@ -0,0 +1,64 @@ +#ifndef LIGHTNING_COMMON_HSM_ENCRYPTION_H +#define LIGHTNING_COMMON_HSM_ENCRYPTION_H +#include "config.h" +#include +#include +#include +#include + +/* Length of the encrypted hsm secret header. */ +#define HS_HEADER_LEN crypto_secretstream_xchacha20poly1305_HEADERBYTES +/* From libsodium: "The ciphertext length is guaranteed to always be message + * length + ABYTES" */ +#define HS_CIPHERTEXT_LEN \ + (sizeof(struct secret) + crypto_secretstream_xchacha20poly1305_ABYTES) +/* Total length of an encrypted hsm_secret */ +#define ENCRYPTED_HSM_SECRET_LEN (HS_HEADER_LEN + HS_CIPHERTEXT_LEN) + +struct encrypted_hsm_secret { + u8 data[ENCRYPTED_HSM_SECRET_LEN]; +}; + +/** Derive the hsm_secret encryption key from a passphrase. + * @pass: the passphrase string. + * @encryption_key: the output key derived from the passphrase. + * + * On success, NULL is returned. On error, a human-readable error is. + */ +char *hsm_secret_encryption_key(const char *pass, struct secret *encryption_key); + +/** Encrypt the hsm_secret using a previously derived encryption key. + * @encryption_key: the key derived from the passphrase. + * @hsm_secret: the plaintext hsm_secret to encrypt. + * @output: the resulting encrypted hsm_secret. + * + * Return false on encryption failure. + */ +bool encrypt_hsm_secret(const struct secret *encryption_key, + const struct secret *hsm_secret, + struct encrypted_hsm_secret *output); + +/** Decrypt the hsm_secret using a previously derived encryption key. + * @encryption_key: the key derived from the passphrase. + * @cipher: the encrypted hsm_secret to decrypt. + * @output: the resulting hsm_secret. + * + * Return false on decryption failure. + */ +bool decrypt_hsm_secret(const struct secret *encryption_key, + const struct encrypted_hsm_secret *cipher, + struct secret *output); + +/** Unlock and zeroize the encryption key memory after use. + * @key: the encryption key. If taken, it will be tal_free'd + */ +void discard_key(struct secret *key TAKES); + +/** Read hsm_secret encryption pass from stdin, disabling echoing. + * @reason: if NULL is returned, will point to the human-readable error. + * + * Caller must free the string as it does tal-reallocate getline's output. + */ +char *read_stdin_pass(char **reason); + +#endif /* LIGHTNING_COMMON_HSM_ENCRYPTION_H */ diff --git a/hsmd/Makefile b/hsmd/Makefile index 397f732b14fb..dadbe280a3c6 100644 --- a/hsmd/Makefile +++ b/hsmd/Makefile @@ -26,6 +26,7 @@ HSMD_COMMON_OBJS := \ common/derive_basepoints.o \ common/status_wiregen.o \ common/hash_u5.o \ + common/hsm_encryption.o \ common/key_derive.o \ common/memleak.o \ common/msg_queue.o \ diff --git a/hsmd/hsmd.c b/hsmd/hsmd.c index 0acd0bf53e23..2356945fc1b7 100644 --- a/hsmd/hsmd.c +++ b/hsmd/hsmd.c @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -562,28 +563,16 @@ static void bitcoin_key(struct privkey *privkey, struct pubkey *pubkey, */ static void create_encrypted_hsm(int fd, const struct secret *encryption_key) { - crypto_secretstream_xchacha20poly1305_state crypto_state; - u8 header[crypto_secretstream_xchacha20poly1305_HEADERBYTES]; - /* The cipher size is static with xchacha20poly1305 */ - u8 cipher[sizeof(struct secret) + crypto_secretstream_xchacha20poly1305_ABYTES]; - - crypto_secretstream_xchacha20poly1305_init_push(&crypto_state, header, - encryption_key->data); - crypto_secretstream_xchacha20poly1305_push(&crypto_state, cipher, - NULL, - secretstuff.hsm_secret.data, - sizeof(secretstuff.hsm_secret.data), - /* Additional data and tag */ - NULL, 0, 0); - if (!write_all(fd, header, sizeof(header))) { - unlink_noerr("hsm_secret"); + struct encrypted_hsm_secret cipher; + + if (!encrypt_hsm_secret(encryption_key, &secretstuff.hsm_secret, + &cipher)) status_failed(STATUS_FAIL_INTERNAL_ERROR, - "Writing header of encrypted secret: %s", strerror(errno)); - } - if (!write_all(fd, cipher, sizeof(cipher))) { + "Encrypting hsm_secret"); + if (!write_all(fd, cipher.data, ENCRYPTED_HSM_SECRET_LEN)) { unlink_noerr("hsm_secret"); status_failed(STATUS_FAIL_INTERNAL_ERROR, - "Writing encrypted secret: %s", strerror(errno)); + "Writing encrypted hsm_secret: %s", strerror(errno)); } } @@ -677,7 +666,7 @@ static void load_hsm(const struct secret *encryption_key) "stating: %s", strerror(errno)); /* If the seed is stored in clear. */ - if (st.st_size <= 32) { + if (st.st_size == 32) { if (!read_all(fd, &secretstuff.hsm_secret, sizeof(secretstuff.hsm_secret))) status_failed(STATUS_FAIL_INTERNAL_ERROR, "reading: %s", strerror(errno)); @@ -697,35 +686,29 @@ static void load_hsm(const struct secret *encryption_key) "opening: %s", strerror(errno)); } } - /*~ If an encryption key was passed and the `hsm_secret` is stored + /* If an encryption key was passed and the `hsm_secret` is stored * encrypted, recover the seed from the cipher. */ - if (encryption_key && st.st_size > 32) { - crypto_secretstream_xchacha20poly1305_state crypto_state; - u8 header[crypto_secretstream_xchacha20poly1305_HEADERBYTES]; - /* The cipher size is static with xchacha20poly1305 */ - u8 cipher[sizeof(struct secret) + crypto_secretstream_xchacha20poly1305_ABYTES]; + else if (st.st_size == ENCRYPTED_HSM_SECRET_LEN) { + struct encrypted_hsm_secret encrypted_secret; - if (!read_all(fd, &header, crypto_secretstream_xchacha20poly1305_HEADERBYTES)) - status_failed(STATUS_FAIL_INTERNAL_ERROR, - "Reading xchacha20 header: %s", strerror(errno)); - if (!read_all(fd, cipher, sizeof(cipher))) - status_failed(STATUS_FAIL_INTERNAL_ERROR, - "Reading encrypted secret: %s", strerror(errno)); - if (crypto_secretstream_xchacha20poly1305_init_pull(&crypto_state, header, - encryption_key->data) != 0) + /* hsm_control must have checked it! */ + assert(encryption_key); + + if (!read_all(fd, encrypted_secret.data, ENCRYPTED_HSM_SECRET_LEN)) status_failed(STATUS_FAIL_INTERNAL_ERROR, - "Initializing the crypto state: %s", strerror(errno)); - if (crypto_secretstream_xchacha20poly1305_pull(&crypto_state, - secretstuff.hsm_secret.data, - NULL, 0, cipher, sizeof(cipher), - NULL, 0) != 0) { + "Reading encrypted hsm_secret: %s", strerror(errno)); + if (!decrypt_hsm_secret(encryption_key, &encrypted_secret, + &secretstuff.hsm_secret)) { /* Exit but don't throw a backtrace when the user made a mistake in typing * its password. Instead exit and `lightningd` will be able to give * an error message. */ exit(1); } } - /* else { handled in hsm_control } */ + else + status_failed(STATUS_FAIL_INTERNAL_ERROR, "Invalid hsm_secret, " + "no plaintext nor encrypted" + " seed."); close(fd); populate_secretstuff(); @@ -779,12 +762,9 @@ static struct io_plan *init_hsm(struct io_conn *conn, maybe_create_new_hsm(hsm_encryption_key, true); load_hsm(hsm_encryption_key); - /*~ We don't need the hsm_secret encryption key anymore. - * Note that sodium_munlock() also zeroes the memory. */ - if (hsm_encryption_key) { - sodium_munlock(hsm_encryption_key, sizeof(*hsm_encryption_key)); - tal_free(hsm_encryption_key); - } + /*~ We don't need the hsm_secret encryption key anymore. */ + if (hsm_encryption_key) + discard_key(take(hsm_encryption_key)); /*~ We tell lightning our node id and (public) bip32 seed. */ node_key(NULL, &key); diff --git a/lightningd/Makefile b/lightningd/Makefile index 91ac153553a1..f9b176996e63 100644 --- a/lightningd/Makefile +++ b/lightningd/Makefile @@ -92,6 +92,7 @@ LIGHTNINGD_COMMON_OBJS := \ common/gossip_rcvd_filter.o \ common/hash_u5.o \ common/hmac.o \ + common/hsm_encryption.o \ common/htlc_state.o \ common/htlc_trim.o \ common/htlc_wire.o \ diff --git a/lightningd/hsm_control.c b/lightningd/hsm_control.c index 15e5eb3886ac..b05e5a2192e8 100644 --- a/lightningd/hsm_control.c +++ b/lightningd/hsm_control.c @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -106,7 +107,8 @@ struct ext_key *hsm_init(struct lightningd *ld) * actual secret. */ if (!ld->config.keypass) { struct stat st; - if (stat("hsm_secret", &st) == 0 && st.st_size > 32) + if (stat("hsm_secret", &st) == 0 && + st.st_size == ENCRYPTED_HSM_SECRET_LEN) errx(1, "hsm_secret is encrypted, you need to pass the " "--encrypted-hsm startup option."); } diff --git a/lightningd/lightningd.c b/lightningd/lightningd.c index 26f2694a9728..c7f7f2e60145 100644 --- a/lightningd/lightningd.c +++ b/lightningd/lightningd.c @@ -59,6 +59,7 @@ #include #include #include +#include #include #include #include @@ -878,7 +879,7 @@ int main(int argc, char *argv[]) /*~ If hsm_secret is encrypted, we don't need its encryption key * anymore. Note that sodium_munlock() also zeroes the memory.*/ if (ld->config.keypass) - sodium_munlock(ld->config.keypass->data, sizeof(ld->config.keypass->data)); + discard_key(take(ld->config.keypass)); /*~ Our default color and alias are derived from our node id, so we * can only set those now (if not set by config options). */ diff --git a/lightningd/options.c b/lightningd/options.c index 119569f01ea9..e5b9297e38bc 100644 --- a/lightningd/options.c +++ b/lightningd/options.c @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -388,16 +389,7 @@ static char *opt_important_plugin(const char *arg, struct lightningd *ld) static char *opt_set_hsm_password(struct lightningd *ld) { struct termios current_term, temp_term; - char *passwd = NULL; - size_t passwd_size = 0; - u8 salt[16] = "c-lightning\0\0\0\0\0"; - ld->encrypted_hsm = true; - - ld->config.keypass = tal(NULL, struct secret); - /* Don't swap the encryption key ! */ - if (sodium_mlock(ld->config.keypass->data, - sizeof(ld->config.keypass->data)) != 0) - return "Could not lock hsm_secret encryption key memory."; + char *passwd, *passwd_confirmation, *err; /* Get the password from stdin, but don't echo it. */ if (tcgetattr(fileno(stdin), ¤t_term) != 0) @@ -408,32 +400,28 @@ static char *opt_set_hsm_password(struct lightningd *ld) return "Could not disable password echoing."; printf("The hsm_secret is encrypted with a password. In order to " "decrypt it and start the node you must provide the password.\n"); - printf("Enter hsm_secret password: "); + printf("Enter hsm_secret password:\n"); /* If we don't flush we might end up being buffered and we might seem * to hang while we wait for the password. */ fflush(stdout); - if (getline(&passwd, &passwd_size, stdin) < 0) - return "Could not read password from stdin."; - if (passwd[strlen(passwd) - 1] == '\n') - passwd[strlen(passwd) - 1] = '\0'; - if (tcsetattr(fileno(stdin), TCSAFLUSH, ¤t_term) != 0) - return "Could not restore terminal options."; + passwd = read_stdin_pass(&err); + if (!passwd) + return err; + printf("Confirm hsm_secret password:\n"); + fflush(stdout); + passwd_confirmation = read_stdin_pass(&err); + if (!passwd_confirmation) + return err; printf("\n"); - /* Derive the key from the password. */ - if (strlen(passwd) < crypto_pwhash_argon2id_PASSWD_MIN) - return "Password too short to be able to derive a key from it."; - if (strlen(passwd) > crypto_pwhash_argon2id_PASSWD_MAX) - return "Password too long to be able to derive a key from it."; - if (crypto_pwhash(ld->config.keypass->data, sizeof(ld->config.keypass->data), - passwd, strlen(passwd), salt, - /* INTERACTIVE needs 64 MiB of RAM, MODERATE needs 256, - * and SENSITIVE needs 1024. */ - crypto_pwhash_argon2id_OPSLIMIT_MODERATE, - crypto_pwhash_argon2id_MEMLIMIT_MODERATE, - crypto_pwhash_ALG_ARGON2ID13) != 0) - return "Could not derive a key from the password."; + ld->config.keypass = tal(NULL, struct secret); + err = hsm_secret_encryption_key(passwd, ld->config.keypass); + if (err) + return err; + ld->encrypted_hsm = true; free(passwd); + free(passwd_confirmation); + return NULL; } diff --git a/lightningd/test/run-find_my_abspath.c b/lightningd/test/run-find_my_abspath.c index 142cbfb5099b..13a64342c604 100644 --- a/lightningd/test/run-find_my_abspath.c +++ b/lightningd/test/run-find_my_abspath.c @@ -50,6 +50,9 @@ s64 db_get_intvar(struct db *db UNNEEDED, char *varname UNNEEDED, s64 defval UNN /* Generated stub for db_in_transaction */ bool db_in_transaction(struct db *db UNNEEDED) { fprintf(stderr, "db_in_transaction called!\n"); abort(); } +/* Generated stub for discard_key */ +void discard_key(struct secret *key TAKES UNNEEDED) +{ fprintf(stderr, "discard_key called!\n"); abort(); } /* Generated stub for ecdh_hsmd_setup */ void ecdh_hsmd_setup(int hsm_fd UNNEEDED, void (*failed)(enum status_failreason UNNEEDED, diff --git a/tests/fuzz/Makefile b/tests/fuzz/Makefile index f37f3f1b6c55..602ee5e0f43e 100644 --- a/tests/fuzz/Makefile +++ b/tests/fuzz/Makefile @@ -23,6 +23,7 @@ FUZZ_COMMON_OBJS := \ common/derive_basepoints.o \ common/descriptor_checksum.o \ common/fee_states.o \ + common/hsm_encryption.o \ common/htlc_state.o \ common/permute_tx.o \ common/initial_channel.o \ @@ -46,6 +47,7 @@ FUZZ_COMMON_OBJS := \ wire/onion_wiregen.o \ wire/peer_wire.o \ wire/peer_wiregen.o \ + wire/tlvstream.o \ wire/towire.o \ wire/wire_io.o \ wire/wire_sync.o diff --git a/tests/fuzz/fuzz-hsm_encryption.c b/tests/fuzz/fuzz-hsm_encryption.c new file mode 100644 index 000000000000..b3b779b8f6e6 --- /dev/null +++ b/tests/fuzz/fuzz-hsm_encryption.c @@ -0,0 +1,43 @@ +#include +#include + +#include +#include +#include +#include +#include +#include + +void init(int *argc, char ***argv) +{ +} + +void run(const uint8_t *data, size_t size) +{ + /* 4294967295 is crypto_pwhash_argon2id_PASSWD_MAX. libfuzzer won't + * generate inputs that large in practice, but hey. */ + if (size > 32 && size < 4294967295) { + struct secret *hsm_secret, decrypted_hsm_secret, encryption_key; + char *passphrase; + struct encrypted_hsm_secret encrypted_secret; + + /* Take the first 32 bytes as the plaintext hsm_secret seed, + * and the remaining ones as the passphrase. */ + hsm_secret = (struct secret *)tal_dup_arr(NULL, u8, data, 32, 0); + passphrase = to_string(NULL, data + 32, size - 32); + + /* A valid seed, a valid passphrase. This should not fail. */ + assert(!hsm_secret_encryption_key(passphrase, &encryption_key)); + /* Roundtrip */ + assert(encrypt_hsm_secret(&encryption_key, hsm_secret, + &encrypted_secret)); + assert(decrypt_hsm_secret(&encryption_key, &encrypted_secret, + &decrypted_hsm_secret)); + assert(memeq(hsm_secret->data, sizeof(hsm_secret->data), + decrypted_hsm_secret.data, + sizeof(decrypted_hsm_secret.data))); + + tal_free(hsm_secret); + tal_free(passphrase); + } +} diff --git a/tests/fuzz/libfuzz.c b/tests/fuzz/libfuzz.c index ca9c4cfdcfe5..a940fcb08f5f 100644 --- a/tests/fuzz/libfuzz.c +++ b/tests/fuzz/libfuzz.c @@ -34,10 +34,11 @@ const uint8_t **get_chunks(const void *ctx, const uint8_t *data, char *to_string(const tal_t *ctx, const u8 *data, size_t data_size) { - char *string = tal_arr(ctx, char, data_size); + char *string = tal_arr(ctx, char, data_size + 1); for (size_t i = 0; i < data_size; i++) string[i] = (char) data[i] % (CHAR_MAX + 1); + string[data_size] = '\0'; return string; } diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 36ce7b63121f..41b6d781cffa 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -956,8 +956,9 @@ def test_hsm_secret_encryption(node_factory): l1.stop() l1.daemon.opts.update({"encrypted-hsm": None}) l1.daemon.start(stdin=slave_fd, wait_for_initialized=False) - l1.daemon.wait_for_log(r'The hsm_secret is encrypted') - + l1.daemon.wait_for_log(r'Enter hsm_secret password') + os.write(master_fd, password.encode("utf-8")) + l1.daemon.wait_for_log(r'Confirm hsm_secret password') os.write(master_fd, password.encode("utf-8")) l1.daemon.wait_for_log("Server started with public key") id = l1.rpc.getinfo()["id"] @@ -972,7 +973,9 @@ def test_hsm_secret_encryption(node_factory): l1.daemon.opts.update({"encrypted-hsm": None}) l1.daemon.start(stdin=slave_fd, stderr=subprocess.STDOUT, wait_for_initialized=False) - l1.daemon.wait_for_log(r'The hsm_secret is encrypted') + l1.daemon.wait_for_log(r'Enter hsm_secret password') + os.write(master_fd, password[2:].encode("utf-8")) + l1.daemon.wait_for_log(r'Confirm hsm_secret password') os.write(master_fd, password[2:].encode("utf-8")) assert(l1.daemon.proc.wait() == 1) assert(l1.daemon.is_in_log("Wrong password for encrypted hsm_secret.")) @@ -981,6 +984,8 @@ def test_hsm_secret_encryption(node_factory): l1.daemon.start(stdin=slave_fd, wait_for_initialized=False) l1.daemon.wait_for_log(r'The hsm_secret is encrypted') os.write(master_fd, password.encode("utf-8")) + l1.daemon.wait_for_log(r'Confirm hsm_secret password') + os.write(master_fd, password.encode("utf-8")) l1.daemon.wait_for_log("Server started with public key") assert id == l1.rpc.getinfo()["id"] @@ -1005,7 +1010,9 @@ def test_hsmtool_secret_decryption(node_factory): l1.stop() l1.daemon.opts.update({"encrypted-hsm": None}) l1.daemon.start(stdin=slave_fd, wait_for_initialized=False) - l1.daemon.wait_for_log(r'The hsm_secret is encrypted') + l1.daemon.wait_for_log(r'Enter hsm_secret password') + os.write(master_fd, password.encode("utf-8")) + l1.daemon.wait_for_log(r'Confirm hsm_secret password') os.write(master_fd, password.encode("utf-8")) l1.daemon.wait_for_log("Server started with public key") node_id = l1.rpc.getinfo()["id"] @@ -1038,6 +1045,8 @@ def test_hsmtool_secret_decryption(node_factory): stdout=subprocess.PIPE, stderr=subprocess.PIPE) hsmtool.wait_for_log(r"Enter hsm_secret password:") os.write(master_fd, password.encode("utf-8")) + hsmtool.wait_for_log(r"Confirm hsm_secret password:") + os.write(master_fd, password.encode("utf-8")) assert hsmtool.proc.wait(5) == 0 # Now we need to pass the encrypted-hsm startup option l1.stop() @@ -1051,6 +1060,8 @@ def test_hsmtool_secret_decryption(node_factory): l1.daemon.wait_for_log(r'The hsm_secret is encrypted') os.write(master_fd, password.encode("utf-8")) + l1.daemon.wait_for_log(r'Confirm hsm_secret password') + os.write(master_fd, password.encode("utf-8")) l1.daemon.wait_for_log("Server started with public key") print(node_id, l1.rpc.getinfo()["id"]) assert node_id == l1.rpc.getinfo()["id"] diff --git a/tools/Makefile b/tools/Makefile index d4c5a4fc18e9..b4dc87710065 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -17,7 +17,7 @@ tools/headerversions: FORCE tools/headerversions.o $(CCAN_OBJS) tools/check-bolt: tools/check-bolt.o $(CCAN_OBJS) $(TOOLS_COMMON_OBJS) -tools/hsmtool: tools/hsmtool.o $(CCAN_OBJS) $(TOOLS_COMMON_OBJS) $(BITCOIN_OBJS) common/amount.o common/bech32.o common/bigsize.o common/configdir.o common/derive_basepoints.o common/descriptor_checksum.o common/node_id.o common/type_to_string.o common/version.o wire/fromwire.o wire/towire.o +tools/hsmtool: tools/hsmtool.o $(CCAN_OBJS) $(TOOLS_COMMON_OBJS) $(BITCOIN_OBJS) common/amount.o common/bech32.o common/bigsize.o common/configdir.o common/derive_basepoints.o common/descriptor_checksum.o common/hsm_encryption.o common/node_id.o common/type_to_string.o common/version.o wire/fromwire.o wire/towire.o tools/lightning-hsmtool: tools/hsmtool cp $< $@ diff --git a/tools/hsmtool.c b/tools/hsmtool.c index eecf54fd9206..bf76a3d8ccfc 100644 --- a/tools/hsmtool.c +++ b/tools/hsmtool.c @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -18,9 +19,7 @@ #include #include #include -#include #include -#include #include #include @@ -77,9 +76,9 @@ static void get_hsm_secret(struct secret *hsm_secret, fd = open(hsm_secret_path, O_RDONLY); if (fd < 0) - err(ERROR_HSM_FILE, "Could not open hsm_secret"); + errx(ERROR_HSM_FILE, "Could not open hsm_secret"); if (!read_all(fd, hsm_secret, sizeof(*hsm_secret))) - err(ERROR_HSM_FILE, "Could not read hsm_secret"); + errx(ERROR_HSM_FILE, "Could not read hsm_secret"); close(fd); } @@ -91,33 +90,21 @@ static void get_encrypted_hsm_secret(struct secret *hsm_secret, { int fd; struct secret key; - u8 salt[16] = "c-lightning\0\0\0\0\0"; - crypto_secretstream_xchacha20poly1305_state crypto_state; - u8 header[crypto_secretstream_xchacha20poly1305_HEADERBYTES]; - /* The cipher size is static with xchacha20poly1305. */ - u8 cipher[sizeof(struct secret) + crypto_secretstream_xchacha20poly1305_ABYTES]; + struct encrypted_hsm_secret encrypted_secret; + char *err; fd = open(hsm_secret_path, O_RDONLY); if (fd < 0) - err(ERROR_HSM_FILE, "Could not open hsm_secret"); - - if (!read_all(fd, header, crypto_secretstream_xchacha20poly1305_HEADERBYTES)) - err(ERROR_HSM_FILE, "Could not read cipher header"); - if (!read_all(fd, cipher, sizeof(cipher))) - err(ERROR_HSM_FILE, "Could not read cipher body"); - - if (crypto_pwhash(key.data, sizeof(key.data), passwd, strlen(passwd), salt, - crypto_pwhash_argon2id_OPSLIMIT_MODERATE, - crypto_pwhash_argon2id_MEMLIMIT_MODERATE, - crypto_pwhash_ALG_ARGON2ID13) != 0) - err(ERROR_LIBSODIUM, "Could not derive a key from the password."); - if (crypto_secretstream_xchacha20poly1305_init_pull(&crypto_state, header, - key.data) != 0) - err(ERROR_LIBSODIUM, "Could not initialize the crypto state"); - if (crypto_secretstream_xchacha20poly1305_pull(&crypto_state, hsm_secret->data, - NULL, 0, cipher, sizeof(cipher), - NULL, 0) != 0) - err(ERROR_LIBSODIUM, "Could not retrieve the seed. Wrong password ?"); + errx(ERROR_HSM_FILE, "Could not open hsm_secret"); + + if (!read_all(fd, encrypted_secret.data, ENCRYPTED_HSM_SECRET_LEN)) + errx(ERROR_HSM_FILE, "Could not read encrypted hsm_secret"); + + err = hsm_secret_encryption_key(passwd, &key); + if (err) + errx(ERROR_LIBSODIUM, "%s", err); + if (!decrypt_hsm_secret(&key, &encrypted_secret, hsm_secret)) + errx(ERROR_LIBSODIUM, "Could not retrieve the seed. Wrong password ?"); close(fd); } @@ -158,59 +145,39 @@ static void get_channel_seed(struct secret *channel_seed, struct node_id *peer_i info, strlen(info)); } -/* We detect an encrypted hsm_secret as a hsm_secret which is larger than - * the plaintext seed. */ +/* We detect an encrypted hsm_secret as a hsm_secret which is 73-bytes long. */ static bool hsm_secret_is_encrypted(const char *hsm_secret_path) { struct stat st; + if (stat(hsm_secret_path, &st) != 0) errx(ERROR_HSM_FILE, "Could not stat hsm_secret"); - return st.st_size > 32; -} -/* Read a pass from stdin, disabling echoing as done in lightning/options for the - * --encrypted-hsm startup option. - * Caller must free the returned string. */ -static char *read_stdin_pass(void) -{ - struct termios current_term, temp_term; - char *passwd = NULL; - size_t passwd_size = 0; - - if (tcgetattr(fileno(stdin), ¤t_term) != 0) - errx(ERROR_TERM, "Could not get current terminal options."); - temp_term = current_term; - temp_term.c_lflag &= ~ECHO; - if (tcsetattr(fileno(stdin), TCSAFLUSH, &temp_term) != 0) - errx(ERROR_TERM, "Could not disable pass echoing."); - /* If we don't flush we might end up being buffered and we might seem - * to hang while we wait for the password. */ - fflush(stdout); - if (getline(&passwd, &passwd_size, stdin) < 0) - errx(ERROR_TERM, "Could not read pass from stdin."); - if (passwd[strlen(passwd) - 1] == '\n') - passwd[strlen(passwd) - 1] = '\0'; - if (tcsetattr(fileno(stdin), TCSAFLUSH, ¤t_term) != 0) - errx(ERROR_TERM, "Could not restore terminal options."); - - return passwd; + if (st.st_size != 32 && st.st_size != ENCRYPTED_HSM_SECRET_LEN) + errx(ERROR_HSM_FILE, "Invalid hsm_secret (neither plaintext " + "nor encrypted)."); + + return st.st_size == ENCRYPTED_HSM_SECRET_LEN; } static int decrypt_hsm(const char *hsm_secret_path) { int fd; struct secret hsm_secret; - char *passwd; + char *passwd, *err; const char *dir, *backup; /* This checks the file existence, too. */ if (!hsm_secret_is_encrypted(hsm_secret_path)) errx(ERROR_USAGE, "hsm_secret is not encrypted"); printf("Enter hsm_secret password:\n"); - passwd = read_stdin_pass(); + fflush(stdout); + passwd = read_stdin_pass(&err); + if (!passwd) + errx(ERROR_TERM, "%s", err); if (sodium_init() == -1) - err(ERROR_LIBSODIUM, + errx(ERROR_LIBSODIUM, "Could not initialize libsodium. Not enough entropy ?"); dir = path_dirname(NULL, hsm_secret_path); @@ -225,13 +192,13 @@ static int decrypt_hsm(const char *hsm_secret_path) rename(hsm_secret_path, backup); fd = open(hsm_secret_path, O_CREAT|O_EXCL|O_WRONLY, 0400); if (fd < 0) - err(ERROR_HSM_FILE, "Could not open new hsm_secret"); + errx(ERROR_HSM_FILE, "Could not open new hsm_secret"); if (!write_all(fd, &hsm_secret, sizeof(hsm_secret))) { unlink_noerr(hsm_secret_path); close(fd); rename("hsm_secret.backup", hsm_secret_path); - err(ERROR_HSM_FILE, + errx(ERROR_HSM_FILE, "Failure writing plaintext seed to hsm_secret."); } @@ -239,7 +206,7 @@ static int decrypt_hsm(const char *hsm_secret_path) if (!ensure_hsm_secret_exists(fd, hsm_secret_path)) { unlink_noerr(hsm_secret_path); rename(backup, hsm_secret_path); - err(ERROR_HSM_FILE, + errx(ERROR_HSM_FILE, "Could not ensure hsm_secret existence."); } unlink_noerr(backup); @@ -253,12 +220,8 @@ static int encrypt_hsm(const char *hsm_secret_path) { int fd; struct secret key, hsm_secret; - char *passwd; - u8 salt[16] = "c-lightning\0\0\0\0\0"; - crypto_secretstream_xchacha20poly1305_state crypto_state; - u8 header[crypto_secretstream_xchacha20poly1305_HEADERBYTES]; - /* The cipher size is static with xchacha20poly1305. */ - u8 cipher[sizeof(struct secret) + crypto_secretstream_xchacha20poly1305_ABYTES]; + struct encrypted_hsm_secret encrypted_hsm_secret; + char *passwd, *passwd_confirmation, *err; const char *dir, *backup; /* This checks the file existence, too. */ @@ -266,57 +229,58 @@ static int encrypt_hsm(const char *hsm_secret_path) errx(ERROR_USAGE, "hsm_secret is already encrypted"); printf("Enter hsm_secret password:\n"); - /* TODO: make the user double check the password. */ - passwd = read_stdin_pass(); + fflush(stdout); + passwd = read_stdin_pass(&err); + if (!passwd) + errx(ERROR_TERM, "%s", err); + printf("Confirm hsm_secret password:\n"); + fflush(stdout); + passwd_confirmation = read_stdin_pass(&err); + if (!passwd_confirmation) + errx(ERROR_TERM, "%s", err); + if (!streq(passwd, passwd_confirmation)) + errx(ERROR_USAGE, "Passwords confirmation mismatch."); get_hsm_secret(&hsm_secret, hsm_secret_path); dir = path_dirname(NULL, hsm_secret_path); backup = path_join(dir, dir, "hsm_secret.backup"); if (sodium_init() == -1) - err(ERROR_LIBSODIUM, + errx(ERROR_LIBSODIUM, "Could not initialize libsodium. Not enough entropy ?"); /* Derive the encryption key from the password provided, and try to encrypt * the seed. */ - if (crypto_pwhash(key.data, sizeof(key.data), passwd, strlen(passwd), salt, - crypto_pwhash_argon2id_OPSLIMIT_MODERATE, - crypto_pwhash_argon2id_MEMLIMIT_MODERATE, - crypto_pwhash_ALG_ARGON2ID13) != 0) - err(ERROR_LIBSODIUM, "Could not derive a key from the password."); - if (crypto_secretstream_xchacha20poly1305_init_push(&crypto_state, header, - key.data) != 0) - err(ERROR_LIBSODIUM, "Could not initialize the crypto state"); - if (crypto_secretstream_xchacha20poly1305_push(&crypto_state, cipher, - NULL, hsm_secret.data, - sizeof(hsm_secret.data), - NULL, 0, 0) != 0) - err(ERROR_LIBSODIUM, "Could not encrypt the seed."); + err = hsm_secret_encryption_key(passwd, &key); + if (err) + errx(ERROR_LIBSODIUM, "%s", err); + if (!encrypt_hsm_secret(&key, &hsm_secret, &encrypted_hsm_secret)) + errx(ERROR_LIBSODIUM, "Could not encrypt the hsm_secret seed."); /* Once the encryption key derived, we don't need it anymore. */ - if (passwd) - free(passwd); + free(passwd); + free(passwd_confirmation); /* Create a backup file, "just in case". */ rename(hsm_secret_path, backup); fd = open(hsm_secret_path, O_CREAT|O_EXCL|O_WRONLY, 0400); if (fd < 0) - err(ERROR_HSM_FILE, "Could not open new hsm_secret"); + errx(ERROR_HSM_FILE, "Could not open new hsm_secret"); /* Write the encrypted hsm_secret. */ - if (!write_all(fd, header, sizeof(header)) - || !write_all(fd, cipher, sizeof(cipher))) { + if (!write_all(fd, encrypted_hsm_secret.data, + sizeof(encrypted_hsm_secret.data))) { unlink_noerr(hsm_secret_path); close(fd); rename(backup, hsm_secret_path); - err(ERROR_HSM_FILE, "Failure writing cipher to hsm_secret."); + errx(ERROR_HSM_FILE, "Failure writing cipher to hsm_secret."); } /* Be as paranoïd as in hsmd with the file state on disk. */ if (!ensure_hsm_secret_exists(fd, hsm_secret_path)) { unlink_noerr(hsm_secret_path); rename(backup, hsm_secret_path); - err(ERROR_HSM_FILE, "Could not ensure hsm_secret existence."); + errx(ERROR_HSM_FILE, "Could not ensure hsm_secret existence."); } unlink_noerr(backup); tal_free(dir); @@ -332,7 +296,7 @@ static int dump_commitments_infos(struct node_id *node_id, u64 channel_id, struct sha256 shaseed; struct secret hsm_secret, channel_seed, per_commitment_secret; struct pubkey per_commitment_point; - char *passwd; + char *passwd, *err; secp256k1_ctx = secp256k1_context_create(SECP256K1_CONTEXT_VERIFY | SECP256K1_CONTEXT_SIGN); @@ -340,7 +304,10 @@ static int dump_commitments_infos(struct node_id *node_id, u64 channel_id, /* This checks the file existence, too. */ if (hsm_secret_is_encrypted(hsm_secret_path)) { printf("Enter hsm_secret password:\n"); - passwd = read_stdin_pass(); + fflush(stdout); + passwd = read_stdin_pass(&err); + if (!passwd) + errx(ERROR_TERM, "%s", err); get_encrypted_hsm_secret(&hsm_secret, hsm_secret_path, passwd); free(passwd); } else @@ -352,12 +319,12 @@ static int dump_commitments_infos(struct node_id *node_id, u64 channel_id, printf("shaseed: %s\n", type_to_string(tmpctx, struct sha256, &shaseed)); for (u64 i = 0; i < depth; i++) { if (!per_commit_secret(&shaseed, &per_commitment_secret, i)) - err(ERROR_KEYDERIV, "Could not derive secret #%"PRIu64, i); + errx(ERROR_KEYDERIV, "Could not derive secret #%"PRIu64, i); printf("commit secret #%"PRIu64": %s\n", i, tal_hexstr(tmpctx, per_commitment_secret.data, sizeof(per_commitment_secret.data))); if (!per_commit_point(&shaseed, &per_commitment_point, i)) - err(ERROR_KEYDERIV, "Could not derive point #%"PRIu64, i); + errx(ERROR_KEYDERIV, "Could not derive point #%"PRIu64, i); printf("commit point #%"PRIu64": %s\n", i, type_to_string(tmpctx, struct pubkey, &per_commitment_point)); } @@ -385,7 +352,7 @@ static int guess_to_remote(const char *address, struct node_id *node_id, u64 tries, char *hsm_secret_path) { struct secret hsm_secret, channel_seed, basepoint_secret; - char *passwd; + char *passwd, *err; struct pubkey basepoint; struct ripemd160 pubkeyhash; /* We only support P2WPKH, hence 20. */ @@ -407,7 +374,10 @@ static int guess_to_remote(const char *address, struct node_id *node_id, /* This checks the file existence, too. */ if (hsm_secret_is_encrypted(hsm_secret_path)) { printf("Enter hsm_secret password:\n"); - passwd = read_stdin_pass(); + fflush(stdout); + passwd = read_stdin_pass(&err); + if (!passwd) + errx(ERROR_TERM, "%s", err); get_encrypted_hsm_secret(&hsm_secret, hsm_secret_path, passwd); free(passwd); } else @@ -507,14 +477,17 @@ static void read_mnemonic(char *mnemonic) { static int generate_hsm(const char *hsm_secret_path) { char mnemonic[BIP39_WORDLIST_LEN]; - char *passphrase; + char *passphrase, *err; read_mnemonic(mnemonic); printf("Warning: remember that different passphrases yield different " "bitcoin wallets.\n"); printf("If left empty, no password is used (echo is disabled).\n"); printf("Enter your passphrase: \n"); - passphrase = read_stdin_pass(); + fflush(stdout); + passphrase = read_stdin_pass(&err); + if (!passphrase) + errx(ERROR_TERM, "%s", err); if (strlen(passphrase) == 0) { free(passphrase); passphrase = NULL; @@ -551,7 +524,7 @@ static int dumponchaindescriptors(const char *hsm_secret_path, const char *old_p const bool is_testnet) { struct secret hsm_secret; - char *passwd; + char *passwd, *err; u8 bip32_seed[BIP32_ENTROPY_LEN_256]; u32 salt = 0; u32 version = is_testnet ? @@ -563,7 +536,10 @@ static int dumponchaindescriptors(const char *hsm_secret_path, const char *old_p /* This checks the file existence, too. */ if (hsm_secret_is_encrypted(hsm_secret_path)) { printf("Enter hsm_secret password:\n"); - passwd = read_stdin_pass(); + fflush(stdout); + passwd = read_stdin_pass(&err); + if (!passwd) + errx(ERROR_TERM, "%s", err); get_encrypted_hsm_secret(&hsm_secret, hsm_secret_path, passwd); free(passwd); } else @@ -634,7 +610,7 @@ int main(int argc, char *argv[]) show_usage(argv[0]); struct node_id node_id; if (!node_id_from_hexstr(argv[2], strlen(argv[2]), &node_id)) - err(ERROR_USAGE, "Bad node id"); + errx(ERROR_USAGE, "Bad node id"); return dump_commitments_infos(&node_id, atol(argv[3]), atol(argv[4]), argv[5]); }