From 410097eecde6a84f497eff9a7956fd94562f4a91 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Fri, 31 Jul 2026 15:33:18 -0400 Subject: [PATCH] mnemonic: support all standard BIP39 word counts --- main/amalgamated.c | 1 + main/bcur.c | 2 +- main/bip39.c | 57 +++++++++++++++++++++ main/bip39.h | 21 ++++++++ main/button_events.h | 8 +++ main/keychain.c | 22 ++++---- main/process/mnemonic.c | 90 +++++++++++++++++++++++---------- main/qrmode.h | 12 +---- main/selfcheck.c | 106 ++++++++++++++++++++++++++++++++------ main/ui/mnemonic.c | 109 +++++++++++++++++++++++++++++----------- test_jade.py | 22 ++++++++ 11 files changed, 356 insertions(+), 94 deletions(-) create mode 100644 main/bip39.c create mode 100644 main/bip39.h diff --git a/main/amalgamated.c b/main/amalgamated.c index 601364922..f8040029b 100644 --- a/main/amalgamated.c +++ b/main/amalgamated.c @@ -28,6 +28,7 @@ void __wrap_abort(void); #include "./assets.c" #include "./attestation/attestation.c" #include "./bcur.c" +#include "./bip39.c" #ifdef CONFIG_BT_ENABLED #include "./ble/ble.c" #endif // CONFIG_BT_ENABLED diff --git a/main/bcur.c b/main/bcur.c index 0044fd9ab..9af185529 100644 --- a/main/bcur.c +++ b/main/bcur.c @@ -100,7 +100,7 @@ bool bcur_parse_bip39( } size_t num_words = 0; cberr = cbor_value_get_array_length(&mapItem, &num_words); - if (cberr != CborNoError || (num_words != 12 && num_words != 24) || !cbor_value_is_container(&mapItem)) { + if (cberr != CborNoError || !jade_bip39_word_count_valid(num_words) || !cbor_value_is_container(&mapItem)) { return false; } CborValue arrayItem; diff --git a/main/bip39.c b/main/bip39.c new file mode 100644 index 000000000..afac23da1 --- /dev/null +++ b/main/bip39.c @@ -0,0 +1,57 @@ +#ifndef AMALGAMATED_BUILD +#include "bip39.h" +#include "jade_wally_verify.h" + +#include + +size_t jade_bip39_entropy_len_from_word_count(const size_t nwords) +{ + switch (nwords) { + case 12: + return BIP39_ENTROPY_LEN_128; + case 15: + return BIP39_ENTROPY_LEN_160; + case 18: + return BIP39_ENTROPY_LEN_192; + case 21: + return BIP39_ENTROPY_LEN_224; + case 24: + return BIP39_ENTROPY_LEN_256; + default: + return 0; + } +} + +size_t jade_bip39_word_count_from_entropy_len(const size_t entropy_len) +{ + switch (entropy_len) { + case BIP39_ENTROPY_LEN_128: + return 12; + case BIP39_ENTROPY_LEN_160: + return 15; + case BIP39_ENTROPY_LEN_192: + return 18; + case BIP39_ENTROPY_LEN_224: + return 21; + case BIP39_ENTROPY_LEN_256: + return 24; + default: + return 0; + } +} + +bool jade_bip39_word_count_valid(const size_t nwords) { return jade_bip39_entropy_len_from_word_count(nwords) != 0; } + +bool jade_bip39_mnemonic_validate(const char* mnemonic) +{ + if (!mnemonic) { + return false; + } + + uint8_t entropy[BIP39_ENTROPY_LEN_256]; + size_t entropy_len = 0; + const int wret = bip39_mnemonic_to_bytes(NULL, mnemonic, entropy, sizeof(entropy), &entropy_len); + JADE_WALLY_VERIFY(wally_bzero(entropy, sizeof(entropy))); + return wret == WALLY_OK && jade_bip39_word_count_from_entropy_len(entropy_len) != 0; +} +#endif // AMALGAMATED_BUILD diff --git a/main/bip39.h b/main/bip39.h new file mode 100644 index 000000000..4d4681635 --- /dev/null +++ b/main/bip39.h @@ -0,0 +1,21 @@ +#ifndef JADE_BIP39_H_ +#define JADE_BIP39_H_ + +#include +#include + +// Jade supports the standard BIP39 English mnemonic lengths. +#define MNEMONIC_MAXWORDS 24 + +// The longest valid words in the English wordlist are 8 characters. +#define MNEMONIC_MAX_WORD_LEN 8 + +// 24 8-character words + 23 spaces + NUL = 216 bytes. +#define MNEMONIC_BUFLEN 216 + +size_t jade_bip39_entropy_len_from_word_count(size_t nwords); +size_t jade_bip39_word_count_from_entropy_len(size_t entropy_len); +bool jade_bip39_word_count_valid(size_t nwords); +bool jade_bip39_mnemonic_validate(const char* mnemonic); + +#endif /* JADE_BIP39_H_ */ diff --git a/main/button_events.h b/main/button_events.h index e0eda9faa..3b0b5363f 100644 --- a/main/button_events.h +++ b/main/button_events.h @@ -61,7 +61,11 @@ typedef enum { BTN_NEW_MNEMONIC, BTN_NEW_MNEMONIC_12, + BTN_NEW_MNEMONIC_15, + BTN_NEW_MNEMONIC_18, + BTN_NEW_MNEMONIC_21, BTN_NEW_MNEMONIC_24, + BTN_NEW_MNEMONIC_MORE, BTN_MNEMONIC_PREV, BTN_MNEMONIC_NEXT, @@ -70,7 +74,11 @@ typedef enum { BTN_RESTORE_MNEMONIC, BTN_RESTORE_MNEMONIC_12, + BTN_RESTORE_MNEMONIC_15, + BTN_RESTORE_MNEMONIC_18, + BTN_RESTORE_MNEMONIC_21, BTN_RESTORE_MNEMONIC_24, + BTN_RESTORE_MNEMONIC_MORE, BTN_RESTORE_MNEMONIC_QR, BTN_MNEMONIC_FINAL_WORD_EXISTING, diff --git a/main/keychain.c b/main/keychain.c index a99a51be6..2e26a2818 100644 --- a/main/keychain.c +++ b/main/keychain.c @@ -1,6 +1,7 @@ #ifndef AMALGAMATED_BUILD #include "keychain.h" #include "aes.h" +#include "bip39.h" #include "jade_assert.h" #include "jade_wally_verify.h" #include "random.h" @@ -194,8 +195,7 @@ void keychain_cache_mnemonic_entropy(const char* mnemonic) JADE_WALLY_VERIFY( bip39_mnemonic_to_bytes(NULL, mnemonic, mnemonic_entropy, sizeof(mnemonic_entropy), &mnemonic_entropy_len)); - // Only 12 or 24 word mnemonics are supported - JADE_ASSERT(mnemonic_entropy_len == BIP39_ENTROPY_LEN_128 || mnemonic_entropy_len == BIP39_ENTROPY_LEN_256); + JADE_ASSERT(jade_bip39_word_count_from_entropy_len(mnemonic_entropy_len)); } // Clear the network type restriction @@ -270,14 +270,13 @@ void keychain_get_new_mnemonic(char** mnemonic, const size_t nwords) { JADE_INIT_OUT_PPTR(mnemonic); - // Support 12-word and 24-word mnemonics only - JADE_ASSERT(nwords == 12 || nwords == 24); + JADE_ASSERT(jade_bip39_word_count_valid(nwords)); - // Large enough for 12 and 24 word mnemonic + // Large enough for any standard BIP39 mnemonic. uint8_t entropy[BIP39_ENTROPY_LEN_256]; SENSITIVE_PUSH(entropy, sizeof(entropy)); - const size_t entropy_len = nwords == 12 ? BIP39_ENTROPY_LEN_128 : BIP39_ENTROPY_LEN_256; + const size_t entropy_len = jade_bip39_entropy_len_from_word_count(nwords); get_random(entropy, entropy_len); const int wret = bip39_mnemonic_from_bytes(NULL, entropy, entropy_len, mnemonic); SENSITIVE_POP(entropy); @@ -328,8 +327,8 @@ bool keychain_derive_from_mnemonic(const char* mnemonic, const char* passphrase, } } - // Mnemonic must be valid - if (bip39_mnemonic_validate(NULL, mnemonic) != WALLY_OK) { + // Mnemonic must have a valid checksum and a standard BIP39 word count. + if (!jade_bip39_mnemonic_validate(mnemonic)) { JADE_LOGE("Invalid mnemonic"); return false; } @@ -557,8 +556,7 @@ bool keychain_store(const uint8_t* aeskey, const size_t aeslen) // 1. Get serialised data to encrypt/persist if (mnemonic_entropy_len) { // Use mnemonic entropy - // Only 12 or 24 word mnemonics are supported - JADE_ASSERT(mnemonic_entropy_len == BIP39_ENTROPY_LEN_128 || mnemonic_entropy_len == BIP39_ENTROPY_LEN_256); + JADE_ASSERT(jade_bip39_word_count_from_entropy_len(mnemonic_entropy_len)); JADE_ASSERT(mnemonic_entropy_len <= sizeof(mnemonic_entropy)); JADE_ASSERT(mnemonic_entropy_len < sizeof(serialized)); p_serialized_data = mnemonic_entropy; @@ -613,8 +611,8 @@ bool keychain_load(const uint8_t* aeskey, const size_t aeslen) } // 2. Cache mnemonic entropy or deserialise keychain - if (serialized_data_len == BIP39_ENTROPY_LEN_128 || serialized_data_len == BIP39_ENTROPY_LEN_256) { - // Write mnemonic entropy - only 12 or 24 word mnemonics are supported + if (jade_bip39_word_count_from_entropy_len(serialized_data_len)) { + // Write mnemonic entropy. memcpy(mnemonic_entropy, serialized, serialized_data_len); mnemonic_entropy_len = serialized_data_len; } else if (serialized_data_len == SERIALIZED_KEY_LEN) { diff --git a/main/process/mnemonic.c b/main/process/mnemonic.c index db726f90b..f881a680e 100644 --- a/main/process/mnemonic.c +++ b/main/process/mnemonic.c @@ -35,7 +35,9 @@ typedef enum { MNEMONIC_SIMPLE, MNEMONIC_ADVANCED, WORDLIST_PASSPHRASE } wordlis gui_activity_t* make_mnemonic_setup_type_activity(void); gui_activity_t* make_mnemonic_setup_method_activity(bool advanced); gui_activity_t* make_new_mnemonic_activity(void); +gui_activity_t* make_new_mnemonic_more_activity(void); gui_activity_t* make_restore_mnemonic_activity(bool temporary_restore); +gui_activity_t* make_restore_mnemonic_more_activity(bool temporary_restore); void make_show_mnemonic_activities(gui_activity_t** first_activity_ptr, gui_activity_t** last_activity_ptr, const char* mnemonic, uint16_t word_offs[], size_t nwords); @@ -77,16 +79,15 @@ static bool mnemonic_export_qr(const char* mnemonic, bool* export_qr_verified) return true; } - // CompactSeedQR is simply the mnemonic entropy - // Only 12 or 24 word mnemonics are supported (ie. 128 & 256 bit entropy) + // CompactSeedQR is simply the mnemonic entropy. size_t entropy_len = 0; - uint8_t entropy[BIP32_ENTROPY_LEN_256]; // Sufficient for 12 and 24 words + uint8_t entropy[BIP39_ENTROPY_LEN_256]; JADE_WALLY_VERIFY(bip39_mnemonic_to_bytes(NULL, mnemonic, entropy, sizeof(entropy), &entropy_len)); - JADE_ASSERT(entropy_len == BIP32_ENTROPY_LEN_128 || entropy_len == BIP32_ENTROPY_LEN_256); + JADE_ASSERT(jade_bip39_word_count_from_entropy_len(entropy_len)); // Convert the entropy into a small (v1 or v2) qr-code QRCode qrcode; - const uint8_t qrcode_version = entropy_len == BIP32_ENTROPY_LEN_128 ? 1 : 2; + const uint8_t qrcode_version = entropy_len == BIP39_ENTROPY_LEN_128 ? 1 : 2; uint8_t qrbuffer[96]; // underlying qrcode data/work area - opaque JADE_ASSERT(sizeof(qrbuffer) > qrcode_getBufferSize(qrcode_version)); const int qret = qrcode_initBytes(&qrcode, qrbuffer, qrcode_version, ECC_LOW, entropy, entropy_len); @@ -259,8 +260,7 @@ static void change_mnemonic_word_separator(char* mnemonic, const size_t len, con // NOTE: this function replaces spaces with \0's in the passed mnemonic! static bool display_confirm_mnemonic(const size_t nwords, char* mnemonic, const size_t mnemonic_len) { - // Support 12-word and 24-word mnemonics only - JADE_ASSERT(nwords == 12 || nwords == 24); + JADE_ASSERT(jade_bip39_word_count_valid(nwords)); JADE_ASSERT(mnemonic); // Show the warning banner screen, user to confirm @@ -273,7 +273,7 @@ static bool display_confirm_mnemonic(const size_t nwords, char* mnemonic, const } // Change the word separator to a null so we can treat each word as a terminated string. - uint16_t word_offs[MNEMONIC_MAXWORDS]; // large enough for 12 and 24 word mnemonic + uint16_t word_offs[MNEMONIC_MAXWORDS]; change_mnemonic_word_separator(mnemonic, mnemonic_len, ' ', '\0', word_offs, nwords); bool mnemonic_confirmed = false; @@ -319,13 +319,13 @@ static bool display_confirm_mnemonic(const size_t nwords, char* mnemonic, const // Pick some other words from the mnemonic as options, but avoid // the words currently displayed on screen (neighbouring words). - // Large enough for 12 and 24 word mnemonic + // Large enough for any standard BIP39 mnemonic. bool already_picked[MNEMONIC_MAXWORDS] = { false }; already_picked[i] = true; already_picked[i + 1] = true; already_picked[i + 2] = true; - // Large enough for 12 and 24 word mnemonic + // Large enough for any standard BIP39 mnemonic. // (Only really needs to be as big as 'num_words_options' so MAXWORDS is plenty) size_t random_words[MNEMONIC_MAXWORDS] = { 0 }; random_words[0] = selected; @@ -392,8 +392,7 @@ static bool display_confirm_mnemonic(const size_t nwords, char* mnemonic, const // NOTE: only the English wordlist is supported. static bool mnemonic_new(const size_t nwords, char* mnemonic, const size_t mnemonic_len) { - // Support 12-word and 24-word mnemonics only - JADE_ASSERT(nwords == 12 || nwords == 24); + JADE_ASSERT(jade_bip39_word_count_valid(nwords)); JADE_ASSERT(mnemonic); JADE_ASSERT(mnemonic_len == MNEMONIC_BUFLEN); @@ -572,7 +571,7 @@ static size_t valid_final_words(const char** mnemonic_words, const size_t num_mn size_t* possible_word_list, const size_t possible_word_list_len) { JADE_ASSERT(mnemonic_words); - JADE_ASSERT(num_mnemonic_words == 11 || num_mnemonic_words == 23); + JADE_ASSERT(jade_bip39_word_count_valid(num_mnemonic_words + 1)); JADE_ASSERT(possible_word_list); JADE_ASSERT(possible_word_list_len); @@ -616,9 +615,8 @@ static size_t get_wordlist_words( JADE_ASSERT(output); JADE_ASSERT(output_len >= (8 + 1) * nwords); // words plus trailing space - // Only 12 and 24 word mnemonics are supported const bool is_mnemonic = (purpose == MNEMONIC_SIMPLE) || (purpose == MNEMONIC_ADVANCED); - JADE_ASSERT(nwords == 12 || nwords == 24 || !is_mnemonic); + JADE_ASSERT(jade_bip39_word_count_valid(nwords) || !is_mnemonic); gui_view_node_t* btns[26] = {}; const size_t btns_len = sizeof(btns) / sizeof(btns[0]); @@ -670,7 +668,8 @@ static size_t get_wordlist_words( num_filter_words = valid_final_words(wordlist_words, word_index, final_words, MAX_NUM_FINAL_WORDS); p_filter_words = final_words; - JADE_ASSERT(num_filter_words == (nwords == 12 ? 128 : 8)); // expected due to checksum bits + const size_t checksum_bits = nwords / 3; + JADE_ASSERT(num_filter_words == (1U << (11 - checksum_bits))); // When we select from the valid words, randomise the initally selected word random_first_selection_word = true; @@ -881,8 +880,7 @@ static size_t get_wordlist_words( // NOTE: only the English wordlist is supported. static bool mnemonic_recover(const size_t nwords, const bool advanced_mode, char* mnemonic, const size_t mnemonic_len) { - // Support 12-word and 24-word mnemonics only - JADE_ASSERT(nwords == 12 || nwords == 24); + JADE_ASSERT(jade_bip39_word_count_valid(nwords)); JADE_ASSERT(mnemonic); JADE_ASSERT(mnemonic_len == MNEMONIC_BUFLEN); @@ -894,7 +892,7 @@ static bool mnemonic_recover(const size_t nwords, const bool advanced_mode, char return false; } - if (words_entered != nwords || bip39_mnemonic_validate(NULL, mnemonic) != WALLY_OK) { + if (words_entered != nwords || !jade_bip39_mnemonic_validate(mnemonic)) { // Invalid mnemonic entered JADE_LOGW("Invalid mnemonic entered"); await_error("Invalid recovery phrase"); @@ -1008,8 +1006,12 @@ static bool import_seedqr( JADE_ASSERT(bytes[bytes_len] == '\0'); - // Must be a string of appropriate length and all digits - if ((bytes_len != 48 && bytes_len != 96) || !string_all((const char*)bytes, isdigit)) { + // Must be a string of four-digit word indices for a standard BIP39 word count. + if (bytes_len % 4 != 0 || !string_all((const char*)bytes, isdigit)) { + return false; + } + const size_t num_words = bytes_len / 4; + if (!jade_bip39_word_count_valid(num_words)) { return false; } @@ -1019,7 +1021,6 @@ static bool import_seedqr( index_code[4] = '\0'; size_t write_pos = 0; - const size_t num_words = bytes_len == 48 ? 12 : 24; for (size_t i = 0; i < num_words; ++i) { memcpy(index_code, bytes + (i * 4), 4); const size_t index = strtol(index_code, NULL, 10); @@ -1065,8 +1066,8 @@ static bool import_compactseedqr( JADE_ASSERT(buf_len); JADE_INIT_OUT_SIZE(written); - // Any buffer of appropriate length will work as a compactseedqr as it's just raw entropy - if ((bytes_len != BIP32_ENTROPY_LEN_128 && bytes_len != BIP32_ENTROPY_LEN_256)) { + // Any standard BIP39 entropy length works as CompactSeedQR raw entropy. + if (!jade_bip39_word_count_from_entropy_len(bytes_len)) { return false; } @@ -1121,7 +1122,7 @@ bool import_and_validate_mnemonic(qr_data_t* qr_data) size_t written = 0; bool ret; if (import_mnemonic(qr_data->data, qr_data->len, mnemonic, sizeof(mnemonic), &written) - && bip39_mnemonic_validate(NULL, mnemonic) == WALLY_OK) { + && jade_bip39_mnemonic_validate(mnemonic)) { JADE_ASSERT(written); JADE_ASSERT(written <= sizeof(mnemonic)); JADE_ASSERT(mnemonic[written - 1] == '\0'); @@ -1302,7 +1303,12 @@ void initialise_with_mnemonic(const bool temporary_restore, const bool force_qr_ } else { // Initial welcome screen, or straight to 'recovery' screen if doing temporary restore if (temporary_restore) { +#ifdef CONFIG_HAS_CAMERA + // Prefer the page containing Scan QR, while keeping all manual lengths accessible. + act = make_restore_mnemonic_more_activity(temporary_restore); +#else act = make_restore_mnemonic_activity(temporary_restore); +#endif } else { const char* message[] = { "For setup instructions", "visit blockstream.com/", "jade" }; if (await_continueback_activity(NULL, message, 3, true, "blkstrm.com/jade")) { @@ -1359,15 +1365,35 @@ void initialise_with_mnemonic(const bool temporary_restore, const bool force_qr_ act = make_new_mnemonic_activity(); continue; + case BTN_NEW_MNEMONIC_MORE: + act = make_new_mnemonic_more_activity(); + continue; + case BTN_RESTORE_MNEMONIC: act = make_restore_mnemonic_activity(temporary_restore); continue; + case BTN_RESTORE_MNEMONIC_MORE: + act = make_restore_mnemonic_more_activity(temporary_restore); + continue; + // Await user mnemonic entry/confirmation case BTN_NEW_MNEMONIC_12: got_mnemonic = mnemonic_new(12, mnemonic, sizeof(mnemonic)); break; + case BTN_NEW_MNEMONIC_15: + got_mnemonic = mnemonic_new(15, mnemonic, sizeof(mnemonic)); + break; + + case BTN_NEW_MNEMONIC_18: + got_mnemonic = mnemonic_new(18, mnemonic, sizeof(mnemonic)); + break; + + case BTN_NEW_MNEMONIC_21: + got_mnemonic = mnemonic_new(21, mnemonic, sizeof(mnemonic)); + break; + case BTN_NEW_MNEMONIC_24: got_mnemonic = mnemonic_new(24, mnemonic, sizeof(mnemonic)); break; @@ -1376,6 +1402,18 @@ void initialise_with_mnemonic(const bool temporary_restore, const bool force_qr_ got_mnemonic = mnemonic_recover(12, advanced_mode, mnemonic, sizeof(mnemonic)); break; + case BTN_RESTORE_MNEMONIC_15: + got_mnemonic = mnemonic_recover(15, advanced_mode, mnemonic, sizeof(mnemonic)); + break; + + case BTN_RESTORE_MNEMONIC_18: + got_mnemonic = mnemonic_recover(18, advanced_mode, mnemonic, sizeof(mnemonic)); + break; + + case BTN_RESTORE_MNEMONIC_21: + got_mnemonic = mnemonic_recover(21, advanced_mode, mnemonic, sizeof(mnemonic)); + break; + case BTN_RESTORE_MNEMONIC_24: got_mnemonic = mnemonic_recover(24, advanced_mode, mnemonic, sizeof(mnemonic)); break; @@ -1396,7 +1434,7 @@ void initialise_with_mnemonic(const bool temporary_restore, const bool force_qr_ // a. newly created mnemonics should always be valid // b. restore by kb-entry includes explicit validation // c. qr-scanner includes a validation check before returning the scanned mnemonic - if (bip39_mnemonic_validate(NULL, mnemonic) != WALLY_OK) { + if (!jade_bip39_mnemonic_validate(mnemonic)) { JADE_LOGE("Invalid mnemonic unexpected"); await_error("Invalid recovery phrase"); goto cleanup; diff --git a/main/qrmode.h b/main/qrmode.h index 25da5c33d..8cf8f99ae 100644 --- a/main/qrmode.h +++ b/main/qrmode.h @@ -7,20 +7,10 @@ #include +#include "bip39.h" #include "jade_assert.h" #include "otpauth.h" -// NOTE: Jade only supports the bip39 English wordlist, -// with a 12 or 24 word mnemonic phrase. -#define MNEMONIC_MAXWORDS 24 - -// The longest valid words in the English wordlist are 8 characters. -#define MNEMONIC_MAX_WORD_LEN 8 - -// Size of a buffer for holding a mnemonic phrase. -// 24 8-character words + 23 spaces + NUL = 216 bytes -#define MNEMONIC_BUFLEN 216 - // Display singlesig xpub qr code void display_xpub_qr(void); diff --git a/main/selfcheck.c b/main/selfcheck.c index eb5d61ebe..0592afe07 100644 --- a/main/selfcheck.c +++ b/main/selfcheck.c @@ -3,6 +3,7 @@ #include #include "bcur.h" +#include "bip39.h" #include "jade_assert.h" #include "jade_wally_verify.h" #include "keychain.h" @@ -23,6 +24,7 @@ #include #include +#include #include #include @@ -33,6 +35,39 @@ int register_multisig_file(const char* multisig_file, size_t multisig_file_len, static const char TEST_MNEMONIC[] = "fish inner face ginger orchard permit useful method fence kidney chuckle party " "favorite sunset draw limb science crane oval letter slot invite sadness banana"; + +// Fixed English vectors use zero-filled entropy and the standard "TREZOR" test passphrase. +static const struct { + size_t nwords; + const char* entropy_hex; + const char* mnemonic; + const char* seed_hex; +} BIP39_TEST_VECTORS[] = { + { 12, "00000000000000000000000000000000", + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + "c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141" + "630c7a3c4ab7c81b2f001698e7463b04" }, + { 15, "0000000000000000000000000000000000000000", + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon " + "abandon address", + "fa08713f46bf5cb48728ceb70e3aae1bc53c5cb7b4e29c5610261d1cbb7be3bed4d805256fec515754d2be35974fc5da678" + "168e9d9bb0cb70948026923b0def3" }, + { 18, "000000000000000000000000000000000000000000000000", + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon " + "abandon abandon abandon abandon agent", + "035895f2f481b1b0f01fcf8c289c794660b289981a78f8106447707fdd9666ca06da5a9a565181599b79f53b844d8a71dd9" + "f439c52a3d7b3e8a79c906ac845fa" }, + { 21, "00000000000000000000000000000000000000000000000000000000", + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon " + "abandon abandon abandon abandon abandon abandon abandon admit", + "e7dadc189d2e8d07ac278d9ec98a1d2d327e4a6b7df494c00cbf2cbf2d3543dac7000fc72d4ada8d9997dc8db388ff22c6" + "d79f604a7455f2df5534a28eee04c6" }, + { 24, "0000000000000000000000000000000000000000000000000000000000000000", + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon " + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art", + "bda85446c68413707090a52022edd26a1c9462295029f2e60cd7c4f2bbd3097170af7a4d73245cafa9c3cca8d561a7c3de6" + "f5d4a10be8ed2a5e608d68f92fcc8" }, +}; static const char SERVICE_PATH_HEX[] = "00c9678fbd9d9f6a96bd43221d56733b5aba8f528487602b894e72d0f56e380f7d145b65639db7e" "e4f528a3fcfb8277b0cbbea00ef64767a531e9a447cacbfbc"; @@ -40,8 +75,8 @@ static const char SERVICE_PATH_HEX[] = "00c9678fbd9d9f6a96bd43221d56733b5aba8f52 // (Payload data is padded to next multiple of 16, and is concatenated between iv and hmac) // 16 (iv) + 208 (length of data stored (78 (key) + 64 (ga path) + 64 (blinding key)) padded to next 16x) + 32 (hmac) static const size_t FULL_KEY_BLOBLEN = 256; -// 16 (iv) + 32 (12-word entropy (16) padded to next 16x) + 32 (hmac) -static const size_t MNEMONIC_12_ENTROPY_BLOBLEN = 80; +// 16 (iv) + 32 (12- to 21-word entropy padded to next 16x) + 32 (hmac) +static const size_t MNEMONIC_SHORT_ENTROPY_BLOBLEN = 80; // 16 (iv) + 48 (24-word entropy (32) padded to next 16x) + 32 (hmac) static const size_t MNEMONIC_24_ENTROPY_BLOBLEN = 96; @@ -153,8 +188,46 @@ static bool test_simple_restore(void) return true; } -// Generate new mnemonics/wallets -// NOTE: only 12- and 24- words supported +static bool test_bip39_vectors(void) +{ + for (size_t i = 0; i < sizeof(BIP39_TEST_VECTORS) / sizeof(BIP39_TEST_VECTORS[0]); ++i) { + const size_t expected_entropy_len = jade_bip39_entropy_len_from_word_count(BIP39_TEST_VECTORS[i].nwords); + uint8_t entropy[BIP39_ENTROPY_LEN_256] = { 0 }; + size_t entropy_len = 0; + if (!expected_entropy_len + || wally_hex_to_bytes(BIP39_TEST_VECTORS[i].entropy_hex, entropy, sizeof(entropy), &entropy_len) != WALLY_OK + || entropy_len != expected_entropy_len) { + FAIL(); + } + + char* mnemonic = NULL; + if (bip39_mnemonic_from_bytes(NULL, entropy, entropy_len, &mnemonic) != WALLY_OK || !mnemonic + || strcmp(mnemonic, BIP39_TEST_VECTORS[i].mnemonic) || !jade_bip39_mnemonic_validate(mnemonic)) { + if (mnemonic) { + WALLY_FREE_STR(mnemonic); + } + FAIL(); + } + + uint8_t seed[BIP39_SEED_LEN_512]; + size_t seed_len = 0; + uint8_t expected_seed[BIP39_SEED_LEN_512]; + size_t expected_seed_len = 0; + if (bip39_mnemonic_to_seed(mnemonic, "TREZOR", seed, sizeof(seed), &seed_len) != WALLY_OK + || wally_hex_to_bytes( + BIP39_TEST_VECTORS[i].seed_hex, expected_seed, sizeof(expected_seed), &expected_seed_len) + != WALLY_OK + || seed_len != sizeof(seed) || expected_seed_len != sizeof(expected_seed) + || sodium_memcmp(seed, expected_seed, sizeof(seed))) { + WALLY_FREE_STR(mnemonic); + FAIL(); + } + WALLY_FREE_STR(mnemonic); + } + return true; +} + +// Generate new mnemonics/wallets. static bool test_new_wallets(const size_t nwords) { char* mnemonic; @@ -316,7 +389,6 @@ static bool test_storage_with_pin(jade_process_t* process) } // Test storing mnemonic entropy in storage, and deriving wallet with passphrase when reloading -// NOTE: only 12- and 24- words supported static bool test_storage_with_passphrase(jade_process_t* process, const size_t nwords) { JADE_ASSERT(process); @@ -354,7 +426,7 @@ static bool test_storage_with_passphrase(jade_process_t* process, const size_t n if (!storage_get_encrypted_blob(blob, sizeof(blob), &blob_len)) { FAIL(); } - const size_t expected_blob_len = nwords == 12 ? MNEMONIC_12_ENTROPY_BLOBLEN : MNEMONIC_24_ENTROPY_BLOBLEN; + const size_t expected_blob_len = nwords == 24 ? MNEMONIC_24_ENTROPY_BLOBLEN : MNEMONIC_SHORT_ENTROPY_BLOBLEN; if (blob_len != expected_blob_len) { FAIL(); } @@ -1536,12 +1608,17 @@ bool debug_selfcheck(jade_process_t* process) FAIL(); } - // Check 12- and 24-word mnemonic generation, with and without passphrase - if (!test_new_wallets(12)) { + // Check fixed vectors for every standard BIP39 mnemonic length. + if (!test_bip39_vectors()) { FAIL(); } - if (!test_new_wallets(24)) { - FAIL(); + + // Check mnemonic generation, with and without passphrase. + static const size_t mnemonic_word_counts[] = { 12, 15, 18, 21, 24 }; + for (size_t i = 0; i < sizeof(mnemonic_word_counts) / sizeof(mnemonic_word_counts[0]); ++i) { + if (!test_new_wallets(mnemonic_word_counts[i])) { + FAIL(); + } } // Test can write and read-back key data from storage @@ -1551,11 +1628,10 @@ bool debug_selfcheck(jade_process_t* process) } // Test save/load when using passphrase - if (!test_storage_with_passphrase(process, 12)) { - FAIL(); - } - if (!test_storage_with_passphrase(process, 24)) { - FAIL(); + for (size_t i = 0; i < sizeof(mnemonic_word_counts) / sizeof(mnemonic_word_counts[0]); ++i) { + if (!test_storage_with_passphrase(process, mnemonic_word_counts[i])) { + FAIL(); + } } // Test multisig file import/export diff --git a/main/ui/mnemonic.c b/main/ui/mnemonic.c index a50aded48..f317a0220 100644 --- a/main/ui/mnemonic.c +++ b/main/ui/mnemonic.c @@ -1,6 +1,7 @@ #ifndef AMALGAMATED_BUILD #include +#include "../bip39.h" #include "../button_events.h" #include "../jade_assert.h" #include "../ui.h" @@ -28,7 +29,7 @@ gui_activity_t* make_mnemonic_setup_method_activity(const bool advanced) btn_data_t hdrbtns[] = { { .txt = "=", .font = JADE_SYMBOLS_16x16_FONT, .ev_id = BTN_MNEMONIC_TYPE }, { .txt = NULL, .font = GUI_DEFAULT_FONT, .ev_id = GUI_BUTTON_EVENT_NONE } }; - // In advanced mode offer 12/14 word new-mnemonics. + // In advanced mode offer all standard BIP39 mnemonic lengths. // Go straight to 12-word new-mnemonic setup in basic case. btn_data_t menubtns[] = { { .txt = "Create New Wallet", .font = GUI_DEFAULT_FONT, @@ -50,9 +51,11 @@ gui_activity_t* make_new_mnemonic_activity(void) { .txt = NULL, .font = GUI_DEFAULT_FONT, .ev_id = GUI_BUTTON_EVENT_NONE } }; btn_data_t menubtns[] = { { .txt = "12 Words", .font = GUI_DEFAULT_FONT, .ev_id = BTN_NEW_MNEMONIC_12 }, - { .txt = "24 Words", .font = GUI_DEFAULT_FONT, .ev_id = BTN_NEW_MNEMONIC_24 } }; + { .txt = "15 Words", .font = GUI_DEFAULT_FONT, .ev_id = BTN_NEW_MNEMONIC_15 }, + { .txt = "18 Words", .font = GUI_DEFAULT_FONT, .ev_id = BTN_NEW_MNEMONIC_18 }, + { .txt = "More Options", .font = GUI_DEFAULT_FONT, .ev_id = BTN_NEW_MNEMONIC_MORE } }; - gui_activity_t* const act = make_menu_activity("Recovery Phrase", hdrbtns, 2, menubtns, 2); + gui_activity_t* const act = make_menu_activity("Recovery Phrase", hdrbtns, 2, menubtns, 4); // Set the intially selected item to the '12 words' button gui_set_activity_initial_selection(menubtns[0].btn); @@ -60,30 +63,61 @@ gui_activity_t* make_new_mnemonic_activity(void) return act; } -gui_activity_t* make_restore_mnemonic_activity(const bool temporary_restore) +gui_activity_t* make_new_mnemonic_more_activity(void) +{ + btn_data_t hdrbtns[] = { { .txt = "=", .font = JADE_SYMBOLS_16x16_FONT, .ev_id = BTN_NEW_MNEMONIC }, + { .txt = NULL, .font = GUI_DEFAULT_FONT, .ev_id = GUI_BUTTON_EVENT_NONE } }; + + btn_data_t menubtns[] = { { .txt = "21 Words", .font = GUI_DEFAULT_FONT, .ev_id = BTN_NEW_MNEMONIC_21 }, + { .txt = "24 Words", .font = GUI_DEFAULT_FONT, .ev_id = BTN_NEW_MNEMONIC_24 } }; + + gui_activity_t* const act = make_menu_activity("Recovery Phrase", hdrbtns, 2, menubtns, 2); + gui_set_activity_initial_selection(menubtns[0].btn); + return act; +} + +gui_activity_t* make_restore_mnemonic_more_activity(const bool temporary_restore) { - // If temporary restore this is the root so 'back' becomes 'exit' btn_data_t hdrbtns[] = { { .txt = "=", .font = JADE_SYMBOLS_16x16_FONT, - .ev_id = temporary_restore ? BTN_MNEMONIC_EXIT : BTN_MNEMONIC_METHOD }, + .ev_id = temporary_restore ? BTN_MNEMONIC_EXIT : BTN_RESTORE_MNEMONIC }, { .txt = NULL, .font = GUI_DEFAULT_FONT, .ev_id = GUI_BUTTON_EVENT_NONE } }; - btn_data_t menubtns[] = { { .txt = "12 Words", .font = GUI_DEFAULT_FONT, .ev_id = BTN_RESTORE_MNEMONIC_12 }, + btn_data_t menubtns[] = { { .txt = "Other Lengths", .font = GUI_DEFAULT_FONT, .ev_id = BTN_RESTORE_MNEMONIC }, + { .txt = "21 Words", .font = GUI_DEFAULT_FONT, .ev_id = BTN_RESTORE_MNEMONIC_21 }, { .txt = "24 Words", .font = GUI_DEFAULT_FONT, .ev_id = BTN_RESTORE_MNEMONIC_24 }, { .txt = "Scan QR", .font = GUI_DEFAULT_FONT, .ev_id = BTN_RESTORE_MNEMONIC_QR } }; #ifdef CONFIG_HAS_CAMERA - const size_t nbtns = 3; - const size_t selected = temporary_restore ? 2 : 0; + const size_t nbtns = 4; + const size_t selected = temporary_restore ? 3 : 1; #else - const size_t nbtns = 2; - const size_t selected = 0; + const size_t nbtns = 3; + const size_t selected = 1; #endif gui_activity_t* const act = make_menu_activity("Restore Wallet", hdrbtns, 2, menubtns, nbtns); - - // Set the intially selected item to the '12 words' or 'Scan QR' buttons gui_set_activity_initial_selection(menubtns[selected].btn); + return act; +} + +gui_activity_t* make_restore_mnemonic_activity(const bool temporary_restore) +{ + // If temporary restore this is the root so 'back' becomes 'exit' + btn_data_t hdrbtns[] = { { .txt = "=", + .font = JADE_SYMBOLS_16x16_FONT, + .ev_id = temporary_restore ? BTN_MNEMONIC_EXIT : BTN_MNEMONIC_METHOD }, + { .txt = NULL, .font = GUI_DEFAULT_FONT, .ev_id = GUI_BUTTON_EVENT_NONE } }; + + btn_data_t menubtns[] = { { .txt = "12 Words", .font = GUI_DEFAULT_FONT, .ev_id = BTN_RESTORE_MNEMONIC_12 }, + { .txt = "15 Words", .font = GUI_DEFAULT_FONT, .ev_id = BTN_RESTORE_MNEMONIC_15 }, + { .txt = "18 Words", .font = GUI_DEFAULT_FONT, .ev_id = BTN_RESTORE_MNEMONIC_18 }, + { .txt = "More Options", .font = GUI_DEFAULT_FONT, .ev_id = BTN_RESTORE_MNEMONIC_MORE } }; + + gui_activity_t* const act = make_menu_activity("Restore Wallet", hdrbtns, 2, menubtns, 4); + + // Set the initially selected item to the '12 words' button. + gui_set_activity_initial_selection(menubtns[0].btn); return act; } @@ -104,19 +138,21 @@ gui_activity_t* make_bip85_mnemonic_words_activity(void) return act; } -static void make_show_new_mnemonic_page( - link_activity_t* page_act, const size_t nwords, const size_t first_index, const char* words[4]) +static void make_show_new_mnemonic_page(link_activity_t* page_act, const size_t nwords, const size_t first_index, + const char* words[4], const size_t page_words) { JADE_ASSERT(page_act); - JADE_ASSERT(words && words[0] && words[1] && words[2] && words[3]); + JADE_ASSERT(words); + JADE_ASSERT(page_words == 3 || page_words == 4); + for (size_t i = 0; i < page_words; ++i) { + JADE_ASSERT(words[i]); + } - // Support 12-word and 24-word mnemonics only - JADE_ASSERT(nwords == 12 || nwords == 24); + JADE_ASSERT(jade_bip39_word_count_valid(nwords)); JADE_ASSERT(first_index < nwords); - JADE_ASSERT(first_index % 4 == 0); const bool first_page = first_index == 0; - const bool last_page = first_index == nwords - 4; + const bool last_page = first_index + page_words == nwords; const uint32_t prev_ev_id = first_page ? BTN_MNEMONIC_EXIT : BTN_MNEMONIC_PREV; const uint32_t next_ev_id = last_page ? BTN_MNEMONIC_VERIFY : BTN_MNEMONIC_NEXT; @@ -127,15 +163,24 @@ static void make_show_new_mnemonic_page( gui_view_node_t* parent = add_title_bar(act, "Recovery Phrase", hdrbtns, 2, NULL); // Rows are the index-prefixed words in a single column - // Display 4 words per page, in a column + // Display up to 4 words per page, in a column. // NOTE: the words prefixed by their index, eg. "1: river" gui_view_node_t* vsplit; - gui_make_vsplit(&vsplit, GUI_SPLIT_RELATIVE, 4, 25, 25, 25, 25); + switch (page_words) { + case 3: + gui_make_vsplit(&vsplit, GUI_SPLIT_RELATIVE, 3, 34, 33, 33); + break; + case 4: + gui_make_vsplit(&vsplit, GUI_SPLIT_RELATIVE, 4, 25, 25, 25, 25); + break; + default: + JADE_ASSERT(false); + } gui_set_padding(vsplit, GUI_MARGIN_ALL_DIFFERENT, 4, 12, 14, 32); gui_set_parent(vsplit, parent); char prefixed_word[16]; - for (int irow = 0; irow < 4; ++irow) { + for (size_t irow = 0; irow < page_words; ++irow) { // index-prefixed word, eg. "1: river" const int ret = snprintf(prefixed_word, sizeof(prefixed_word), "%2u: %s", first_index + irow + 1, words[irow]); JADE_ASSERT(ret > 0 && ret < sizeof(prefixed_word)); @@ -162,22 +207,28 @@ void make_show_mnemonic_activities(gui_activity_t** first_activity_ptr, gui_acti JADE_INIT_OUT_PPTR(last_activity_ptr); JADE_ASSERT(word_offs); - // Support 12-word and 24-word mnemonics only - JADE_ASSERT(nwords == 12 || nwords == 24); + JADE_ASSERT(jade_bip39_word_count_valid(nwords)); // Chain the screen activities link_activity_t page_act = {}; linked_activities_info_t act_info = {}; - const size_t npages = nwords / 4; // 4 words per page + const size_t npages = (nwords + 3) / 4; const char* words[4] = {}; + size_t first_index = 0; for (size_t j = 0; j < npages; ++j) { - for (size_t w = 0; w < 4; ++w) { - words[w] = mnemonic + word_offs[j * 4 + w]; + const size_t remaining_pages = npages - j; + const size_t remaining_words = nwords - first_index; + const size_t page_words = (remaining_words + remaining_pages - 1) / remaining_pages; + JADE_ASSERT(page_words == 3 || page_words == 4); + for (size_t w = 0; w < page_words; ++w) { + words[w] = mnemonic + word_offs[first_index + w]; } - make_show_new_mnemonic_page(&page_act, nwords, j * 4, words); + make_show_new_mnemonic_page(&page_act, nwords, first_index, words, page_words); gui_chain_activities(&page_act, &act_info); + first_index += page_words; } + JADE_ASSERT(first_index == nwords); *first_activity_ptr = act_info.first_activity; *last_activity_ptr = act_info.last_activity; diff --git a/test_jade.py b/test_jade.py index 61f21dd4f..6a2775e80 100644 --- a/test_jade.py +++ b/test_jade.py @@ -255,6 +255,16 @@ def _get_test_cases(pattern, allow_sampling=True): TEST_MNEMONIC_12_IDENTITY = 'alcohol woman abuse must during monitor noble \ actual mixed trade anger aisle' +# Fixed vectors covering every standard BIP39 mnemonic length. The numeric +# and compact forms represent the same zero-filled entropy as each phrase. +STANDARD_BIP39_TEST_VECTORS = [ + (' '.join(['abandon'] * 11 + ['about']), '0000' * 11 + '0003', bytes(16)), + (' '.join(['abandon'] * 14 + ['address']), '0000' * 14 + '0027', bytes(20)), + (' '.join(['abandon'] * 17 + ['agent']), '0000' * 17 + '0039', bytes(24)), + (' '.join(['abandon'] * 20 + ['admit']), '0000' * 20 + '0029', bytes(28)), + (' '.join(['abandon'] * 23 + ['art']), '0000' * 23 + '0102', bytes(32)), +] + # Seedsigner's own test vectors # See: https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md SEEDSIGNER_MNEMONIC_TEST_VECTORS = [ @@ -2262,6 +2272,15 @@ def test_mnemonic_import(jade): assert xpub_root2 == xpub_root0 assert xpub_root3 == xpub_root0 + # Check all standard BIP39 lengths as text, SeedQR word indices and + # CompactSeedQR entropy. + for mnemonic, seedqr, compactseedqr in STANDARD_BIP39_TEST_VECTORS: + xpub_root0 = _set_wallet(jade, mnemonic=mnemonic) + xpub_root1 = _set_wallet(jade, mnemonic=seedqr) + xpub_root2 = _set_wallet(jade, mnemonic=compactseedqr) + assert xpub_root1 == xpub_root0 + assert xpub_root2 == xpub_root0 + # Check that mnemonic-prefixes are accepted even if they are prefixes to multiple # words, provided one of them is an exact/full match for the entire word. # eg. 'pen' is a prefix to 'pen', 'penalty' and 'pencil' - but is accepted as it @@ -2302,6 +2321,9 @@ def test_mnemonic_import_bad(jade): TEST_MNEMONIC_BCUR_BIP39_TOO_FEW, # too few words TEST_MNEMONIC_BCUR_BIP39_LONG_WORD, # word too long TEST_MNEMONIC_BCUR_BIP39_EMPTY_WORD, # empty word + # libwally extensions outside the standard BIP39 entropy range + wally.bip39_mnemonic_from_bytes(None, bytes(36)), + wally.bip39_mnemonic_from_bytes(None, bytes(40)), ] for i, bad_mnemonic in enumerate(bad_mnemonics): request = jade.build_request('badmnemonic_' + str(i), 'debug_set_mnemonic',