#include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include /* HSM secret size constants */ #define HSM_SECRET_PLAIN_SIZE 32 #define HSM_SECRET_MNEMONIC_SIZE 64 /* 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) #define PASSPHRASE_HASH_LEN 32 #define HSM_SECRET_PLAIN_SIZE 32 /* Helper function to validate a mnemonic string */ enum hsm_secret_error validate_mnemonic(const char *mnemonic) { struct words *words; bool ok; tal_wally_start(); if (bip39_get_wordlist("en", &words) != WALLY_OK) { abort(); } ok = (bip39_mnemonic_validate(words, mnemonic) == WALLY_OK); /* Wordlists can persist, so provide a common context! */ tal_wally_end(notleak_with_children(tal(NULL, char))); if (!ok) return HSM_SECRET_ERR_INVALID_MNEMONIC; return HSM_SECRET_OK; } struct secret *get_encryption_key(const tal_t *ctx, const char *passphrase) { struct secret *secret = tal(ctx, struct secret); const u8 salt[16] = "c-lightning\0\0\0\0\0"; /* Check bounds. */ if (strlen(passphrase) < crypto_pwhash_argon2id_PASSWD_MIN) { return tal_free(secret); } else if (strlen(passphrase) > crypto_pwhash_argon2id_PASSWD_MAX) { return tal_free(secret); } /* Don't swap the encryption key ! */ mlock_tal_memory(secret); /* Now derive the key. */ if (crypto_pwhash(secret->data, sizeof(secret->data), passphrase, strlen(passphrase), 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 tal_free(secret); } return secret; } bool hsm_secret_needs_passphrase(const u8 *hsm_secret, size_t len) { switch (detect_hsm_secret_type(hsm_secret, len)) { case HSM_SECRET_ENCRYPTED: case HSM_SECRET_MNEMONIC_WITH_PASS: return true; case HSM_SECRET_PLAIN: case HSM_SECRET_MNEMONIC_NO_PASS: case HSM_SECRET_INVALID: return false; } abort(); } enum hsm_secret_type detect_hsm_secret_type(const u8 *hsm_secret, size_t len) { /* Check for invalid cases first and return early */ if (len < HSM_SECRET_PLAIN_SIZE) return HSM_SECRET_INVALID; /* Legacy 32-byte plain format */ if (len == HSM_SECRET_PLAIN_SIZE) return HSM_SECRET_PLAIN; /* Legacy 73-byte encrypted format */ if (len == ENCRYPTED_HSM_SECRET_LEN) return HSM_SECRET_ENCRYPTED; /* Since HSM_SECRET_PLAIN_SIZE == 32, this must be true! */ assert(len >= sizeof(struct sha256)); /* First 32 bytes are the hash of the resulting seed: all 0 * for "no passphrase" */ if (memeqzero(hsm_secret, sizeof(struct sha256))) return HSM_SECRET_MNEMONIC_NO_PASS; else return HSM_SECRET_MNEMONIC_WITH_PASS; } struct bip32_seed { u8 seed[BIP39_SEED_LEN_512]; }; static bool mnemonic_to_seed(const char *mnemonic, const char *passphrase, struct bip32_seed *bip32_seed) { size_t len; tal_wally_start(); if (bip39_mnemonic_to_seed(mnemonic, passphrase, bip32_seed->seed, sizeof(bip32_seed->seed), &len) != WALLY_OK) { tal_wally_end(tmpctx); return false; } /* libwally only allocate the salt temporarily, so context * doesn't matter. */ tal_wally_end(tmpctx); assert(len == sizeof(bip32_seed->seed)); return true; } /* Helper function to derive seed hash from mnemonic + passphrase */ bool derive_seed_hash(const char *mnemonic, const char *passphrase, struct sha256 *seed_hash) { struct bip32_seed bip32_seed; if (!passphrase) { /* No passphrase - return zero hash */ memset(seed_hash, 0, sizeof(*seed_hash)); return true; } if (!mnemonic_to_seed(mnemonic, passphrase, &bip32_seed)) return false; sha256(seed_hash, bip32_seed.seed, sizeof(bip32_seed.seed)); return true; } static bool decrypt_hsm_secret(const struct secret *encryption_key, const u8 *cipher, struct secret *output) { crypto_secretstream_xchacha20poly1305_state crypto_state; /* The header part */ if (crypto_secretstream_xchacha20poly1305_init_pull(&crypto_state, cipher, encryption_key->data) != 0) return false; /* The ciphertext part */ if (crypto_secretstream_xchacha20poly1305_pull(&crypto_state, output->data, NULL, 0, cipher + HS_HEADER_LEN, HS_CIPHERTEXT_LEN, NULL, 0) != 0) return false; return true; } /* Helper function to convert error codes to human-readable messages */ const char *hsm_secret_error_str(enum hsm_secret_error err) { switch (err) { case HSM_SECRET_OK: return "Success"; case HSM_SECRET_ERR_PASSPHRASE_REQUIRED: return "Passphrase required but not provided"; case HSM_SECRET_ERR_PASSPHRASE_NOT_NEEDED: return "Passphrase provided but not needed"; case HSM_SECRET_ERR_WRONG_PASSPHRASE: return "Wrong passphrase"; case HSM_SECRET_ERR_INVALID_MNEMONIC: return "Invalid mnemonic"; case HSM_SECRET_ERR_ENCRYPTION_FAILED: return "Encryption failed"; case HSM_SECRET_ERR_SEED_DERIVATION_FAILED: return "Could not derive seed from mnemonic"; case HSM_SECRET_ERR_INVALID_FORMAT: return "Invalid hsm_secret format"; case HSM_SECRET_ERR_TERMINAL: return "Terminal error"; } return "Unknown error"; } static struct hsm_secret *extract_plain_secret(const tal_t *ctx, const u8 *hsm_secret, size_t len, enum hsm_secret_error *err) { struct hsm_secret *hsms = tal(ctx, struct hsm_secret); assert(len == HSM_SECRET_PLAIN_SIZE); hsms->type = HSM_SECRET_PLAIN; hsms->mnemonic = NULL; /* Allocate and populate secret_data (new field) */ hsms->secret_data = tal_dup_arr(hsms, u8, hsm_secret, HSM_SECRET_PLAIN_SIZE, 0); *err = HSM_SECRET_OK; return hsms; } static struct hsm_secret *extract_encrypted_secret(const tal_t *ctx, const u8 *hsm_secret, size_t len, const char *passphrase, enum hsm_secret_error *err) { struct hsm_secret *hsms = tal(ctx, struct hsm_secret); struct secret *encryption_key; bool decrypt_success; if (!passphrase) { *err = HSM_SECRET_ERR_PASSPHRASE_REQUIRED; return tal_free(hsms); } encryption_key = get_encryption_key(tmpctx, passphrase); if (!encryption_key) { *err = HSM_SECRET_ERR_WRONG_PASSPHRASE; return tal_free(hsms); } /* Attempt decryption */ struct secret temp_secret; decrypt_success = decrypt_hsm_secret(encryption_key, hsm_secret, &temp_secret); if (!decrypt_success) { *err = HSM_SECRET_ERR_WRONG_PASSPHRASE; return tal_free(hsms); } /* Duplicate decrypted secret data */ hsms->secret_data = tal_dup_arr(hsms, u8, temp_secret.data, HSM_SECRET_PLAIN_SIZE, 0); hsms->type = HSM_SECRET_ENCRYPTED; hsms->mnemonic = NULL; *err = HSM_SECRET_OK; return hsms; } static struct hsm_secret *extract_mnemonic_secret(const tal_t *ctx, const u8 *hsm_secret, size_t len, const char *passphrase, enum hsm_secret_type type, enum hsm_secret_error *err) { struct hsm_secret *hsms = tal(ctx, struct hsm_secret); const u8 *mnemonic_start; size_t mnemonic_len; struct bip32_seed bip32_seed; assert(type == HSM_SECRET_MNEMONIC_NO_PASS || type == HSM_SECRET_MNEMONIC_WITH_PASS); hsms->type = type; /* Extract mnemonic portion (skip first 32 bytes which are passphrase hash) */ mnemonic_start = hsm_secret + PASSPHRASE_HASH_LEN; assert(len > PASSPHRASE_HASH_LEN); mnemonic_len = len - PASSPHRASE_HASH_LEN; /* Copy into convenient string form */ hsms->mnemonic = tal_strndup(hsms, (const char *)mnemonic_start, mnemonic_len); /* Validate passphrase if required */ if (type == HSM_SECRET_MNEMONIC_WITH_PASS) { if (!passphrase) { *err = HSM_SECRET_ERR_PASSPHRASE_REQUIRED; return tal_free(hsms); } /* Validate passphrase by comparing stored hash with computed hash */ struct sha256 stored_hash, computed_hash; memcpy(&stored_hash, hsm_secret, sizeof(stored_hash)); if (!derive_seed_hash(hsms->mnemonic, passphrase, &computed_hash)) { *err = HSM_SECRET_ERR_SEED_DERIVATION_FAILED; return tal_free(hsms); } if (!sha256_eq(&stored_hash, &computed_hash)) { *err = HSM_SECRET_ERR_WRONG_PASSPHRASE; return tal_free(hsms); } } else { if (passphrase) { *err = HSM_SECRET_ERR_PASSPHRASE_NOT_NEEDED; return tal_free(hsms); } } /* Validate mnemonic */ *err = validate_mnemonic(hsms->mnemonic); if (*err != HSM_SECRET_OK) { return tal_free(hsms); } if (!mnemonic_to_seed(hsms->mnemonic, passphrase, &bip32_seed)) { *err = HSM_SECRET_ERR_SEED_DERIVATION_FAILED; return tal_free(hsms); } /* Allocate and populate secret_data with full 64-byte seed */ hsms->secret_data = tal_dup_arr(hsms, u8, bip32_seed.seed, sizeof(bip32_seed.seed), 0); *err = HSM_SECRET_OK; return hsms; } /* If hsm_secret_needs_passphrase, passphrase must not be NULL. * Returns NULL on failure. */ struct hsm_secret *extract_hsm_secret(const tal_t *ctx, const u8 *hsm_secret, size_t len, const char *passphrase, enum hsm_secret_error *err) { enum hsm_secret_type type = detect_hsm_secret_type(hsm_secret, len); /* Switch will cause gcc to complain if a type isn't handled. */ switch (type) { case HSM_SECRET_PLAIN: return extract_plain_secret(ctx, hsm_secret, len, err); case HSM_SECRET_ENCRYPTED: return extract_encrypted_secret(ctx, hsm_secret, len, passphrase, err); case HSM_SECRET_MNEMONIC_NO_PASS: case HSM_SECRET_MNEMONIC_WITH_PASS: return extract_mnemonic_secret(ctx, hsm_secret, len, passphrase, type, err); case HSM_SECRET_INVALID: *err = HSM_SECRET_ERR_INVALID_FORMAT; return NULL; } /* detect_hsm_secret_type promised to return a valid type. */ abort(); } bool encrypt_legacy_hsm_secret(const struct secret *encryption_key, const struct secret *hsm_secret, u8 *output) { crypto_secretstream_xchacha20poly1305_state crypto_state; if (crypto_secretstream_xchacha20poly1305_init_push(&crypto_state, output, encryption_key->data) != 0) return false; if (crypto_secretstream_xchacha20poly1305_push(&crypto_state, output + HS_HEADER_LEN, NULL, hsm_secret->data, sizeof(hsm_secret->data), /* Additional data and tag */ NULL, 0, 0)) return false; return true; } /* Disable terminal echo if needed */ static bool disable_echo(struct termios *saved_term) { if (!isatty(fileno(stdin))) return false; if (tcgetattr(fileno(stdin), saved_term) != 0) return false; struct termios tmp = *saved_term; tmp.c_lflag &= ~ECHO; if (tcsetattr(fileno(stdin), TCSANOW, &tmp) != 0) return false; return true; } /* Restore terminal echo if it was disabled */ static void restore_echo(const struct termios *saved_term) { tcsetattr(fileno(stdin), TCSANOW, saved_term); } /* Read line from stdin (uses tal allocation) */ static const char *read_line(const tal_t *ctx) { char *line = NULL; size_t size = 0; if (getline(&line, &size, stdin) < 0) { free(line); return NULL; } /* Strip newline */ size_t len = strlen(line); if (strends(line, "\n")) len--; /* Convert to tal string */ char *result = tal_strndup(ctx, line, len); free(line); return result; } const char *read_stdin_pass(const tal_t *ctx, enum hsm_secret_error *err) { struct termios saved_term; bool echo_disabled = disable_echo(&saved_term); if (isatty(fileno(stdin)) && !echo_disabled) { *err = HSM_SECRET_ERR_TERMINAL; return NULL; } const char *input = read_line(ctx); if (!input) { if (echo_disabled) restore_echo(&saved_term); *err = HSM_SECRET_ERR_INVALID_FORMAT; return NULL; } mlock_tal_memory(input); if (echo_disabled) restore_echo(&saved_term); *err = HSM_SECRET_OK; return input; } const char *read_stdin_mnemonic(const tal_t *ctx, enum hsm_secret_error *err) { printf("Introduce your BIP39 word list separated by space (at least 12 words):\n"); fflush(stdout); const char *line = read_line(ctx); if (!line) { *err = HSM_SECRET_ERR_INVALID_FORMAT; return NULL; } /* Validate mnemonic */ *err = validate_mnemonic(line); if (*err != HSM_SECRET_OK) { return tal_free(line); } *err = HSM_SECRET_OK; return line; } int is_legacy_hsm_secret_encrypted(const char *path) { struct stat st; if (stat(path, &st) != 0) return -1; return st.st_size == ENCRYPTED_HSM_SECRET_LEN; } const char *format_type_name(enum hsm_secret_type type) { switch (type) { case HSM_SECRET_PLAIN: return "plain (32-byte binary)"; case HSM_SECRET_ENCRYPTED: return "encrypted (73-byte binary)"; case HSM_SECRET_MNEMONIC_NO_PASS: return "mnemonic (no password)"; case HSM_SECRET_MNEMONIC_WITH_PASS: return "mnemonic (with password)"; case HSM_SECRET_INVALID: return "invalid"; } return "unknown"; } bool is_mnemonic_secret(size_t secret_len) { return secret_len == HSM_SECRET_MNEMONIC_SIZE; } bool use_bip86_derivation(size_t secret_len) { /* BIP86 was introduced alongside mnemonic support, so they're available together */ return is_mnemonic_secret(secret_len); }