From d39029330f483d76bad4b3f26606b60d81021fc0 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Thu, 18 May 2023 05:03:32 +0200 Subject: [PATCH 1/3] ui/components/trinary_input_string: allow mode to enter only numbers. In BIP-85, the user needs to specify the index at which to derive a secret. This change allows the keyboard input to be used to enter integers. --- src/rust/bitbox02/src/ui/types.rs | 2 ++ src/ui/components/trinary_input_string.c | 10 +++++++++- src/ui/components/trinary_input_string.h | 2 ++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/rust/bitbox02/src/ui/types.rs b/src/rust/bitbox02/src/ui/types.rs index 36ba4d950..c51c56231 100644 --- a/src/rust/bitbox02/src/ui/types.rs +++ b/src/rust/bitbox02/src/ui/types.rs @@ -102,6 +102,7 @@ pub struct TrinaryInputStringParams<'a> { pub title: &'a str, /// Currently specialized to the BIP39 wordlist. Can be extended if needed. pub wordlist: Option<&'a crate::keystore::Bip39Wordlist>, + pub number_input: bool, pub hide: bool, pub special_chars: bool, pub longtouch: bool, @@ -132,6 +133,7 @@ impl<'a> TrinaryInputStringParams<'a> { None => 0, Some(wordlist) => wordlist.len() as _, }, + number_input: self.number_input, hide: self.hide, special_chars: self.special_chars, longtouch: self.longtouch, diff --git a/src/ui/components/trinary_input_string.c b/src/ui/components/trinary_input_string.c index 095326424..bcc38a05c 100644 --- a/src/ui/components/trinary_input_string.c +++ b/src/ui/components/trinary_input_string.c @@ -64,6 +64,7 @@ typedef struct { // Can be NULL. const char* const* wordlist; size_t wordlist_size; + bool number_input; // Only applies if wordlist != NULL: determines if a word from the wordlist was entered. bool can_confirm; // Mask user input with '*'? @@ -294,6 +295,8 @@ static void _set_alphabet(component_t* trinary_input_string) } // Since wordlist is sorted, charset is sorted automatically. trinary_input_char_set_alphabet(trinary_char, charset, 1); + } else if (data->number_input) { + trinary_input_char_set_alphabet(trinary_char, _digits, 1); } else { // Otherwise set the input charset based on the user selected keyboard mode. keyboard_mode_t keyboard_mode = keyboard_current_mode(data->keyboard_switch_component); @@ -450,12 +453,17 @@ component_t* trinary_input_string_create( memset(component, 0, sizeof(component_t)); memset(data, 0, sizeof(data_t)); + if (params->number_input && data->wordlist != NULL) { + Abort("trinary_input_string: invalid params"); + } + data->confirm_cb = confirm_cb; data->confirm_callback_param = confirm_callback_param; data->cancel_cb = cancel_cb; data->cancel_callback_param = cancel_callback_param; data->wordlist = params->wordlist; data->wordlist_size = params->wordlist_size; + data->number_input = params->number_input; data->hide = params->hide; data->longtouch = params->longtouch; data->cancel_is_backbutton = params->cancel_is_backbutton; @@ -481,7 +489,7 @@ component_t* trinary_input_string_create( data->confirm_component = confirm_button_create(params->longtouch, ICON_BUTTON_CHECK); ui_util_add_sub_component(component, data->confirm_component); - if (params->wordlist == NULL) { + if (params->wordlist == NULL && !params->number_input) { data->keyboard_switch_component = keyboard_switch_create(top_slider, params->special_chars, component); ui_util_add_sub_component(component, data->keyboard_switch_component); diff --git a/src/ui/components/trinary_input_string.h b/src/ui/components/trinary_input_string.h index 8365577d1..96ac6602e 100644 --- a/src/ui/components/trinary_input_string.h +++ b/src/ui/components/trinary_input_string.h @@ -26,6 +26,8 @@ typedef struct { const char* title; // Restrict and autocomplete to this list of words. Set to NULL to allow arbitrary input. const char* const* wordlist; + // If true, the user can enter numbers only. + bool number_input; // Set to 0 if wordlist is NULL. size_t wordlist_size; // Mask the chars entered as `*`. For password input. From a60e1d3016b4ab8a0ce73520879d73f184dcb747 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Thu, 18 May 2023 05:03:39 +0200 Subject: [PATCH 2/3] api: add support for BIP85-derived BIP39 mnemonics See https://github.com/bitcoin/bips/blob/master/bip-0085.mediawiki#bip39. Some of the other "apps" (derived secrets) using BIP-85 are not added with this commit and may be added in the future. For now we add the app that is probably used the most, which is deriving BIP-39 mnemonics. The new protobuf messages are empty messages instead of reusing e.g. the `Success {}` message so they can be extended in the future without having to return different types of response messages (e.g. Success OR a custom message). --- CHANGELOG.md | 1 + messages/hww.proto | 2 + messages/keystore.proto | 6 + py/bitbox02/bitbox02/bitbox02/bitbox02.py | 9 ++ .../communication/generated/hww_pb2.py | 8 +- .../communication/generated/hww_pb2.pyi | 20 +++- .../communication/generated/keystore_pb2.py | 6 +- .../communication/generated/keystore_pb2.pyi | 12 ++ py/send_message.py | 4 + src/CMakeLists.txt | 1 + src/keystore.c | 60 ++++++++++ src/keystore.h | 15 +++ src/rust/bitbox02-rust/src/hww/api.rs | 3 + src/rust/bitbox02-rust/src/hww/api/bip85.rs | 108 ++++++++++++++++++ .../bitbox02-rust/src/shiftcrypto.bitbox02.rs | 14 ++- src/rust/bitbox02/src/keystore.rs | 60 ++++++++++ 16 files changed, 316 insertions(+), 13 deletions(-) create mode 100644 src/rust/bitbox02-rust/src/hww/api/bip85.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9daba4153..f1c34c956 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ customers cannot upgrade their bootloader, its changes are recorded separately. For 24 words, this feature was already introduced in 9.4.0. - Bitcoin: don't warn about "unusual" sequence numbers in transactions anymore - Add EIP-1559 transaction support for Ethereum +- Add support for deriving BIP-39 mnemonics according to BIP-85 ### 9.15.0 - Security bugfix: check index of an input's previous output to prevent the fee attack originally diff --git a/messages/hww.proto b/messages/hww.proto index e3dbe8cc9..54c34f6d4 100644 --- a/messages/hww.proto +++ b/messages/hww.proto @@ -66,6 +66,7 @@ message Request { BTCRequest btc = 25; ElectrumEncryptionKeyRequest electrum_encryption_key = 26; CardanoRequest cardano = 27; + BIP85Request bip85 = 28; } } @@ -87,5 +88,6 @@ message Response { BTCResponse btc = 13; ElectrumEncryptionKeyResponse electrum_encryption_key = 14; CardanoResponse cardano = 15; + BIP85Response bip85 = 16; } } diff --git a/messages/keystore.proto b/messages/keystore.proto index 15f703f9f..e4a94e6d5 100644 --- a/messages/keystore.proto +++ b/messages/keystore.proto @@ -11,3 +11,9 @@ message ElectrumEncryptionKeyRequest { message ElectrumEncryptionKeyResponse { string key = 1; } + +message BIP85Request { +} + +message BIP85Response { +} diff --git a/py/bitbox02/bitbox02/bitbox02/bitbox02.py b/py/bitbox02/bitbox02/bitbox02/bitbox02.py index 0725a82af..19a59f666 100644 --- a/py/bitbox02/bitbox02/bitbox02/bitbox02.py +++ b/py/bitbox02/bitbox02/bitbox02/bitbox02.py @@ -678,6 +678,15 @@ def electrum_encryption_key(self, keypath: Sequence[int]) -> str: ) return self._msg_query(request).electrum_encryption_key.key + def bip85(self) -> None: + """Invokes the BIP-85 workflow on the device""" + self._require_atleast(semver.VersionInfo(9, 16, 0)) + + # pylint: disable=no-member + request = hww.Request() + request.bip85.CopyFrom(keystore.BIP85Request()) + self._msg_query(request) + def enable_mnemonic_passphrase(self) -> None: """ Enable the bip39 passphrase. diff --git a/py/bitbox02/bitbox02/communication/generated/hww_pb2.py b/py/bitbox02/bitbox02/communication/generated/hww_pb2.py index 8d6efa0a2..4606d1d94 100644 --- a/py/bitbox02/bitbox02/communication/generated/hww_pb2.py +++ b/py/bitbox02/bitbox02/communication/generated/hww_pb2.py @@ -23,7 +23,7 @@ from . import perform_attestation_pb2 as perform__attestation__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\thww.proto\x12\x14shiftcrypto.bitbox02\x1a\x0c\x63ommon.proto\x1a\x15\x62\x61\x63kup_commands.proto\x1a\x15\x62itbox02_system.proto\x1a\tbtc.proto\x1a\rcardano.proto\x1a\teth.proto\x1a\x0ekeystore.proto\x1a\x0emnemonic.proto\x1a\x0csystem.proto\x1a\x19perform_attestation.proto\"&\n\x05\x45rror\x12\x0c\n\x04\x63ode\x18\x01 \x01(\x05\x12\x0f\n\x07message\x18\x02 \x01(\t\"\t\n\x07Success\"\xc8\r\n\x07Request\x12\x41\n\x0b\x64\x65vice_name\x18\x02 \x01(\x0b\x32*.shiftcrypto.bitbox02.SetDeviceNameRequestH\x00\x12I\n\x0f\x64\x65vice_language\x18\x03 \x01(\x0b\x32..shiftcrypto.bitbox02.SetDeviceLanguageRequestH\x00\x12>\n\x0b\x64\x65vice_info\x18\x04 \x01(\x0b\x32\'.shiftcrypto.bitbox02.DeviceInfoRequestH\x00\x12@\n\x0cset_password\x18\x05 \x01(\x0b\x32(.shiftcrypto.bitbox02.SetPasswordRequestH\x00\x12\x42\n\rcreate_backup\x18\x06 \x01(\x0b\x32).shiftcrypto.bitbox02.CreateBackupRequestH\x00\x12\x42\n\rshow_mnemonic\x18\x07 \x01(\x0b\x32).shiftcrypto.bitbox02.ShowMnemonicRequestH\x00\x12\x36\n\x07\x62tc_pub\x18\x08 \x01(\x0b\x32#.shiftcrypto.bitbox02.BTCPubRequestH\x00\x12\x41\n\rbtc_sign_init\x18\t \x01(\x0b\x32(.shiftcrypto.bitbox02.BTCSignInitRequestH\x00\x12\x43\n\x0e\x62tc_sign_input\x18\n \x01(\x0b\x32).shiftcrypto.bitbox02.BTCSignInputRequestH\x00\x12\x45\n\x0f\x62tc_sign_output\x18\x0b \x01(\x0b\x32*.shiftcrypto.bitbox02.BTCSignOutputRequestH\x00\x12O\n\x14insert_remove_sdcard\x18\x0c \x01(\x0b\x32/.shiftcrypto.bitbox02.InsertRemoveSDCardRequestH\x00\x12@\n\x0c\x63heck_sdcard\x18\r \x01(\x0b\x32(.shiftcrypto.bitbox02.CheckSDCardRequestH\x00\x12\x64\n\x1fset_mnemonic_passphrase_enabled\x18\x0e \x01(\x0b\x32\x39.shiftcrypto.bitbox02.SetMnemonicPassphraseEnabledRequestH\x00\x12@\n\x0clist_backups\x18\x0f \x01(\x0b\x32(.shiftcrypto.bitbox02.ListBackupsRequestH\x00\x12\x44\n\x0erestore_backup\x18\x10 \x01(\x0b\x32*.shiftcrypto.bitbox02.RestoreBackupRequestH\x00\x12N\n\x13perform_attestation\x18\x11 \x01(\x0b\x32/.shiftcrypto.bitbox02.PerformAttestationRequestH\x00\x12\x35\n\x06reboot\x18\x12 \x01(\x0b\x32#.shiftcrypto.bitbox02.RebootRequestH\x00\x12@\n\x0c\x63heck_backup\x18\x13 \x01(\x0b\x32(.shiftcrypto.bitbox02.CheckBackupRequestH\x00\x12/\n\x03\x65th\x18\x14 \x01(\x0b\x32 .shiftcrypto.bitbox02.ETHRequestH\x00\x12\x33\n\x05reset\x18\x15 \x01(\x0b\x32\".shiftcrypto.bitbox02.ResetRequestH\x00\x12Q\n\x15restore_from_mnemonic\x18\x16 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.RestoreFromMnemonicRequestH\x00\x12\x43\n\x0b\x66ingerprint\x18\x18 \x01(\x0b\x32,.shiftcrypto.bitbox02.RootFingerprintRequestH\x00\x12/\n\x03\x62tc\x18\x19 \x01(\x0b\x32 .shiftcrypto.bitbox02.BTCRequestH\x00\x12U\n\x17\x65lectrum_encryption_key\x18\x1a \x01(\x0b\x32\x32.shiftcrypto.bitbox02.ElectrumEncryptionKeyRequestH\x00\x12\x37\n\x07\x63\x61rdano\x18\x1b \x01(\x0b\x32$.shiftcrypto.bitbox02.CardanoRequestH\x00\x42\t\n\x07requestJ\x04\x08\x01\x10\x02J\x04\x08\x17\x10\x18\"\x89\x07\n\x08Response\x12\x30\n\x07success\x18\x01 \x01(\x0b\x32\x1d.shiftcrypto.bitbox02.SuccessH\x00\x12,\n\x05\x65rror\x18\x02 \x01(\x0b\x32\x1b.shiftcrypto.bitbox02.ErrorH\x00\x12?\n\x0b\x64\x65vice_info\x18\x04 \x01(\x0b\x32(.shiftcrypto.bitbox02.DeviceInfoResponseH\x00\x12\x30\n\x03pub\x18\x05 \x01(\x0b\x32!.shiftcrypto.bitbox02.PubResponseH\x00\x12\x42\n\rbtc_sign_next\x18\x06 \x01(\x0b\x32).shiftcrypto.bitbox02.BTCSignNextResponseH\x00\x12\x41\n\x0clist_backups\x18\x07 \x01(\x0b\x32).shiftcrypto.bitbox02.ListBackupsResponseH\x00\x12\x41\n\x0c\x63heck_backup\x18\x08 \x01(\x0b\x32).shiftcrypto.bitbox02.CheckBackupResponseH\x00\x12O\n\x13perform_attestation\x18\t \x01(\x0b\x32\x30.shiftcrypto.bitbox02.PerformAttestationResponseH\x00\x12\x41\n\x0c\x63heck_sdcard\x18\n \x01(\x0b\x32).shiftcrypto.bitbox02.CheckSDCardResponseH\x00\x12\x30\n\x03\x65th\x18\x0b \x01(\x0b\x32!.shiftcrypto.bitbox02.ETHResponseH\x00\x12\x44\n\x0b\x66ingerprint\x18\x0c \x01(\x0b\x32-.shiftcrypto.bitbox02.RootFingerprintResponseH\x00\x12\x30\n\x03\x62tc\x18\r \x01(\x0b\x32!.shiftcrypto.bitbox02.BTCResponseH\x00\x12V\n\x17\x65lectrum_encryption_key\x18\x0e \x01(\x0b\x32\x33.shiftcrypto.bitbox02.ElectrumEncryptionKeyResponseH\x00\x12\x38\n\x07\x63\x61rdano\x18\x0f \x01(\x0b\x32%.shiftcrypto.bitbox02.CardanoResponseH\x00\x42\n\n\x08responseJ\x04\x08\x03\x10\x04\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\thww.proto\x12\x14shiftcrypto.bitbox02\x1a\x0c\x63ommon.proto\x1a\x15\x62\x61\x63kup_commands.proto\x1a\x15\x62itbox02_system.proto\x1a\tbtc.proto\x1a\rcardano.proto\x1a\teth.proto\x1a\x0ekeystore.proto\x1a\x0emnemonic.proto\x1a\x0csystem.proto\x1a\x19perform_attestation.proto\"&\n\x05\x45rror\x12\x0c\n\x04\x63ode\x18\x01 \x01(\x05\x12\x0f\n\x07message\x18\x02 \x01(\t\"\t\n\x07Success\"\xfd\r\n\x07Request\x12\x41\n\x0b\x64\x65vice_name\x18\x02 \x01(\x0b\x32*.shiftcrypto.bitbox02.SetDeviceNameRequestH\x00\x12I\n\x0f\x64\x65vice_language\x18\x03 \x01(\x0b\x32..shiftcrypto.bitbox02.SetDeviceLanguageRequestH\x00\x12>\n\x0b\x64\x65vice_info\x18\x04 \x01(\x0b\x32\'.shiftcrypto.bitbox02.DeviceInfoRequestH\x00\x12@\n\x0cset_password\x18\x05 \x01(\x0b\x32(.shiftcrypto.bitbox02.SetPasswordRequestH\x00\x12\x42\n\rcreate_backup\x18\x06 \x01(\x0b\x32).shiftcrypto.bitbox02.CreateBackupRequestH\x00\x12\x42\n\rshow_mnemonic\x18\x07 \x01(\x0b\x32).shiftcrypto.bitbox02.ShowMnemonicRequestH\x00\x12\x36\n\x07\x62tc_pub\x18\x08 \x01(\x0b\x32#.shiftcrypto.bitbox02.BTCPubRequestH\x00\x12\x41\n\rbtc_sign_init\x18\t \x01(\x0b\x32(.shiftcrypto.bitbox02.BTCSignInitRequestH\x00\x12\x43\n\x0e\x62tc_sign_input\x18\n \x01(\x0b\x32).shiftcrypto.bitbox02.BTCSignInputRequestH\x00\x12\x45\n\x0f\x62tc_sign_output\x18\x0b \x01(\x0b\x32*.shiftcrypto.bitbox02.BTCSignOutputRequestH\x00\x12O\n\x14insert_remove_sdcard\x18\x0c \x01(\x0b\x32/.shiftcrypto.bitbox02.InsertRemoveSDCardRequestH\x00\x12@\n\x0c\x63heck_sdcard\x18\r \x01(\x0b\x32(.shiftcrypto.bitbox02.CheckSDCardRequestH\x00\x12\x64\n\x1fset_mnemonic_passphrase_enabled\x18\x0e \x01(\x0b\x32\x39.shiftcrypto.bitbox02.SetMnemonicPassphraseEnabledRequestH\x00\x12@\n\x0clist_backups\x18\x0f \x01(\x0b\x32(.shiftcrypto.bitbox02.ListBackupsRequestH\x00\x12\x44\n\x0erestore_backup\x18\x10 \x01(\x0b\x32*.shiftcrypto.bitbox02.RestoreBackupRequestH\x00\x12N\n\x13perform_attestation\x18\x11 \x01(\x0b\x32/.shiftcrypto.bitbox02.PerformAttestationRequestH\x00\x12\x35\n\x06reboot\x18\x12 \x01(\x0b\x32#.shiftcrypto.bitbox02.RebootRequestH\x00\x12@\n\x0c\x63heck_backup\x18\x13 \x01(\x0b\x32(.shiftcrypto.bitbox02.CheckBackupRequestH\x00\x12/\n\x03\x65th\x18\x14 \x01(\x0b\x32 .shiftcrypto.bitbox02.ETHRequestH\x00\x12\x33\n\x05reset\x18\x15 \x01(\x0b\x32\".shiftcrypto.bitbox02.ResetRequestH\x00\x12Q\n\x15restore_from_mnemonic\x18\x16 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.RestoreFromMnemonicRequestH\x00\x12\x43\n\x0b\x66ingerprint\x18\x18 \x01(\x0b\x32,.shiftcrypto.bitbox02.RootFingerprintRequestH\x00\x12/\n\x03\x62tc\x18\x19 \x01(\x0b\x32 .shiftcrypto.bitbox02.BTCRequestH\x00\x12U\n\x17\x65lectrum_encryption_key\x18\x1a \x01(\x0b\x32\x32.shiftcrypto.bitbox02.ElectrumEncryptionKeyRequestH\x00\x12\x37\n\x07\x63\x61rdano\x18\x1b \x01(\x0b\x32$.shiftcrypto.bitbox02.CardanoRequestH\x00\x12\x33\n\x05\x62ip85\x18\x1c \x01(\x0b\x32\".shiftcrypto.bitbox02.BIP85RequestH\x00\x42\t\n\x07requestJ\x04\x08\x01\x10\x02J\x04\x08\x17\x10\x18\"\xbf\x07\n\x08Response\x12\x30\n\x07success\x18\x01 \x01(\x0b\x32\x1d.shiftcrypto.bitbox02.SuccessH\x00\x12,\n\x05\x65rror\x18\x02 \x01(\x0b\x32\x1b.shiftcrypto.bitbox02.ErrorH\x00\x12?\n\x0b\x64\x65vice_info\x18\x04 \x01(\x0b\x32(.shiftcrypto.bitbox02.DeviceInfoResponseH\x00\x12\x30\n\x03pub\x18\x05 \x01(\x0b\x32!.shiftcrypto.bitbox02.PubResponseH\x00\x12\x42\n\rbtc_sign_next\x18\x06 \x01(\x0b\x32).shiftcrypto.bitbox02.BTCSignNextResponseH\x00\x12\x41\n\x0clist_backups\x18\x07 \x01(\x0b\x32).shiftcrypto.bitbox02.ListBackupsResponseH\x00\x12\x41\n\x0c\x63heck_backup\x18\x08 \x01(\x0b\x32).shiftcrypto.bitbox02.CheckBackupResponseH\x00\x12O\n\x13perform_attestation\x18\t \x01(\x0b\x32\x30.shiftcrypto.bitbox02.PerformAttestationResponseH\x00\x12\x41\n\x0c\x63heck_sdcard\x18\n \x01(\x0b\x32).shiftcrypto.bitbox02.CheckSDCardResponseH\x00\x12\x30\n\x03\x65th\x18\x0b \x01(\x0b\x32!.shiftcrypto.bitbox02.ETHResponseH\x00\x12\x44\n\x0b\x66ingerprint\x18\x0c \x01(\x0b\x32-.shiftcrypto.bitbox02.RootFingerprintResponseH\x00\x12\x30\n\x03\x62tc\x18\r \x01(\x0b\x32!.shiftcrypto.bitbox02.BTCResponseH\x00\x12V\n\x17\x65lectrum_encryption_key\x18\x0e \x01(\x0b\x32\x33.shiftcrypto.bitbox02.ElectrumEncryptionKeyResponseH\x00\x12\x38\n\x07\x63\x61rdano\x18\x0f \x01(\x0b\x32%.shiftcrypto.bitbox02.CardanoResponseH\x00\x12\x34\n\x05\x62ip85\x18\x10 \x01(\x0b\x32#.shiftcrypto.bitbox02.BIP85ResponseH\x00\x42\n\n\x08responseJ\x04\x08\x03\x10\x04\x62\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'hww_pb2', globals()) @@ -35,7 +35,7 @@ _SUCCESS._serialized_start=245 _SUCCESS._serialized_end=254 _REQUEST._serialized_start=257 - _REQUEST._serialized_end=1993 - _RESPONSE._serialized_start=1996 - _RESPONSE._serialized_end=2901 + _REQUEST._serialized_end=2046 + _RESPONSE._serialized_start=2049 + _RESPONSE._serialized_end=3008 # @@protoc_insertion_point(module_scope) diff --git a/py/bitbox02/bitbox02/communication/generated/hww_pb2.pyi b/py/bitbox02/bitbox02/communication/generated/hww_pb2.pyi index 2244acb8e..df9aeacf1 100644 --- a/py/bitbox02/bitbox02/communication/generated/hww_pb2.pyi +++ b/py/bitbox02/bitbox02/communication/generated/hww_pb2.pyi @@ -67,6 +67,7 @@ class Request(google.protobuf.message.Message): BTC_FIELD_NUMBER: builtins.int ELECTRUM_ENCRYPTION_KEY_FIELD_NUMBER: builtins.int CARDANO_FIELD_NUMBER: builtins.int + BIP85_FIELD_NUMBER: builtins.int @property def device_name(self) -> bitbox02_system_pb2.SetDeviceNameRequest: """removed: RandomNumberRequest random_number = 1;""" @@ -121,6 +122,8 @@ class Request(google.protobuf.message.Message): def electrum_encryption_key(self) -> keystore_pb2.ElectrumEncryptionKeyRequest: ... @property def cardano(self) -> cardano_pb2.CardanoRequest: ... + @property + def bip85(self) -> keystore_pb2.BIP85Request: ... def __init__(self, *, device_name: typing.Optional[bitbox02_system_pb2.SetDeviceNameRequest] = ..., @@ -148,10 +151,11 @@ class Request(google.protobuf.message.Message): btc: typing.Optional[btc_pb2.BTCRequest] = ..., electrum_encryption_key: typing.Optional[keystore_pb2.ElectrumEncryptionKeyRequest] = ..., cardano: typing.Optional[cardano_pb2.CardanoRequest] = ..., + bip85: typing.Optional[keystore_pb2.BIP85Request] = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["btc",b"btc","btc_pub",b"btc_pub","btc_sign_init",b"btc_sign_init","btc_sign_input",b"btc_sign_input","btc_sign_output",b"btc_sign_output","cardano",b"cardano","check_backup",b"check_backup","check_sdcard",b"check_sdcard","create_backup",b"create_backup","device_info",b"device_info","device_language",b"device_language","device_name",b"device_name","electrum_encryption_key",b"electrum_encryption_key","eth",b"eth","fingerprint",b"fingerprint","insert_remove_sdcard",b"insert_remove_sdcard","list_backups",b"list_backups","perform_attestation",b"perform_attestation","reboot",b"reboot","request",b"request","reset",b"reset","restore_backup",b"restore_backup","restore_from_mnemonic",b"restore_from_mnemonic","set_mnemonic_passphrase_enabled",b"set_mnemonic_passphrase_enabled","set_password",b"set_password","show_mnemonic",b"show_mnemonic"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["btc",b"btc","btc_pub",b"btc_pub","btc_sign_init",b"btc_sign_init","btc_sign_input",b"btc_sign_input","btc_sign_output",b"btc_sign_output","cardano",b"cardano","check_backup",b"check_backup","check_sdcard",b"check_sdcard","create_backup",b"create_backup","device_info",b"device_info","device_language",b"device_language","device_name",b"device_name","electrum_encryption_key",b"electrum_encryption_key","eth",b"eth","fingerprint",b"fingerprint","insert_remove_sdcard",b"insert_remove_sdcard","list_backups",b"list_backups","perform_attestation",b"perform_attestation","reboot",b"reboot","request",b"request","reset",b"reset","restore_backup",b"restore_backup","restore_from_mnemonic",b"restore_from_mnemonic","set_mnemonic_passphrase_enabled",b"set_mnemonic_passphrase_enabled","set_password",b"set_password","show_mnemonic",b"show_mnemonic"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["request",b"request"]) -> typing.Optional[typing_extensions.Literal["device_name","device_language","device_info","set_password","create_backup","show_mnemonic","btc_pub","btc_sign_init","btc_sign_input","btc_sign_output","insert_remove_sdcard","check_sdcard","set_mnemonic_passphrase_enabled","list_backups","restore_backup","perform_attestation","reboot","check_backup","eth","reset","restore_from_mnemonic","fingerprint","btc","electrum_encryption_key","cardano"]]: ... + def HasField(self, field_name: typing_extensions.Literal["bip85",b"bip85","btc",b"btc","btc_pub",b"btc_pub","btc_sign_init",b"btc_sign_init","btc_sign_input",b"btc_sign_input","btc_sign_output",b"btc_sign_output","cardano",b"cardano","check_backup",b"check_backup","check_sdcard",b"check_sdcard","create_backup",b"create_backup","device_info",b"device_info","device_language",b"device_language","device_name",b"device_name","electrum_encryption_key",b"electrum_encryption_key","eth",b"eth","fingerprint",b"fingerprint","insert_remove_sdcard",b"insert_remove_sdcard","list_backups",b"list_backups","perform_attestation",b"perform_attestation","reboot",b"reboot","request",b"request","reset",b"reset","restore_backup",b"restore_backup","restore_from_mnemonic",b"restore_from_mnemonic","set_mnemonic_passphrase_enabled",b"set_mnemonic_passphrase_enabled","set_password",b"set_password","show_mnemonic",b"show_mnemonic"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["bip85",b"bip85","btc",b"btc","btc_pub",b"btc_pub","btc_sign_init",b"btc_sign_init","btc_sign_input",b"btc_sign_input","btc_sign_output",b"btc_sign_output","cardano",b"cardano","check_backup",b"check_backup","check_sdcard",b"check_sdcard","create_backup",b"create_backup","device_info",b"device_info","device_language",b"device_language","device_name",b"device_name","electrum_encryption_key",b"electrum_encryption_key","eth",b"eth","fingerprint",b"fingerprint","insert_remove_sdcard",b"insert_remove_sdcard","list_backups",b"list_backups","perform_attestation",b"perform_attestation","reboot",b"reboot","request",b"request","reset",b"reset","restore_backup",b"restore_backup","restore_from_mnemonic",b"restore_from_mnemonic","set_mnemonic_passphrase_enabled",b"set_mnemonic_passphrase_enabled","set_password",b"set_password","show_mnemonic",b"show_mnemonic"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["request",b"request"]) -> typing.Optional[typing_extensions.Literal["device_name","device_language","device_info","set_password","create_backup","show_mnemonic","btc_pub","btc_sign_init","btc_sign_input","btc_sign_output","insert_remove_sdcard","check_sdcard","set_mnemonic_passphrase_enabled","list_backups","restore_backup","perform_attestation","reboot","check_backup","eth","reset","restore_from_mnemonic","fingerprint","btc","electrum_encryption_key","cardano","bip85"]]: ... global___Request = Request class Response(google.protobuf.message.Message): @@ -170,6 +174,7 @@ class Response(google.protobuf.message.Message): BTC_FIELD_NUMBER: builtins.int ELECTRUM_ENCRYPTION_KEY_FIELD_NUMBER: builtins.int CARDANO_FIELD_NUMBER: builtins.int + BIP85_FIELD_NUMBER: builtins.int @property def success(self) -> global___Success: ... @property @@ -200,6 +205,8 @@ class Response(google.protobuf.message.Message): def electrum_encryption_key(self) -> keystore_pb2.ElectrumEncryptionKeyResponse: ... @property def cardano(self) -> cardano_pb2.CardanoResponse: ... + @property + def bip85(self) -> keystore_pb2.BIP85Response: ... def __init__(self, *, success: typing.Optional[global___Success] = ..., @@ -216,8 +223,9 @@ class Response(google.protobuf.message.Message): btc: typing.Optional[btc_pb2.BTCResponse] = ..., electrum_encryption_key: typing.Optional[keystore_pb2.ElectrumEncryptionKeyResponse] = ..., cardano: typing.Optional[cardano_pb2.CardanoResponse] = ..., + bip85: typing.Optional[keystore_pb2.BIP85Response] = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["btc",b"btc","btc_sign_next",b"btc_sign_next","cardano",b"cardano","check_backup",b"check_backup","check_sdcard",b"check_sdcard","device_info",b"device_info","electrum_encryption_key",b"electrum_encryption_key","error",b"error","eth",b"eth","fingerprint",b"fingerprint","list_backups",b"list_backups","perform_attestation",b"perform_attestation","pub",b"pub","response",b"response","success",b"success"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["btc",b"btc","btc_sign_next",b"btc_sign_next","cardano",b"cardano","check_backup",b"check_backup","check_sdcard",b"check_sdcard","device_info",b"device_info","electrum_encryption_key",b"electrum_encryption_key","error",b"error","eth",b"eth","fingerprint",b"fingerprint","list_backups",b"list_backups","perform_attestation",b"perform_attestation","pub",b"pub","response",b"response","success",b"success"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["response",b"response"]) -> typing.Optional[typing_extensions.Literal["success","error","device_info","pub","btc_sign_next","list_backups","check_backup","perform_attestation","check_sdcard","eth","fingerprint","btc","electrum_encryption_key","cardano"]]: ... + def HasField(self, field_name: typing_extensions.Literal["bip85",b"bip85","btc",b"btc","btc_sign_next",b"btc_sign_next","cardano",b"cardano","check_backup",b"check_backup","check_sdcard",b"check_sdcard","device_info",b"device_info","electrum_encryption_key",b"electrum_encryption_key","error",b"error","eth",b"eth","fingerprint",b"fingerprint","list_backups",b"list_backups","perform_attestation",b"perform_attestation","pub",b"pub","response",b"response","success",b"success"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["bip85",b"bip85","btc",b"btc","btc_sign_next",b"btc_sign_next","cardano",b"cardano","check_backup",b"check_backup","check_sdcard",b"check_sdcard","device_info",b"device_info","electrum_encryption_key",b"electrum_encryption_key","error",b"error","eth",b"eth","fingerprint",b"fingerprint","list_backups",b"list_backups","perform_attestation",b"perform_attestation","pub",b"pub","response",b"response","success",b"success"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["response",b"response"]) -> typing.Optional[typing_extensions.Literal["success","error","device_info","pub","btc_sign_next","list_backups","check_backup","perform_attestation","check_sdcard","eth","fingerprint","btc","electrum_encryption_key","cardano","bip85"]]: ... global___Response = Response diff --git a/py/bitbox02/bitbox02/communication/generated/keystore_pb2.py b/py/bitbox02/bitbox02/communication/generated/keystore_pb2.py index ec262caf7..66139dc44 100644 --- a/py/bitbox02/bitbox02/communication/generated/keystore_pb2.py +++ b/py/bitbox02/bitbox02/communication/generated/keystore_pb2.py @@ -13,7 +13,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0ekeystore.proto\x12\x14shiftcrypto.bitbox02\"/\n\x1c\x45lectrumEncryptionKeyRequest\x12\x0f\n\x07keypath\x18\x01 \x03(\r\",\n\x1d\x45lectrumEncryptionKeyResponse\x12\x0b\n\x03key\x18\x01 \x01(\tb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0ekeystore.proto\x12\x14shiftcrypto.bitbox02\"/\n\x1c\x45lectrumEncryptionKeyRequest\x12\x0f\n\x07keypath\x18\x01 \x03(\r\",\n\x1d\x45lectrumEncryptionKeyResponse\x12\x0b\n\x03key\x18\x01 \x01(\t\"\x0e\n\x0c\x42IP85Request\"\x0f\n\rBIP85Responseb\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'keystore_pb2', globals()) @@ -24,4 +24,8 @@ _ELECTRUMENCRYPTIONKEYREQUEST._serialized_end=87 _ELECTRUMENCRYPTIONKEYRESPONSE._serialized_start=89 _ELECTRUMENCRYPTIONKEYRESPONSE._serialized_end=133 + _BIP85REQUEST._serialized_start=135 + _BIP85REQUEST._serialized_end=149 + _BIP85RESPONSE._serialized_start=151 + _BIP85RESPONSE._serialized_end=166 # @@protoc_insertion_point(module_scope) diff --git a/py/bitbox02/bitbox02/communication/generated/keystore_pb2.pyi b/py/bitbox02/bitbox02/communication/generated/keystore_pb2.pyi index 3296d1162..03ef6c345 100644 --- a/py/bitbox02/bitbox02/communication/generated/keystore_pb2.pyi +++ b/py/bitbox02/bitbox02/communication/generated/keystore_pb2.pyi @@ -33,3 +33,15 @@ class ElectrumEncryptionKeyResponse(google.protobuf.message.Message): ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["key",b"key"]) -> None: ... global___ElectrumEncryptionKeyResponse = ElectrumEncryptionKeyResponse + +class BIP85Request(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + def __init__(self, + ) -> None: ... +global___BIP85Request = BIP85Request + +class BIP85Response(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + def __init__(self, + ) -> None: ... +global___BIP85Response = BIP85Response diff --git a/py/send_message.py b/py/send_message.py index 9fe7d6d5c..a56f66487 100755 --- a/py/send_message.py +++ b/py/send_message.py @@ -338,6 +338,9 @@ def _get_electrum_encryption_key(self) -> None: ), ) + def _bip85(self) -> None: + self._device.bip85() + def _btc_address(self) -> None: def address(display: bool) -> str: # pylint: disable=no-member @@ -1386,6 +1389,7 @@ def _menu_init(self) -> None: ("Sign Ethereum Typed Message (EIP-712)", self._sign_eth_typed_message), ("Cardano", self._cardano), ("Show Electrum wallet encryption key", self._get_electrum_encryption_key), + ("BIP85", self._bip85), ("Reset Device", self._reset_device), ) choice = ask_user(choices) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 994a48cd4..1adcc8949 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -316,6 +316,7 @@ add_custom_target(rust-bindgen --allowlist-function keystore_get_bip39_mnemonic --allowlist-function keystore_get_bip39_word --allowlist-function keystore_get_ed25519_seed + --allowlist-function keystore_bip85_bip39 --allowlist-function keystore_secp256k1_compressed_to_uncompressed --allowlist-function keystore_secp256k1_nonce_commit --allowlist-function keystore_secp256k1_sign diff --git a/src/keystore.c b/src/keystore.c index e56ed95f0..321be2de4 100644 --- a/src/keystore.c +++ b/src/keystore.c @@ -804,6 +804,66 @@ bool keystore_get_ed25519_seed(uint8_t* seed_out) return true; } +static bool _bip85_entropy(const uint32_t* keypath, size_t keypath_len, uint8_t* out) +{ + struct ext_key xprv __attribute__((__cleanup__(keystore_zero_xkey))) = {0}; + if (!_get_xprv_twice(keypath, keypath_len, &xprv)) { + return false; + } + const uint8_t* priv_key = xprv.priv_key + 1; // first byte is 0 + const uint8_t key[] = "bip-entropy-from-k"; + return wally_hmac_sha512(key, sizeof(key), priv_key, 32, out, 64) == WALLY_OK; +} + +bool keystore_bip85_bip39( + uint32_t words, + uint32_t index, + char* mnemonic_out, + size_t mnemonic_out_size) +{ + size_t seed_size; + switch (words) { + case 12: + seed_size = 16; + break; + case 18: + seed_size = 24; + break; + case 24: + seed_size = 32; + break; + default: + return false; + } + + if (index >= BIP32_INITIAL_HARDENED_CHILD) { + return false; + } + + const uint32_t keypath[] = { + 83696968 + BIP32_INITIAL_HARDENED_CHILD, + 39 + BIP32_INITIAL_HARDENED_CHILD, + 0 + BIP32_INITIAL_HARDENED_CHILD, + words + BIP32_INITIAL_HARDENED_CHILD, + index + BIP32_INITIAL_HARDENED_CHILD, + }; + + uint8_t entropy[64] = {0}; + UTIL_CLEANUP_64(entropy); + if (!_bip85_entropy(keypath, sizeof(keypath) / sizeof(uint32_t), entropy)) { + return false; + } + + char* mnemonic = NULL; + if (bip39_mnemonic_from_bytes(NULL, entropy, seed_size, &mnemonic) != WALLY_OK) { + return false; + } + int snprintf_result = snprintf(mnemonic_out, mnemonic_out_size, "%s", mnemonic); + util_cleanup_str(&mnemonic); + free(mnemonic); + return snprintf_result >= 0 && snprintf_result < (int)mnemonic_out_size; +} + USE_RESULT bool keystore_encode_xpub_at_keypath( const uint32_t* keypath, size_t keypath_len, diff --git a/src/keystore.h b/src/keystore.h index 54ef8844d..2fdce5c97 100644 --- a/src/keystore.h +++ b/src/keystore.h @@ -238,6 +238,21 @@ USE_RESULT bool keystore_get_u2f_seed(uint8_t* seed_out); */ USE_RESULT bool keystore_get_ed25519_seed(uint8_t* seed_out); +/** + * Computes a BIP39 mnemonic according to BIP-85: + * https://github.com/bitcoin/bips/blob/master/bip-0085.mediawiki#bip39 + * @param[in] words must be 12, 18 or 24. + * @param[in] index must be smaller than `BIP32_INITIAL_HARDENED_CHILD`. + * @param[out] mnemonic_out resulting mnemonic + * @param[in] mnemonic_out_size size of mnemonic_out. Should be at least 216 bytes (longest possible + * 24 word phrase plus null terminator). + */ +USE_RESULT bool keystore_bip85_bip39( + uint32_t words, + uint32_t index, + char* mnemonic_out, + size_t mnemonic_out_size); + /** * Encode an xpub at the given `keypath` as 78 bytes according to BIP32. The version bytes are * the ones corresponding to `xpub`, i.e. 0x0488B21E. diff --git a/src/rust/bitbox02-rust/src/hww/api.rs b/src/rust/bitbox02-rust/src/hww/api.rs index 3f4fd3151..ba528f22a 100644 --- a/src/rust/bitbox02-rust/src/hww/api.rs +++ b/src/rust/bitbox02-rust/src/hww/api.rs @@ -26,6 +26,7 @@ pub mod bitcoin; mod cardano; mod backup; +mod bip85; mod device_info; mod electrum; mod reset; @@ -133,6 +134,7 @@ fn can_call(request: &Request) -> bool { Request::Eth(_) => matches!(state, State::Initialized), Request::Reset(_) => matches!(state, State::Initialized), Request::Cardano(_) => matches!(state, State::Initialized), + Request::Bip85(_) => matches!(state, State::Initialized), } } @@ -181,6 +183,7 @@ async fn process_api(request: &Request) -> Result { .map(|r| Response::Cardano(pb::CardanoResponse { response: Some(r) })), #[cfg(not(feature = "app-cardano"))] Request::Cardano(_) => Err(Error::Disabled), + Request::Bip85(ref request) => bip85::process(request).await, _ => Err(Error::InvalidInput), } } diff --git a/src/rust/bitbox02-rust/src/hww/api/bip85.rs b/src/rust/bitbox02-rust/src/hww/api/bip85.rs new file mode 100644 index 000000000..52a2a586b --- /dev/null +++ b/src/rust/bitbox02-rust/src/hww/api/bip85.rs @@ -0,0 +1,108 @@ +// Copyright 2023 Shift Crypto AG +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::pb; +use super::Error; + +use pb::response::Response; + +use bitbox02::keystore; + +use crate::workflow::trinary_choice::{choose, TrinaryChoice}; +use crate::workflow::{confirm, menu, mnemonic, status, trinary_input_string}; + +use alloc::vec::Vec; + +/// Derives and displays a BIP-39 seed according to BIP-85: +/// https://github.com/bitcoin/bips/blob/master/bip-0085.mediawiki#bip39. +pub async fn process(pb::Bip85Request {}: &pb::Bip85Request) -> Result { + confirm::confirm(&confirm::Params { + title: "BIP-85", + body: "Derive BIP-39\nmnemonic?", + accept_is_nextarrow: true, + ..Default::default() + }) + .await?; + + confirm::confirm(&confirm::Params { + title: "BIP-85", + body: "This is an advanced feature. Proceed only if you know what you are doing.", + scrollable: true, + accept_is_nextarrow: true, + ..Default::default() + }) + .await?; + + let num_words: u32 = match choose("How many words?", "12", "18", "24").await { + TrinaryChoice::TRINARY_CHOICE_LEFT => 12, + TrinaryChoice::TRINARY_CHOICE_MIDDLE => 18, + TrinaryChoice::TRINARY_CHOICE_RIGHT => 24, + }; + + status::status(&format!("{} words", num_words), true).await; + + // Pick index. The first few are quick-access. "More" leads to a full number input keyboard. + let index: u32 = + match menu::pick(&["0", "1", "2", "3", "4", "More"], Some("Select index")).await? { + i @ 0..=4 => i.into(), + 5 => { + let number_string = trinary_input_string::enter( + &trinary_input_string::Params { + title: "Enter index", + number_input: true, + longtouch: true, + ..Default::default() + }, + trinary_input_string::CanCancel::Yes, + "", + ) + .await?; + match number_string.as_str().parse::() { + Ok(i) if i < util::bip32::HARDENED => i, + _ => { + status::status("Invalid index", false).await; + return Err(Error::InvalidInput); + } + } + } + 6.. => panic!("bip85 error"), + }; + + status::status(&format!("Index: {}", index), true).await; + + confirm::confirm(&confirm::Params { + title: "Keypath", + body: &format!("m/83696968'/39'/0'/{}'/{}'", num_words, index), + scrollable: true, + longtouch: true, + ..Default::default() + }) + .await?; + + confirm::confirm(&confirm::Params { + title: "", + body: &format!("{} word mnemonic\nfollows", num_words), + accept_is_nextarrow: true, + ..Default::default() + }) + .await?; + + let mnemonic = keystore::bip85_bip39(num_words, index)?; + let words: Vec<&str> = mnemonic.split(' ').collect(); + mnemonic::show_mnemonic(&words).await?; + + status::status("Finished", true).await; + + Ok(Response::Bip85(pb::Bip85Response {})) +} diff --git a/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs b/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs index 1df3436b3..d999dada9 100644 --- a/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs +++ b/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs @@ -1512,6 +1512,12 @@ pub struct ElectrumEncryptionKeyResponse { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct Bip85Request {} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Bip85Response {} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct ShowMnemonicRequest {} #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -1611,7 +1617,7 @@ pub struct Success {} pub struct Request { #[prost( oneof = "request::Request", - tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 24, 25, 26, 27" + tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 24, 25, 26, 27, 28" )] pub request: ::core::option::Option, } @@ -1672,6 +1678,8 @@ pub mod request { ElectrumEncryptionKey(super::ElectrumEncryptionKeyRequest), #[prost(message, tag = "27")] Cardano(super::CardanoRequest), + #[prost(message, tag = "28")] + Bip85(super::Bip85Request), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -1679,7 +1687,7 @@ pub mod request { pub struct Response { #[prost( oneof = "response::Response", - tags = "1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15" + tags = "1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16" )] pub response: ::core::option::Option, } @@ -1717,5 +1725,7 @@ pub mod response { ElectrumEncryptionKey(super::ElectrumEncryptionKeyResponse), #[prost(message, tag = "15")] Cardano(super::CardanoResponse), + #[prost(message, tag = "16")] + Bip85(super::Bip85Response), } } diff --git a/src/rust/bitbox02/src/keystore.rs b/src/rust/bitbox02/src/keystore.rs index 588cc17bb..3963a241b 100644 --- a/src/rust/bitbox02/src/keystore.rs +++ b/src/rust/bitbox02/src/keystore.rs @@ -307,6 +307,20 @@ pub fn get_ed25519_seed() -> Result>, ()> { } } +pub fn bip85_bip39(words: u32, index: u32) -> Result, ()> { + let mut mnemonic = zeroize::Zeroizing::new([0u8; 256]); + match unsafe { + bitbox02_sys::keystore_bip85_bip39(words, index, mnemonic.as_mut_ptr(), mnemonic.len() as _) + } { + false => Err(()), + true => Ok(zeroize::Zeroizing::new( + crate::util::str_from_null_terminated(&mnemonic[..]) + .unwrap() + .into(), + )), + } +} + pub fn secp256k1_schnorr_bip86_sign(keypath: &[u32], msg: &[u8; 32]) -> Result<[u8; 64], ()> { let mut signature = [0u8; 64]; match unsafe { @@ -439,4 +453,50 @@ mod tests { b"\xf8\xcb\x28\x85\x37\x60\x2b\x90\xd1\x29\x75\x4b\xdd\x0e\x4b\xed\xf9\xe2\x92\x3a\x04\xb6\x86\x7e\xdb\xeb\xc7\x93\xa7\x17\x6f\x5d\xca\xc5\xc9\x5d\x5f\xd2\x3a\x8e\x01\x6c\x95\x57\x69\x0e\xad\x1f\x00\x2b\x0f\x35\xd7\x06\xff\x8e\x59\x84\x1c\x09\xe0\xb6\xbb\x23\xf0\xa5\x91\x06\x42\xd0\x77\x98\x17\x40\x2e\x5e\x7a\x75\x54\x95\xe7\x44\xf5\x5c\xf1\x1e\x49\xee\xfd\x22\xa4\x60\xe9\xb2\xf7\x53", ); } + + #[test] + fn test_bip85_bip39() { + lock(); + assert!(bip85_bip39(12, 0).is_err()); + + // Test fixtures generated using: + // `docker build -t bip85 .` + // `podman run --rm bip85 --index 0 --bip39-mnemonic "virtual weapon code laptop defy cricket vicious target wave leopard garden give" bip39 --num-words 12` + // `podman run --rm bip85 --index 1 --bip39-mnemonic "virtual weapon code laptop defy cricket vicious target wave leopard garden give" bip39 --num-words 12` + // `podman run --rm bip85 --index 2147483647 --bip39-mnemonic "virtual weapon code laptop defy cricket vicious target wave leopard garden give" bip39 --num-words 12` + // `podman run --rm bip85 --index 0 --bip39-mnemonic "virtual weapon code laptop defy cricket vicious target wave leopard garden give" bip39 --num-words 18` + // `podman run --rm bip85 --index 0 --bip39-mnemonic "virtual weapon code laptop defy cricket vicious target wave leopard garden give" bip39 --num-words 24` + // in https://github.com/ethankosakovsky/bip85/tree/435a0589746c1036735d0a5081167e08abfa7413. + + mock_unlocked_using_mnemonic( + "virtual weapon code laptop defy cricket vicious target wave leopard garden give", + "", + ); + + assert_eq!( + bip85_bip39(12, 0).unwrap().as_ref() as &str, + "slender whip place siren tissue chaos ankle door only assume tent shallow", + ); + assert_eq!( + bip85_bip39(12, 1).unwrap().as_ref() as &str, + "income soft level reunion height pony crane use unfold win keen satisfy", + ); + assert_eq!( + bip85_bip39(12, util::bip32::HARDENED - 1).unwrap().as_ref() as &str, + "carry build nerve market domain energy mistake script puzzle replace mixture idea", + ); + assert_eq!( + bip85_bip39(18, 0).unwrap().as_ref() as &str, + "enact peasant tragic habit expand jar senior melody coin acid logic upper soccer later earn napkin planet stereo", + ); + assert_eq!( + bip85_bip39(24, 0).unwrap().as_ref() as &str, + "cabbage wink october add anchor mean tray surprise gasp tomorrow garbage habit beyond merge where arrive beef gentle animal office drop panel chest size", + ); + + // Invalid number of words. + assert!(bip85_bip39(10, 0).is_err()); + // Index too high. + assert!(bip85_bip39(12, util::bip32::HARDENED).is_err()); + } } From fa3fed5a179ae64a1b2d4123be7f4d0fe7e36bbe Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Thu, 18 May 2023 10:53:13 +0200 Subject: [PATCH 3/3] bip85: confirm mnemonic words This moves the mnemonic word confirmation code from the api module to the workflow module, so it can be used by both the show_mnemonic API call and the bip85 API call. --- src/rust/bitbox02-rust/src/hww/api/bip85.rs | 2 +- .../src/hww/api/show_mnemonic.rs | 71 +---------------- .../bitbox02-rust/src/workflow/mnemonic.rs | 79 ++++++++++++++++++- 3 files changed, 79 insertions(+), 73 deletions(-) diff --git a/src/rust/bitbox02-rust/src/hww/api/bip85.rs b/src/rust/bitbox02-rust/src/hww/api/bip85.rs index 52a2a586b..89b72a110 100644 --- a/src/rust/bitbox02-rust/src/hww/api/bip85.rs +++ b/src/rust/bitbox02-rust/src/hww/api/bip85.rs @@ -100,7 +100,7 @@ pub async fn process(pb::Bip85Request {}: &pb::Bip85Request) -> Result = mnemonic.split(' ').collect(); - mnemonic::show_mnemonic(&words).await?; + mnemonic::show_and_confirm_mnemonic(&words).await?; status::status("Finished", true).await; diff --git a/src/rust/bitbox02-rust/src/hww/api/show_mnemonic.rs b/src/rust/bitbox02-rust/src/hww/api/show_mnemonic.rs index 6c726073a..8abee6360 100644 --- a/src/rust/bitbox02-rust/src/hww/api/show_mnemonic.rs +++ b/src/rust/bitbox02-rust/src/hww/api/show_mnemonic.rs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use alloc::string::String; use alloc::vec::Vec; use super::Error; @@ -24,48 +23,6 @@ use pb::response::Response; use crate::workflow::{mnemonic, status, unlock}; use bitbox02::keystore; -const NUM_RANDOM_WORDS: u8 = 5; - -/// Return 5 words from the BIP39 wordlist, 4 of which are random, and -/// one of them is provided `word`. Returns the position of `word` in -/// the list of words, and the lis of words. This is used to test if -/// the user wrote down the seed words properly. -fn create_random_unique_words(word: &str, length: u8) -> (u8, Vec>) { - fn rand16() -> u16 { - let mut rand = [0u8; 32]; - bitbox02::random::mcu_32_bytes(&mut rand); - ((rand[0] as u16) << 8) | (rand[1] as u16) - } - - let index_word = (rand16() as u8) % length; - let mut picked_indices = Vec::new(); - let result = (0..length) - .map(|i| { - // The correct word at the right index. - if i == index_word { - return zeroize::Zeroizing::new(word.into()); - } - - // A random word everywhere else. - // Loop until we get a unique word, we don't want repeated words in the list. - loop { - let idx = rand16() % keystore::BIP39_WORDLIST_LEN; - if picked_indices.contains(&idx) { - continue; - }; - let random_word = keystore::get_bip39_word(idx).unwrap(); - if random_word.as_str() == word { - continue; - } - picked_indices.push(idx); - return random_word; - } - }) - .collect(); - - (index_word, result) -} - /// Handle the ShowMnemonic API call. This shows the seed encoded as /// 12/18/24 BIP39 English words. Afterwards, for each word, the user /// is asked to pick the right word among 5 words, to check if they @@ -95,33 +52,7 @@ pub async fn process() -> Result { let words: Vec<&str> = mnemonic_sentence.split(' ').collect(); - // Part 1) Scroll through words - mnemonic::show_mnemonic(&words).await?; - - confirm::confirm(&confirm::Params { - title: "", - body: "Please confirm\neach word", - accept_only: true, - accept_is_nextarrow: true, - ..Default::default() - }) - .await?; - - // Part 2) Confirm words - for (word_idx, word) in words.iter().enumerate() { - let title = format!("{:02}", word_idx + 1); - let (correct_idx, choices) = create_random_unique_words(word, NUM_RANDOM_WORDS); - let mut choices: Vec<&str> = choices.iter().map(|c| c.as_ref()).collect(); - choices.push("Back to\nrecovery words"); - let back_idx = (choices.len() - 1) as u8; - loop { - match mnemonic::confirm_word(&choices, &title).await? { - selected_idx if selected_idx == correct_idx => break, - selected_idx if selected_idx == back_idx => mnemonic::show_mnemonic(&words).await?, - _ => status::status("Incorrect word\nTry again", false).await, - } - } - } + mnemonic::show_and_confirm_mnemonic(&words).await?; bitbox02::memory::set_initialized().or(Err(Error::Memory))?; diff --git a/src/rust/bitbox02-rust/src/workflow/mnemonic.rs b/src/rust/bitbox02-rust/src/workflow/mnemonic.rs index d9cf183ca..075b28384 100644 --- a/src/rust/bitbox02-rust/src/workflow/mnemonic.rs +++ b/src/rust/bitbox02-rust/src/workflow/mnemonic.rs @@ -27,12 +27,54 @@ use core::cell::RefCell; use sha2::{Digest, Sha256}; +const NUM_RANDOM_WORDS: u8 = 5; + fn as_str_vec(v: &[zeroize::Zeroizing]) -> Vec<&str> { v.iter().map(|s| s.as_str()).collect() } +/// Return 5 words from the BIP39 wordlist, 4 of which are random, and +/// one of them is provided `word`. Returns the position of `word` in +/// the list of words, and the lis of words. This is used to test if +/// the user wrote down the seed words properly. +fn create_random_unique_words(word: &str, length: u8) -> (u8, Vec>) { + fn rand16() -> u16 { + let mut rand = [0u8; 32]; + bitbox02::random::mcu_32_bytes(&mut rand); + ((rand[0] as u16) << 8) | (rand[1] as u16) + } + + let index_word = (rand16() as u8) % length; + let mut picked_indices = Vec::new(); + let result = (0..length) + .map(|i| { + // The correct word at the right index. + if i == index_word { + return zeroize::Zeroizing::new(word.into()); + } + + // A random word everywhere else. + // Loop until we get a unique word, we don't want repeated words in the list. + loop { + let idx = rand16() % bitbox02::keystore::BIP39_WORDLIST_LEN; + if picked_indices.contains(&idx) { + continue; + }; + let random_word = bitbox02::keystore::get_bip39_word(idx).unwrap(); + if random_word.as_str() == word { + continue; + } + picked_indices.push(idx); + return random_word; + } + }) + .collect(); + + (index_word, result) +} + /// Displays all mnemonic words in a scroll-through screen. -pub async fn show_mnemonic(words: &[&str]) -> Result<(), CancelError> { +async fn show_mnemonic(words: &[&str]) -> Result<(), CancelError> { let result = RefCell::new(None); let mut component = bitbox02::ui::menu_create(bitbox02::ui::MenuParams { words, @@ -49,7 +91,7 @@ pub async fn show_mnemonic(words: &[&str]) -> Result<(), CancelError> { } /// Displays the `choices` to the user, returning the index of the selected choice. -pub async fn confirm_word(choices: &[&str], title: &str) -> Result { +async fn confirm_word(choices: &[&str], title: &str) -> Result { let result = RefCell::new(None); let mut component = bitbox02::ui::menu_create(bitbox02::ui::MenuParams { words: choices, @@ -65,6 +107,39 @@ pub async fn confirm_word(choices: &[&str], title: &str) -> Result Result<(), CancelError> { + // Part 1) Scroll through words + show_mnemonic(words).await?; + + // Can only succeed due to `accept_only`. + let _ = confirm::confirm(&confirm::Params { + title: "", + body: "Please confirm\neach word", + accept_only: true, + accept_is_nextarrow: true, + ..Default::default() + }) + .await; + + // Part 2) Confirm words + for (word_idx, word) in words.iter().enumerate() { + let title = format!("{:02}", word_idx + 1); + let (correct_idx, choices) = create_random_unique_words(word, NUM_RANDOM_WORDS); + let mut choices: Vec<&str> = choices.iter().map(|c| c.as_ref()).collect(); + choices.push("Back to\nrecovery words"); + let back_idx = (choices.len() - 1) as u8; + loop { + match confirm_word(&choices, &title).await? { + selected_idx if selected_idx == correct_idx => break, + selected_idx if selected_idx == back_idx => show_mnemonic(words).await?, + _ => status("Incorrect word\nTry again", false).await, + } + } + } + + Ok(()) +} + /// Given 11/17/23 initial words, this function returns a list of candidate words for the last word, /// such that the resulting bip39 phrase has a valid checksum. There are always exactly 8 such words /// for 24 word mnemonics, 32 words for 18 word mnemonics and 128 words for 12 word mnemonics.