Skip to content

Commit

Permalink
hsm_secret generation from a seed-phrase
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
positiveblue committed Nov 5, 2020
1 parent d151d55 commit 828d3b2
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 1 deletion.
6 changes: 5 additions & 1 deletion doc/lightning-hsmtool.8
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
3 changes: 3 additions & 0 deletions doc/lightning-hsmtool.8.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
----

Expand Down
160 changes: 160 additions & 0 deletions tools/hsmtool.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include <bitcoin/privkey.h>
#include <ccan/array_size/array_size.h>
#include <ccan/crypto/hkdf_sha256/hkdf_sha256.h>
#include <ccan/err/err.h>
#include <ccan/noerr/noerr.h>
Expand All @@ -13,15 +14,19 @@
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <limits.h>
#include <sodium.h>
#include <sys/stat.h>
#include <termios.h>
#include <unistd.h>
#include <wally_bip39.h>

#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)
{
Expand All @@ -33,6 +38,7 @@ static void show_usage(const char *progname)
"<path/to/hsm_secret> [hsm_secret password]\n");
printf(" - guesstoremote <P2WPKH address> <node id> <tries> "
"<path/to/hsm_secret> [hsm_secret password]\n");
printf(" - generatehsm <path/to/new//hsm_secret>\n");
exit(0);
}

Expand Down Expand Up @@ -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), &current_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, &current_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 fsycning 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;
Expand Down Expand Up @@ -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]);
}

0 comments on commit 828d3b2

Please sign in to comment.