From fa1483a00d86303862ca76559d6c8b6837de6a19 Mon Sep 17 00:00:00 2001 From: positiveblue Date: Wed, 28 Oct 2020 23:59:49 +0100 Subject: [PATCH] `hsm_secret` generation from a seed-phrase tools: Add `generatehsm` method to hsmtool to derivate BIP32 seeds from a mnemonic using the BIP39 standard. The new method uses libwally for the BIP39 to BIP32 derivation. It also fails if an hsm_secret file already exists, so we do not overwrite someone else's wallet without noticing. It allows the use of passphrases, the ECHO mode in the terminal is disable for higher security. It currently supports "en", "es", "fr", "it", "jp", "zhs", "zht". Changelog-Added: hsmtool: `hsm_secret` generation from a seed-phrase following BIP39. --- doc/lightning-hsmtool.8 | 6 +- doc/lightning-hsmtool.8.md | 3 + tools/hsmtool.c | 160 +++++++++++++++++++++++++++++++++++++ 3 files changed, 168 insertions(+), 1 deletion(-) diff --git a/doc/lightning-hsmtool.8 b/doc/lightning-hsmtool.8 index f40d174fe..8057c86b5 100644 --- a/doc/lightning-hsmtool.8 +++ b/doc/lightning-hsmtool.8 @@ -51,6 +51,10 @@ and is usually no greater than the number of channels that the node has ever had\. Specify \fIpassword\fR if the \fBhsm_secret\fR is encrypted\. + +\fBgeneratehsm\fR \fIhsm_secret_path\fR +Generates a new hsm_secret using BIP39\. + .SH BUGS You should report bugs on our github issues page, and maybe submit a fix @@ -76,4 +80,4 @@ Note: the modules in the ccan/ directory have their own licenses, but the rest of the code is covered by the BSD-style MIT license\. Main web site: \fIhttps://github.com/ElementsProject/lightning\fR -\" SHA256STAMP:11b3e6c050eb31e7faac10e8b75c3f7b7d57269f7f8c47c67f6d115b7a479c09 +\" SHA256STAMP:918981692d3840344e15c539b007b473d5ea0ad481145eccff092bf61ec6ddb0 diff --git a/doc/lightning-hsmtool.8.md b/doc/lightning-hsmtool.8.md index c87c2c449..3920fd328 100644 --- a/doc/lightning-hsmtool.8.md +++ b/doc/lightning-hsmtool.8.md @@ -48,6 +48,9 @@ and is usually no greater than the number of channels that the node has ever had. Specify *password* if the `hsm_secret` is encrypted. +**generatehsm** *hsm\_secret\_path* +Generates a new hsm_secret using BIP39. + BUGS ---- diff --git a/tools/hsmtool.c b/tools/hsmtool.c index b795c3cab..8091ac181 100644 --- a/tools/hsmtool.c +++ b/tools/hsmtool.c @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -13,15 +14,19 @@ #include #include #include +#include #include #include +#include #include +#include #define ERROR_HSM_FILE errno #define ERROR_USAGE 2 #define ERROR_LIBSODIUM 3 #define ERROR_LIBWALLY 4 #define ERROR_KEYDERIV 5 +#define ERROR_LANG_NOT_SUPPORTED 6 static void show_usage(const char *progname) { @@ -33,6 +38,7 @@ static void show_usage(const char *progname) " [hsm_secret password]\n"); printf(" - guesstoremote " " [hsm_secret password]\n"); + printf(" - generatehsm \n"); exit(0); } @@ -368,6 +374,145 @@ static int guess_to_remote(const char *address, struct node_id *node_id, return 1; } +static void get_words(struct words **words) { + struct wordlist_lang { + char *abbr; + char *name; + }; + + struct wordlist_lang languages[] = { + {"en", "English"}, + {"es", "Spanish"}, + {"fr", "French"}, + {"it", "Italian"}, + {"jp", "Japanese"}, + {"zhs", "Chinese Simplified"}, + {"zht", "Chinese Traditional"}, + }; + + printf("Select your language:\n"); + for (size_t i = 0; i < ARRAY_SIZE(languages); i++) { + printf(" %zu) %s (%s)\n", i, languages[i].name, languages[i].abbr); + } + printf("Select [0-%zu]: ", ARRAY_SIZE(languages)); + + char *selected = NULL; + size_t size = 0; + size_t characters = getline(&selected, &size, stdin); + if (characters < 0) + errx(ERROR_USAGE, "Could not read line from stdin."); + + /* To distinguish success/failure after call */ + errno = 0; + char *endptr; + long val = strtol(selected, &endptr, 10); + if (errno == ERANGE || (errno != 0 && val == 0) || endptr == selected || val < 0 || val >= ARRAY_SIZE(languages)) + errx(ERROR_USAGE, "Invalid language selection, select one from the list [0-6]."); + + bip39_get_wordlist(languages[val].abbr, words); +} + +static void get_mnemonic(char *mnemonic) { + char *line = NULL; + size_t line_size = 0; + + printf("Introduce your BIP39 word list separated by space:\n"); + size_t characters = getline(&line, &line_size, stdin); + if (characters < 0) + errx(ERROR_USAGE, "Could not read line from stdin."); + line[characters-1] = '\0'; + strcpy(mnemonic, line); + free(line); +} + +static void read_mnemonic(char *mnemonic) { + /* Get words for the mnemonic language */ + struct words *words; + get_words(&words); + + /* Get mnemonic */ + get_mnemonic(mnemonic); + + if (bip39_mnemonic_validate(words, mnemonic) != 0) { + errx(ERROR_USAGE, "Invalid mnemonic: \"%s\"", mnemonic); + } +} + +static void read_passphrase(char **passphrase) { + struct termios current_term, temp_term; + printf("Warning: remember that different passphrases yield different " + "bitcoin wallets.\n"); + printf("If left empty, no password is used (echo is " + "disabled now).\n"); + printf("Enter your passphrase: \n"); + + /* Change terminal options so we do not echo the passphrase */ + if (tcgetattr(fileno(stdin), ¤t_term) != 0) + errx(ERROR_USAGE, "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_USAGE, "Could not disable passphrase 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); + + size_t passphrase_size = 0; + size_t characters = getline(passphrase, &passphrase_size, stdin); + if (characters < 0) + errx(ERROR_USAGE, "Could not read passphrase from stdin."); + + /* Newline is not part of the valid passphrase */ + if ( (*passphrase)[characters-1] == '\n' ) { + (*passphrase)[characters-1] = '\0'; + } + + /* If the user did not introduce any password, we want to set passphrase + * to NULL not to '\0' for libwally */ + if (strlen(*passphrase) == 0) { + free(*passphrase); + *passphrase = NULL; + } + + if (tcsetattr(fileno(stdin), TCSAFLUSH, ¤t_term) != 0) + errx(ERROR_USAGE, "Could not restore terminal options."); +} + +static int generate_hsm(const char *hsm_secret_path) +{ + char mnemonic[BIP39_WORDLIST_LEN]; + read_mnemonic(mnemonic); + + char *passphrase = NULL; + read_passphrase(&passphrase); + + u8 bip32_seed[BIP39_SEED_LEN_512]; + size_t bip32_seed_len; + + if (bip39_mnemonic_to_seed(mnemonic, passphrase, bip32_seed, sizeof(bip32_seed), &bip32_seed_len) != WALLY_OK) + errx(ERROR_LIBWALLY, "Unable to derive BIP32 seed from BIP39 mnemonic"); + + int fd = open(hsm_secret_path, O_CREAT|O_EXCL|O_WRONLY, 0400); + if (fd < 0) { + errx(ERROR_USAGE, "Unable to create hsm_secret file"); + } + if (!write_all(fd, bip32_seed, bip32_seed_len)) + errx(ERROR_USAGE, "Error writing secret to hsm_secret file"); + + if (fsync(fd) != 0) + errx(ERROR_USAGE, "Error fsyncing hsm_secret file"); + + /* This should never fail if fsync succeeded. But paranoia is good, and bugs exist */ + if (close(fd) != 0) + errx(ERROR_USAGE, "Error closing hsm_secret file"); + + printf("New hsm_secret file created at %s\n", hsm_secret_path); + printf("Use the `encrypt` command to encrypt the BIP32 seed if needed\n"); + + free(passphrase); + return 0; +} + int main(int argc, char *argv[]) { const char *method; @@ -413,5 +558,20 @@ int main(int argc, char *argv[]) argv[5], argc >= 7 ? argv[6] : NULL); } + if (streq(method, "generatehsm")) { + if (argc != 3) + show_usage(argv[0]); + + char *hsm_secret_path = argv[2]; + + /* if hsm_secret already exists we abort the process + * we do not want to lose someone else's funds */ + struct stat st; + if (stat(hsm_secret_path, &st) == 0) + errx(ERROR_USAGE, "hsm_secret file at %s already exists", hsm_secret_path); + + return generate_hsm(hsm_secret_path); + } + show_usage(argv[0]); }