From 8e1cf1f4cf309a362c902a0063b610ca22476ed4 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Sun, 30 Mar 2025 22:46:38 +0200 Subject: [PATCH 1/3] api: centralize state check We had two separate checks to decide if we will allow a protobuf message API call to proceed: in hww.rs (forbid if initialized and locked) and in api.rs in the can_call() function that defines which states the API call can run in. This makes it a bit hard to follow as split into two locations, and it also prevents us from introducing a protobuf call that is only allowed to run if the device is initialized but still locked. `Unlock` would be such a useful new API call if we want to allow entering the passphrase on the host. The hww.rs check passed if `!initialized || !locked`, which means the same as `uninitialized || seeded || unlocked`. We change the `can_call` state checks to include this condition. For this we split the local `Initialized` state into `InitializedAndLocked` and `InitializedAndUnlocked`. For each line in can_call, the two conditions are combined. The equivalent state check in the end is simply to convert all `Initialized` checks to `InitializedAndUnlocked`. The `true` entries become explicit as they can't include `InitializedAndLocked` (e.g. can't change the name using `DeviceName` if initialized but not yet unlocked). --- src/rust/bitbox02-rust/src/hww.rs | 15 +++--- src/rust/bitbox02-rust/src/hww/api.rs | 69 +++++++++++++++++++-------- 2 files changed, 56 insertions(+), 28 deletions(-) diff --git a/src/rust/bitbox02-rust/src/hww.rs b/src/rust/bitbox02-rust/src/hww.rs index 2314d0567..27363e77c 100644 --- a/src/rust/bitbox02-rust/src/hww.rs +++ b/src/rust/bitbox02-rust/src/hww.rs @@ -119,12 +119,6 @@ pub async fn process_packet(usb_in: Vec) -> Vec { _ => (), } - // No other message than the attestation and unlock calls shall pass until the device is - // unlocked or ready to be initialized. - if bitbox02::memory::is_initialized() && bitbox02::keystore::is_locked() { - return Vec::new(); - } - let mut out = [OP_STATUS_SUCCESS].to_vec(); match noise::process(usb_in, &mut out).await { Ok(()) => out, @@ -417,7 +411,14 @@ mod tests { // Can't reboot when initialized but locked. bitbox02::keystore::lock(); - assert!(make_request(reboot_request.encode_to_vec().as_ref()).is_err()); + let response_encoded = make_request(&reboot_request.encode_to_vec()).unwrap(); + let response = crate::pb::Response::decode(&response_encoded[..]).unwrap(); + assert_eq!( + response, + crate::pb::Response { + response: Some(api::error::make_error(api::error::Error::InvalidState)) + }, + ); // Unlock. assert_eq!( diff --git a/src/rust/bitbox02-rust/src/hww/api.rs b/src/rust/bitbox02-rust/src/hww/api.rs index ba528f22a..62d99bd26 100644 --- a/src/rust/bitbox02-rust/src/hww/api.rs +++ b/src/rust/bitbox02-rust/src/hww/api.rs @@ -94,11 +94,17 @@ fn can_call(request: &Request) -> bool { Uninitialized, // Seeded (password defined, seed created/loaded). Seeded, - // Initialized (seed backuped up on SD card). - Initialized, + // InitializedAndLocked (seed backuped up on SD card, keystore locked). + InitializedAndLocked, + // InitializedAndUnlocked (seed backuped up on SD card, keystore unlocked). + InitializedAndUnlocked, } let state: State = if bitbox02::memory::is_initialized() { - State::Initialized + if bitbox02::keystore::is_locked() { + State::InitializedAndLocked + } else { + State::InitializedAndUnlocked + } } else if bitbox02::memory::is_seeded() { State::Seeded } else { @@ -108,33 +114,54 @@ fn can_call(request: &Request) -> bool { match request { // Deprecated call, last used in v1.0.0. Request::PerformAttestation(_) => false, - Request::DeviceInfo(_) => true, - Request::Reboot(_) => true, - Request::DeviceName(_) => true, - Request::DeviceLanguage(_) => true, - Request::CheckSdcard(_) => true, - Request::InsertRemoveSdcard(_) => true, - Request::ListBackups(_) => true, + Request::DeviceInfo(_) => matches!( + state, + State::Uninitialized | State::Seeded | State::InitializedAndUnlocked + ), + Request::Reboot(_) => matches!( + state, + State::Uninitialized | State::Seeded | State::InitializedAndUnlocked + ), + Request::DeviceName(_) => matches!( + state, + State::Uninitialized | State::Seeded | State::InitializedAndUnlocked + ), + Request::DeviceLanguage(_) => matches!( + state, + State::Uninitialized | State::Seeded | State::InitializedAndUnlocked + ), + Request::CheckSdcard(_) => matches!( + state, + State::Uninitialized | State::Seeded | State::InitializedAndUnlocked + ), + Request::InsertRemoveSdcard(_) => matches!( + state, + State::Uninitialized | State::Seeded | State::InitializedAndUnlocked + ), + Request::ListBackups(_) => matches!( + state, + State::Uninitialized | State::Seeded | State::InitializedAndUnlocked + ), Request::SetPassword(_) => matches!(state, State::Uninitialized | State::Seeded), Request::RestoreBackup(_) => matches!(state, State::Uninitialized | State::Seeded), Request::RestoreFromMnemonic(_) => matches!(state, State::Uninitialized | State::Seeded), - Request::CreateBackup(_) => matches!(state, State::Seeded | State::Initialized), - Request::ShowMnemonic(_) => matches!(state, State::Seeded | State::Initialized), - Request::Fingerprint(_) => matches!(state, State::Initialized), - Request::ElectrumEncryptionKey(_) => matches!(state, State::Initialized), + Request::CreateBackup(_) => matches!(state, State::Seeded | State::InitializedAndUnlocked), + Request::ShowMnemonic(_) => matches!(state, State::Seeded | State::InitializedAndUnlocked), + Request::Fingerprint(_) => matches!(state, State::InitializedAndUnlocked), + Request::ElectrumEncryptionKey(_) => matches!(state, State::InitializedAndUnlocked), Request::BtcPub(_) | Request::Btc(_) | Request::BtcSignInit(_) => { - matches!(state, State::Initialized) + matches!(state, State::InitializedAndUnlocked) } // These are streamed asynchronously using the `next_request()` primitive in // bitcoin/signtx.rs and are not handled directly. Request::BtcSignInput(_) | Request::BtcSignOutput(_) => false, - Request::CheckBackup(_) => matches!(state, State::Initialized), - Request::SetMnemonicPassphraseEnabled(_) => matches!(state, State::Initialized), - Request::Eth(_) => matches!(state, State::Initialized), - Request::Reset(_) => matches!(state, State::Initialized), - Request::Cardano(_) => matches!(state, State::Initialized), - Request::Bip85(_) => matches!(state, State::Initialized), + Request::CheckBackup(_) => matches!(state, State::InitializedAndUnlocked), + Request::SetMnemonicPassphraseEnabled(_) => matches!(state, State::InitializedAndUnlocked), + Request::Eth(_) => matches!(state, State::InitializedAndUnlocked), + Request::Reset(_) => matches!(state, State::InitializedAndUnlocked), + Request::Cardano(_) => matches!(state, State::InitializedAndUnlocked), + Request::Bip85(_) => matches!(state, State::InitializedAndUnlocked), } } From b51b95cc0ee563261be91a307e6e7485fe1a0d24 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Mon, 31 Mar 2025 14:50:59 +0200 Subject: [PATCH 2/3] trinary_choice: make labels optional So one can make a binary choice by leaving the middle one empty. --- src/rust/bitbox02-rust/src/hww/api/bip85.rs | 2 +- .../bitbox02-rust/src/workflow/mnemonic.rs | 3 +- .../src/workflow/trinary_choice.rs | 6 +-- src/rust/bitbox02/src/ui/ui.rs | 27 +++++++++--- src/rust/bitbox02/src/ui/ui_stub.rs | 6 +-- .../bitbox02/src/ui/ui_stub_c_unit_tests.rs | 6 +-- src/ui/components/trinary_choice.c | 43 +++++++++++-------- 7 files changed, 57 insertions(+), 36 deletions(-) diff --git a/src/rust/bitbox02-rust/src/hww/api/bip85.rs b/src/rust/bitbox02-rust/src/hww/api/bip85.rs index 995a904ba..6cf5ddda6 100644 --- a/src/rust/bitbox02-rust/src/hww/api/bip85.rs +++ b/src/rust/bitbox02-rust/src/hww/api/bip85.rs @@ -59,7 +59,7 @@ async fn process_bip39() -> Result<(), Error> { }) .await?; - let num_words: u32 = match choose("How many words?", "12", "18", "24").await { + let num_words: u32 = match choose("How many words?", Some("12"), Some("18"), Some("24")).await { TrinaryChoice::TRINARY_CHOICE_LEFT => 12, TrinaryChoice::TRINARY_CHOICE_MIDDLE => 18, TrinaryChoice::TRINARY_CHOICE_RIGHT => 24, diff --git a/src/rust/bitbox02-rust/src/workflow/mnemonic.rs b/src/rust/bitbox02-rust/src/workflow/mnemonic.rs index d4074878c..2b5ad57a1 100644 --- a/src/rust/bitbox02-rust/src/workflow/mnemonic.rs +++ b/src/rust/bitbox02-rust/src/workflow/mnemonic.rs @@ -290,7 +290,8 @@ async fn get_12th_18th_word( /// Retrieve a BIP39 mnemonic sentence of 12, 18 or 24 words from the user. pub async fn get() -> Result, CancelError> { - let num_words: usize = match choose("How many words?", "12", "18", "24").await { + let num_words: usize = match choose("How many words?", Some("12"), Some("18"), Some("24")).await + { TrinaryChoice::TRINARY_CHOICE_LEFT => 12, TrinaryChoice::TRINARY_CHOICE_MIDDLE => 18, TrinaryChoice::TRINARY_CHOICE_RIGHT => 24, diff --git a/src/rust/bitbox02-rust/src/workflow/trinary_choice.rs b/src/rust/bitbox02-rust/src/workflow/trinary_choice.rs index 9e823cff1..26cc9e0ca 100644 --- a/src/rust/bitbox02-rust/src/workflow/trinary_choice.rs +++ b/src/rust/bitbox02-rust/src/workflow/trinary_choice.rs @@ -22,9 +22,9 @@ pub use bitbox02::ui::TrinaryChoice; pub async fn choose( message: &str, - label_left: &str, - label_middle: &str, - label_right: &str, + label_left: Option<&str>, + label_middle: Option<&str>, + label_right: Option<&str>, ) -> TrinaryChoice { let result = RefCell::new(None as Option); diff --git a/src/rust/bitbox02/src/ui/ui.rs b/src/rust/bitbox02/src/ui/ui.rs index 79e93d3e8..9301fba29 100644 --- a/src/rust/bitbox02/src/ui/ui.rs +++ b/src/rust/bitbox02/src/ui/ui.rs @@ -318,9 +318,9 @@ pub fn menu_create(params: MenuParams<'_>) -> Component<'_> { pub fn trinary_choice_create<'a>( message: &'a str, - label_left: &'a str, - label_middle: &'a str, - label_right: &'a str, + label_left: Option<&'a str>, + label_middle: Option<&'a str>, + label_right: Option<&'a str>, chosen_callback: TrinaryChoiceCb, ) -> Component<'a> { unsafe extern "C" fn c_chosen_cb(choice: TrinaryChoice, param: *mut c_void) { @@ -329,12 +329,25 @@ pub fn trinary_choice_create<'a>( } let chosen_cb_param = Box::into_raw(Box::new(chosen_callback)) as *mut c_void; + let label_left = label_left.map(|label| crate::util::str_to_cstr_vec(label).unwrap()); + let label_middle = label_middle.map(|label| crate::util::str_to_cstr_vec(label).unwrap()); + let label_right = label_right.map(|label| crate::util::str_to_cstr_vec(label).unwrap()); let component = unsafe { bitbox02_sys::trinary_choice_create( - crate::util::str_to_cstr_vec(message).unwrap().as_ptr(), // copied in C - crate::util::str_to_cstr_vec(label_left).unwrap().as_ptr(), // copied in C - crate::util::str_to_cstr_vec(label_middle).unwrap().as_ptr(), // copied in C - crate::util::str_to_cstr_vec(label_right).unwrap().as_ptr(), // copied in C + // copied in C + crate::util::str_to_cstr_vec(message).unwrap().as_ptr(), + // copied in C + label_left + .as_ref() + .map_or_else(|| core::ptr::null(), |label| label.as_ptr()), + // copied in C + label_middle + .as_ref() + .map_or_else(|| core::ptr::null(), |label| label.as_ptr()), + // copied in C + label_right + .as_ref() + .map_or_else(|| core::ptr::null(), |label| label.as_ptr()), Some(c_chosen_cb as _), chosen_cb_param, core::ptr::null_mut(), // parent component, there is no parent. diff --git a/src/rust/bitbox02/src/ui/ui_stub.rs b/src/rust/bitbox02/src/ui/ui_stub.rs index bca04d282..226c2260e 100644 --- a/src/rust/bitbox02/src/ui/ui_stub.rs +++ b/src/rust/bitbox02/src/ui/ui_stub.rs @@ -107,9 +107,9 @@ pub fn menu_create(_params: MenuParams<'_>) -> Component<'_> { pub fn trinary_choice_create<'a>( _message: &'a str, - _label_left: &'a str, - _label_middle: &'a str, - _label_right: &'a str, + _label_left: Option<&'a str>, + _label_middle: Option<&'a str>, + _label_right: Option<&'a str>, _chosen_callback: TrinaryChoiceCb, ) -> Component<'a> { panic!("not implemented") diff --git a/src/rust/bitbox02/src/ui/ui_stub_c_unit_tests.rs b/src/rust/bitbox02/src/ui/ui_stub_c_unit_tests.rs index 046bf829f..acc76c5c1 100644 --- a/src/rust/bitbox02/src/ui/ui_stub_c_unit_tests.rs +++ b/src/rust/bitbox02/src/ui/ui_stub_c_unit_tests.rs @@ -117,9 +117,9 @@ pub fn menu_create(_params: MenuParams<'_>) -> Component<'_> { pub fn trinary_choice_create<'a>( _message: &'a str, - _label_left: &'a str, - _label_middle: &'a str, - _label_right: &'a str, + _label_left: Option<&'a str>, + _label_middle: Option<&'a str>, + _label_right: Option<&'a str>, _chosen_callback: TrinaryChoiceCb, ) -> Component<'a> { panic!("not implemented") diff --git a/src/ui/components/trinary_choice.c b/src/ui/components/trinary_choice.c index c13e4373e..2e56ee428 100644 --- a/src/ui/components/trinary_choice.c +++ b/src/ui/components/trinary_choice.c @@ -95,24 +95,31 @@ component_t* trinary_choice_create( ui_util_add_sub_component(component, label_create(message, NULL, CENTER, component)); } - data->button_left = button_create(label_left, bottom_slider, 0, _left_selected, component); - ui_util_add_sub_component(component, data->button_left); - - data->button_middle = - button_create(label_middle, bottom_slider, 0, _middle_selected, component); - ui_util_add_sub_component(component, data->button_middle); - - data->button_right = button_create(label_right, bottom_slider, 0, _right_selected, component); - ui_util_add_sub_component(component, data->button_right); - - ui_util_position_left_bottom_offset(component, data->button_left, 0, 0); - ui_util_position_left_bottom_offset( - component, - data->button_middle, - SCREEN_WIDTH / 2 - data->button_middle->dimension.width / 2, - 0); - ui_util_position_left_bottom_offset( - component, data->button_right, SCREEN_WIDTH - data->button_right->dimension.width, 0); + if (label_left != NULL) { + data->button_left = button_create(label_left, bottom_slider, 0, _left_selected, component); + ui_util_add_sub_component(component, data->button_left); + ui_util_position_left_bottom_offset(component, data->button_left, 0, 0); + } + + if (label_middle != NULL) { + data->button_middle = + button_create(label_middle, bottom_slider, 0, _middle_selected, component); + ui_util_add_sub_component(component, data->button_middle); + ui_util_position_left_bottom_offset( + component, + data->button_middle, + SCREEN_WIDTH / 2 - data->button_middle->dimension.width / 2, + 0); + } + + if (label_right != NULL) { + data->button_right = + button_create(label_right, bottom_slider, 0, _right_selected, component); + ui_util_add_sub_component(component, data->button_right); + + ui_util_position_left_bottom_offset( + component, data->button_right, SCREEN_WIDTH - data->button_right->dimension.width, 0); + } return component; } From 30e47f107b3b7029b417c8f32411e10d0136363d Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Fri, 28 Mar 2025 11:11:20 +0100 Subject: [PATCH 3/3] api: add Unlock call w/ support for entering passphrase on the host Previously, the host would unlock before the noise channel was established using `OP_UNLOCK`, using a raw unencrypted call that is using protobufs. To support entering a passphrase on the host, we add a `UnlockRequest` protobuf message. This needs to happen after the noise channel was established because: - we want the passphrase to be encrypted in flight - we want to make use of protobuf and `next_request()` to query the host `UnlockRequest` has a flag `supports_host_passphrase`, so wallets (especially third party apps) that do not support this yet still work seamlessly, as without support the passphrase is simply entered on the device. If the host app supports it, then the device will ask the user if they want to enter on the device or on the host. The query to the host uses `UnlockRequestHostInfoResponse`. It contains the `type` enum for future compatbilitiy in case we want to add more unlock options in the future (e.g. fingerprints). The `workflow::unlock` calls now get a callback to pick the method of entering the passphrase, so the old `OP_UNLOCK` can use the previous way, while `UnlockRequest` provides a callback to ask the user where to enter and proceeds accordingly. --- messages/hww.proto | 3 + messages/keystore.proto | 21 ++++ .../communication/bitbox_api_protocol.py | 61 +++++++++++- .../communication/generated/hww_pb2.py | 8 +- .../communication/generated/hww_pb2.pyi | 24 +++-- .../communication/generated/keystore_pb2.py | 10 +- .../communication/generated/keystore_pb2.pyi | 49 ++++++++++ py/send_message.py | 23 ++++- src/rust/bitbox02-rust-c/src/workflow.rs | 4 +- src/rust/bitbox02-rust/src/hww.rs | 9 +- src/rust/bitbox02-rust/src/hww/api.rs | 8 ++ .../bitbox02-rust/src/hww/api/keystore.rs | 95 +++++++++++++++++++ src/rust/bitbox02-rust/src/hww/api/restore.rs | 4 +- .../bitbox02-rust/src/hww/api/set_password.rs | 2 +- .../bitbox02-rust/src/shiftcrypto.bitbox02.rs | 72 +++++++++++++- src/rust/bitbox02-rust/src/workflow/unlock.rs | 66 ++++++++----- src/rust/bitbox02/src/ui/types.rs | 4 + src/rust/bitbox02/src/ui/ui.rs | 2 +- src/rust/bitbox02/src/ui/ui_stub.rs | 2 +- .../bitbox02/src/ui/ui_stub_c_unit_tests.rs | 2 +- 20 files changed, 421 insertions(+), 48 deletions(-) create mode 100644 src/rust/bitbox02-rust/src/hww/api/keystore.rs diff --git a/messages/hww.proto b/messages/hww.proto index 54c34f6d4..3488b2243 100644 --- a/messages/hww.proto +++ b/messages/hww.proto @@ -67,6 +67,8 @@ message Request { ElectrumEncryptionKeyRequest electrum_encryption_key = 26; CardanoRequest cardano = 27; BIP85Request bip85 = 28; + UnlockRequest unlock = 29; + UnlockHostInfoRequest unlock_host_info = 30; } } @@ -89,5 +91,6 @@ message Response { ElectrumEncryptionKeyResponse electrum_encryption_key = 14; CardanoResponse cardano = 15; BIP85Response bip85 = 16; + UnlockRequestHostInfoResponse unlock_host_info = 17; } } diff --git a/messages/keystore.proto b/messages/keystore.proto index d6d564121..b8c7d68b5 100644 --- a/messages/keystore.proto +++ b/messages/keystore.proto @@ -31,3 +31,24 @@ message BIP85Response { bytes ln = 2; } } + +message UnlockRequest { + // If true, the device will be allowed to ask the user to enter the passphrase on the host. If + // the user accepts, the host will receive a `UnlockRequestHostInfoResponse` with type + // `PASSPHRASE`. + bool supports_host_passphrase = 1; +} + +message UnlockRequestHostInfoResponse { + enum InfoType { + UNKNOWN = 0; + // Respond with `UnlockHostInfoRequest` containing the passphrase. + PASSPHRASE = 1; + } + InfoType type = 1; +} + +message UnlockHostInfoRequest { + // Omit if type==PASSPHRASE and entering the passhrase is cancelled on the host. + optional string passphrase = 1; +} diff --git a/py/bitbox02/bitbox02/communication/bitbox_api_protocol.py b/py/bitbox02/bitbox02/communication/bitbox_api_protocol.py index e03e10145..d21830f53 100644 --- a/py/bitbox02/bitbox02/communication/bitbox_api_protocol.py +++ b/py/bitbox02/bitbox02/communication/bitbox_api_protocol.py @@ -14,6 +14,7 @@ """BitBox02""" from abc import ABC, abstractmethod +from dataclasses import dataclass import os import enum import sys @@ -36,6 +37,7 @@ try: from .generated import hww_pb2 as hww from .generated import system_pb2 as system + from .generated import keystore_pb2 as keystore except ModuleNotFoundError: print("Run `make py` to generate the protobuf messages") sys.exit() @@ -265,6 +267,15 @@ def set_app_static_privkey(self, privkey: bytes) -> None: pass +@dataclass +class BitBoxConfig: + """Configuration options""" + + # If defined, this is called to enter the mnemonic passphrase on the host. + # It should return the passphrase, or None if the operation was cancelled. + enter_mnemonic_passphrase: Optional[Callable[[], Optional[str]]] = None + + class BitBoxProtocol(ABC): """ Class for executing versioned BitBox operations @@ -525,12 +536,13 @@ def cancel_outstanding_request(self) -> None: class BitBoxCommonAPI: """Class to communicate with a BitBox device""" - # pylint: disable=too-many-public-methods,too-many-arguments + # pylint: disable=too-many-public-methods,too-many-arguments,too-many-branches def __init__( self, transport: TransportLayer, device_info: Optional[DeviceInfo], noise_config: BitBoxNoiseConfig, + config: BitBoxConfig = BitBoxConfig(), ): """ Can raise LibraryVersionOutdatedException. check_min_version() should be called following @@ -577,9 +589,13 @@ def __init__( if self.version >= semver.VersionInfo(2, 0, 0): noise_config.attestation_check(self._perform_attestation()) - self._bitbox_protocol.unlock_query() + # Starting with v9.23, we can use the unlock function below after noise pairing. + if self.version < semver.VersionInfo(9, 23, 0): + self._bitbox_protocol.unlock_query() self._bitbox_protocol.noise_connect(noise_config) + if self.version >= semver.VersionInfo(9, 23, 0): + self.unlock(config) # pylint: disable=too-many-return-statements def _perform_attestation(self) -> bool: @@ -658,6 +674,47 @@ def _msg_query( print(response) return response + def unlock( + self, + config: BitBoxConfig, + ) -> None: + """ + Prompt to unlock the device. If already unlocked, nothing happens. If + `config.enter_mnemonic_passphrase` is defined and the user chooses to enter the passphrase + on the host, this callback will be called to retrieve the passphrase. + """ + # pylint: disable=no-member + try: + request = hww.Request() + request.unlock.CopyFrom( + keystore.UnlockRequest( + supports_host_passphrase=config.enter_mnemonic_passphrase is not None, + ) + ) + + while True: + response = self._msg_query(request) + response_type = response.WhichOneof("response") + + if ( + response_type == "unlock_host_info" + and response.unlock_host_info.type + == keystore.UnlockRequestHostInfoResponse.InfoType.PASSPHRASE + ): + assert config.enter_mnemonic_passphrase is not None + request = hww.Request() + request.unlock_host_info.CopyFrom( + keystore.UnlockHostInfoRequest( + passphrase=config.enter_mnemonic_passphrase(), + ) + ) + elif response_type == "success": + break + else: + raise Exception("Unexpected response") + except OSError: + pass + def reboot( self, purpose: "system.RebootRequest.Purpose.V" = system.RebootRequest.Purpose.UPGRADE ) -> bool: diff --git a/py/bitbox02/bitbox02/communication/generated/hww_pb2.py b/py/bitbox02/bitbox02/communication/generated/hww_pb2.py index 4606d1d94..8576bf6e8 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\"\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') +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\x0e\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\x12\x35\n\x06unlock\x18\x1d \x01(\x0b\x32#.shiftcrypto.bitbox02.UnlockRequestH\x00\x12G\n\x10unlock_host_info\x18\x1e \x01(\x0b\x32+.shiftcrypto.bitbox02.UnlockHostInfoRequestH\x00\x42\t\n\x07requestJ\x04\x08\x01\x10\x02J\x04\x08\x17\x10\x18\"\x90\x08\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\x12O\n\x10unlock_host_info\x18\x11 \x01(\x0b\x32\x33.shiftcrypto.bitbox02.UnlockRequestHostInfoResponseH\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=2046 - _RESPONSE._serialized_start=2049 - _RESPONSE._serialized_end=3008 + _REQUEST._serialized_end=2174 + _RESPONSE._serialized_start=2177 + _RESPONSE._serialized_end=3217 # @@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 df9aeacf1..3b4e6cf53 100644 --- a/py/bitbox02/bitbox02/communication/generated/hww_pb2.pyi +++ b/py/bitbox02/bitbox02/communication/generated/hww_pb2.pyi @@ -68,6 +68,8 @@ class Request(google.protobuf.message.Message): ELECTRUM_ENCRYPTION_KEY_FIELD_NUMBER: builtins.int CARDANO_FIELD_NUMBER: builtins.int BIP85_FIELD_NUMBER: builtins.int + UNLOCK_FIELD_NUMBER: builtins.int + UNLOCK_HOST_INFO_FIELD_NUMBER: builtins.int @property def device_name(self) -> bitbox02_system_pb2.SetDeviceNameRequest: """removed: RandomNumberRequest random_number = 1;""" @@ -124,6 +126,10 @@ class Request(google.protobuf.message.Message): def cardano(self) -> cardano_pb2.CardanoRequest: ... @property def bip85(self) -> keystore_pb2.BIP85Request: ... + @property + def unlock(self) -> keystore_pb2.UnlockRequest: ... + @property + def unlock_host_info(self) -> keystore_pb2.UnlockHostInfoRequest: ... def __init__(self, *, device_name: typing.Optional[bitbox02_system_pb2.SetDeviceNameRequest] = ..., @@ -152,10 +158,12 @@ class Request(google.protobuf.message.Message): electrum_encryption_key: typing.Optional[keystore_pb2.ElectrumEncryptionKeyRequest] = ..., cardano: typing.Optional[cardano_pb2.CardanoRequest] = ..., bip85: typing.Optional[keystore_pb2.BIP85Request] = ..., + unlock: typing.Optional[keystore_pb2.UnlockRequest] = ..., + unlock_host_info: typing.Optional[keystore_pb2.UnlockHostInfoRequest] = ..., ) -> None: ... - 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"]]: ... + 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","unlock",b"unlock","unlock_host_info",b"unlock_host_info"]) -> 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","unlock",b"unlock","unlock_host_info",b"unlock_host_info"]) -> 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","unlock","unlock_host_info"]]: ... global___Request = Request class Response(google.protobuf.message.Message): @@ -175,6 +183,7 @@ class Response(google.protobuf.message.Message): ELECTRUM_ENCRYPTION_KEY_FIELD_NUMBER: builtins.int CARDANO_FIELD_NUMBER: builtins.int BIP85_FIELD_NUMBER: builtins.int + UNLOCK_HOST_INFO_FIELD_NUMBER: builtins.int @property def success(self) -> global___Success: ... @property @@ -207,6 +216,8 @@ class Response(google.protobuf.message.Message): def cardano(self) -> cardano_pb2.CardanoResponse: ... @property def bip85(self) -> keystore_pb2.BIP85Response: ... + @property + def unlock_host_info(self) -> keystore_pb2.UnlockRequestHostInfoResponse: ... def __init__(self, *, success: typing.Optional[global___Success] = ..., @@ -224,8 +235,9 @@ class Response(google.protobuf.message.Message): electrum_encryption_key: typing.Optional[keystore_pb2.ElectrumEncryptionKeyResponse] = ..., cardano: typing.Optional[cardano_pb2.CardanoResponse] = ..., bip85: typing.Optional[keystore_pb2.BIP85Response] = ..., + unlock_host_info: typing.Optional[keystore_pb2.UnlockRequestHostInfoResponse] = ..., ) -> None: ... - 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"]]: ... + 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","unlock_host_info",b"unlock_host_info"]) -> 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","unlock_host_info",b"unlock_host_info"]) -> 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","unlock_host_info"]]: ... global___Response = Response diff --git a/py/bitbox02/bitbox02/communication/generated/keystore_pb2.py b/py/bitbox02/bitbox02/communication/generated/keystore_pb2.py index ba14a68d7..481160c4d 100644 --- a/py/bitbox02/bitbox02/communication/generated/keystore_pb2.py +++ b/py/bitbox02/bitbox02/communication/generated/keystore_pb2.py @@ -14,7 +14,7 @@ from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0ekeystore.proto\x12\x14shiftcrypto.bitbox02\x1a\x1bgoogle/protobuf/empty.proto\"/\n\x1c\x45lectrumEncryptionKeyRequest\x12\x0f\n\x07keypath\x18\x01 \x03(\r\",\n\x1d\x45lectrumEncryptionKeyResponse\x12\x0b\n\x03key\x18\x01 \x01(\t\"\x97\x01\n\x0c\x42IP85Request\x12\'\n\x05\x62ip39\x18\x01 \x01(\x0b\x32\x16.google.protobuf.EmptyH\x00\x12\x36\n\x02ln\x18\x02 \x01(\x0b\x32(.shiftcrypto.bitbox02.BIP85Request.AppLnH\x00\x1a\x1f\n\x05\x41ppLn\x12\x16\n\x0e\x61\x63\x63ount_number\x18\x01 \x01(\rB\x05\n\x03\x61pp\"M\n\rBIP85Response\x12\'\n\x05\x62ip39\x18\x01 \x01(\x0b\x32\x16.google.protobuf.EmptyH\x00\x12\x0c\n\x02ln\x18\x02 \x01(\x0cH\x00\x42\x05\n\x03\x61ppb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0ekeystore.proto\x12\x14shiftcrypto.bitbox02\x1a\x1bgoogle/protobuf/empty.proto\"/\n\x1c\x45lectrumEncryptionKeyRequest\x12\x0f\n\x07keypath\x18\x01 \x03(\r\",\n\x1d\x45lectrumEncryptionKeyResponse\x12\x0b\n\x03key\x18\x01 \x01(\t\"\x97\x01\n\x0c\x42IP85Request\x12\'\n\x05\x62ip39\x18\x01 \x01(\x0b\x32\x16.google.protobuf.EmptyH\x00\x12\x36\n\x02ln\x18\x02 \x01(\x0b\x32(.shiftcrypto.bitbox02.BIP85Request.AppLnH\x00\x1a\x1f\n\x05\x41ppLn\x12\x16\n\x0e\x61\x63\x63ount_number\x18\x01 \x01(\rB\x05\n\x03\x61pp\"M\n\rBIP85Response\x12\'\n\x05\x62ip39\x18\x01 \x01(\x0b\x32\x16.google.protobuf.EmptyH\x00\x12\x0c\n\x02ln\x18\x02 \x01(\x0cH\x00\x42\x05\n\x03\x61pp\"1\n\rUnlockRequest\x12 \n\x18supports_host_passphrase\x18\x01 \x01(\x08\"\x94\x01\n\x1dUnlockRequestHostInfoResponse\x12J\n\x04type\x18\x01 \x01(\x0e\x32<.shiftcrypto.bitbox02.UnlockRequestHostInfoResponse.InfoType\"\'\n\x08InfoType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0e\n\nPASSPHRASE\x10\x01\"?\n\x15UnlockHostInfoRequest\x12\x17\n\npassphrase\x18\x01 \x01(\tH\x00\x88\x01\x01\x42\r\n\x0b_passphraseb\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'keystore_pb2', globals()) @@ -31,4 +31,12 @@ _BIP85REQUEST_APPLN._serialized_end=309 _BIP85RESPONSE._serialized_start=318 _BIP85RESPONSE._serialized_end=395 + _UNLOCKREQUEST._serialized_start=397 + _UNLOCKREQUEST._serialized_end=446 + _UNLOCKREQUESTHOSTINFORESPONSE._serialized_start=449 + _UNLOCKREQUESTHOSTINFORESPONSE._serialized_end=597 + _UNLOCKREQUESTHOSTINFORESPONSE_INFOTYPE._serialized_start=558 + _UNLOCKREQUESTHOSTINFORESPONSE_INFOTYPE._serialized_end=597 + _UNLOCKHOSTINFOREQUEST._serialized_start=599 + _UNLOCKHOSTINFOREQUEST._serialized_end=662 # @@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 8159aee00..7bb5843dc 100644 --- a/py/bitbox02/bitbox02/communication/generated/keystore_pb2.pyi +++ b/py/bitbox02/bitbox02/communication/generated/keystore_pb2.pyi @@ -6,6 +6,7 @@ import builtins import google.protobuf.descriptor import google.protobuf.empty_pb2 import google.protobuf.internal.containers +import google.protobuf.internal.enum_type_wrapper import google.protobuf.message import typing import typing_extensions @@ -79,3 +80,51 @@ class BIP85Response(google.protobuf.message.Message): def ClearField(self, field_name: typing_extensions.Literal["app",b"app","bip39",b"bip39","ln",b"ln"]) -> None: ... def WhichOneof(self, oneof_group: typing_extensions.Literal["app",b"app"]) -> typing.Optional[typing_extensions.Literal["bip39","ln"]]: ... global___BIP85Response = BIP85Response + +class UnlockRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + SUPPORTS_HOST_PASSPHRASE_FIELD_NUMBER: builtins.int + supports_host_passphrase: builtins.bool + def __init__(self, + *, + supports_host_passphrase: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["supports_host_passphrase",b"supports_host_passphrase"]) -> None: ... +global___UnlockRequest = UnlockRequest + +class UnlockRequestHostInfoResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + class _InfoType: + ValueType = typing.NewType('ValueType', builtins.int) + V: typing_extensions.TypeAlias = ValueType + class _InfoTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[UnlockRequestHostInfoResponse._InfoType.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + UNKNOWN: UnlockRequestHostInfoResponse._InfoType.ValueType # 0 + PASSPHRASE: UnlockRequestHostInfoResponse._InfoType.ValueType # 1 + class InfoType(_InfoType, metaclass=_InfoTypeEnumTypeWrapper): + pass + + UNKNOWN: UnlockRequestHostInfoResponse.InfoType.ValueType # 0 + PASSPHRASE: UnlockRequestHostInfoResponse.InfoType.ValueType # 1 + + TYPE_FIELD_NUMBER: builtins.int + type: global___UnlockRequestHostInfoResponse.InfoType.ValueType + def __init__(self, + *, + type: global___UnlockRequestHostInfoResponse.InfoType.ValueType = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["type",b"type"]) -> None: ... +global___UnlockRequestHostInfoResponse = UnlockRequestHostInfoResponse + +class UnlockHostInfoRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + PASSPHRASE_FIELD_NUMBER: builtins.int + passphrase: typing.Text + def __init__(self, + *, + passphrase: typing.Optional[typing.Text] = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["_passphrase",b"_passphrase","passphrase",b"passphrase"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["_passphrase",b"_passphrase","passphrase",b"passphrase"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["_passphrase",b"_passphrase"]) -> typing.Optional[typing_extensions.Literal["passphrase"]]: ... +global___UnlockHostInfoRequest = UnlockHostInfoRequest diff --git a/py/send_message.py b/py/send_message.py index aa00a14a3..2c23269bb 100755 --- a/py/send_message.py +++ b/py/send_message.py @@ -1682,6 +1682,23 @@ def run(self) -> int: return 0 +def enter_mnemonic_passphrase() -> Optional[str]: + try: + return input("Enter passphrase: ") + except EOFError: + # Aborted via Ctrl-D. + print("") + return None + except: + # We want to abort the operation on the device regardless of what happens. + return None + + +bitbox_config = bitbox_api_protocol.BitBoxConfig( + enter_mnemonic_passphrase=enter_mnemonic_passphrase +) + + def connect_to_simulator_bitbox(debug: bool, port: int) -> int: """ Connects and runs the main menu on host computer, @@ -1721,6 +1738,7 @@ def __del__(self) -> None: transport=u2fhid.U2FHid(simulator), device_info=None, noise_config=noise_config, + config=bitbox_config, ) try: bitbox_connection.check_min_version() @@ -1799,7 +1817,10 @@ def attestation_check(self, result: bool) -> None: hid_device = hid.device() hid_device.open_path(bitbox["path"]) bitbox_connection = bitbox02.BitBox02( - transport=u2fhid.U2FHid(hid_device), device_info=bitbox, noise_config=config + transport=u2fhid.U2FHid(hid_device), + device_info=bitbox, + noise_config=config, + config=bitbox_config, ) try: bitbox_connection.check_min_version() diff --git a/src/rust/bitbox02-rust-c/src/workflow.rs b/src/rust/bitbox02-rust-c/src/workflow.rs index 1dfec6a65..6163f83d5 100644 --- a/src/rust/bitbox02-rust-c/src/workflow.rs +++ b/src/rust/bitbox02-rust-c/src/workflow.rs @@ -43,7 +43,9 @@ static mut CONFIRM_STATE: TaskState<'static, Result<(), confirm::UserAbort>> = T #[no_mangle] pub unsafe extern "C" fn rust_workflow_spawn_unlock() { - UNLOCK_STATE = TaskState::Running(Box::pin(bitbox02_rust::workflow::unlock::unlock())); + UNLOCK_STATE = TaskState::Running(Box::pin(bitbox02_rust::workflow::unlock::unlock( + bitbox02_rust::workflow::unlock::enter_mnemonic_passphrase_on_device, + ))); } #[no_mangle] diff --git a/src/rust/bitbox02-rust/src/hww.rs b/src/rust/bitbox02-rust/src/hww.rs index 27363e77c..0b64cf2b0 100644 --- a/src/rust/bitbox02-rust/src/hww.rs +++ b/src/rust/bitbox02-rust/src/hww.rs @@ -74,7 +74,14 @@ pub async fn next_request( /// Process OP_UNLOCK. async fn api_unlock() -> Vec { - match crate::workflow::unlock::unlock().await { + if !bitbox02::memory::is_initialized() { + return [OP_STATUS_FAILURE_UNINITIALIZED].to_vec(); + } + match crate::workflow::unlock::unlock( + crate::workflow::unlock::enter_mnemonic_passphrase_on_device, + ) + .await + { Ok(()) => [OP_STATUS_SUCCESS].to_vec(), Err(()) => [OP_STATUS_FAILURE_UNINITIALIZED].to_vec(), } diff --git a/src/rust/bitbox02-rust/src/hww/api.rs b/src/rust/bitbox02-rust/src/hww/api.rs index 62d99bd26..fa0f7690a 100644 --- a/src/rust/bitbox02-rust/src/hww/api.rs +++ b/src/rust/bitbox02-rust/src/hww/api.rs @@ -29,6 +29,7 @@ mod backup; mod bip85; mod device_info; mod electrum; +mod keystore; mod reset; mod restore; mod rootfingerprint; @@ -162,6 +163,12 @@ fn can_call(request: &Request) -> bool { Request::Reset(_) => matches!(state, State::InitializedAndUnlocked), Request::Cardano(_) => matches!(state, State::InitializedAndUnlocked), Request::Bip85(_) => matches!(state, State::InitializedAndUnlocked), + Request::Unlock(_) => matches!( + state, + State::InitializedAndLocked | State::InitializedAndUnlocked + ), + // Streamed asynchronously using the `next_request()` primitive. + Request::UnlockHostInfo(_) => false, } } @@ -211,6 +218,7 @@ async fn process_api(request: &Request) -> Result { #[cfg(not(feature = "app-cardano"))] Request::Cardano(_) => Err(Error::Disabled), Request::Bip85(ref request) => bip85::process(request).await, + Request::Unlock(ref request) => keystore::process_unlock(request).await, _ => Err(Error::InvalidInput), } } diff --git a/src/rust/bitbox02-rust/src/hww/api/keystore.rs b/src/rust/bitbox02-rust/src/hww/api/keystore.rs new file mode 100644 index 000000000..c9cd5d73d --- /dev/null +++ b/src/rust/bitbox02-rust/src/hww/api/keystore.rs @@ -0,0 +1,95 @@ +// Copyright 2025 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 pb::unlock_request_host_info_response::InfoType; + +use crate::workflow::{ + status, + trinary_choice::{choose, TrinaryChoice}, + unlock, +}; + +use alloc::string::String; + +// We check that the passphrase only contains characters that can be entered on the device too, to +// avoid a situation where the user would not be able to type their passphrase into the device, +// e.g. when using a third party wallet app that does not support entering the passphrase on the +// host. +// +// It's also good to not allow all UTF-8 chars for compatiblity with the wider ecosystem in general. +fn is_host_passphrase_valid(passphrase: &str) -> bool { + let allowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 !\"#$%&'()*+,-./:;<=>?^[\\]@_{|}"; + passphrase.len() <= bitbox02::ui::INPUT_STRING_MAX_SIZE + && passphrase.chars().all(|c| allowed.contains(c)) +} + +async fn get_mnemonic_passphrase_from_host() -> Result>, Error> { + let response = crate::hww::next_request(Response::UnlockHostInfo( + pb::UnlockRequestHostInfoResponse { + r#type: InfoType::Passphrase as _, + }, + )) + .await?; + + let info: &pb::UnlockHostInfoRequest = match &response { + pb::request::Request::UnlockHostInfo(info) => info, + _ => return Err(Error::InvalidState), + }; + + Ok(match info.passphrase.as_deref() { + Some(passphrase) if is_host_passphrase_valid(passphrase) => { + Some(zeroize::Zeroizing::new(passphrase.into())) + } + Some(_) => { + status::status("Invalid\npassphrase", false).await; + None + } + None => None, + }) +} + +async fn get_mnemonic_passphrase( + supports_host_passphrase: bool, +) -> Result>, Error> { + if supports_host_passphrase { + let choice = choose( + "Where to enter\npassphrase?", + Some("Device"), + None, + Some("Host"), + ) + .await; + + if choice == TrinaryChoice::TRINARY_CHOICE_RIGHT { + // While we are waiting for the passphrsae from the host, he BitBox shows the regular + // waiting screen. We could show a custom "Waiting on host..." component, but the + // problem is that if the host wallet is closed and we never get a response, this screen + // would remain there and not disappear until a new BitBox connection is made. The + // reason for this is that while we wait, there is no active request from the host, so + // there is no polling/pinging, so the regular USB request timeout cancellation which + // drops the current task never happens. + return get_mnemonic_passphrase_from_host().await; + } + } + Ok(unlock::enter_mnemonic_passphrase_on_device().await?) +} + +pub async fn process_unlock(request: &pb::UnlockRequest) -> Result { + unlock::unlock(|| get_mnemonic_passphrase(request.supports_host_passphrase)).await?; + Ok(Response::Success(pb::Success {})) +} diff --git a/src/rust/bitbox02-rust/src/hww/api/restore.rs b/src/rust/bitbox02-rust/src/hww/api/restore.rs index 6cc65c451..481b29a0c 100644 --- a/src/rust/bitbox02-rust/src/hww/api/restore.rs +++ b/src/rust/bitbox02-rust/src/hww/api/restore.rs @@ -83,7 +83,7 @@ pub async fn from_file(request: &pb::RestoreBackupRequest) -> Result &'static str { + match self { + InfoType::Unknown => "UNKNOWN", + InfoType::Passphrase => "PASSPHRASE", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "UNKNOWN" => Some(Self::Unknown), + "PASSPHRASE" => Some(Self::Passphrase), + _ => None, + } + } + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct UnlockHostInfoRequest { + /// Omit if type==PASSPHRASE and entering the passhrase is cancelled on the host. + #[prost(string, optional, tag = "1")] + pub passphrase: ::core::option::Option<::prost::alloc::string::String>, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, Copy, PartialEq, ::prost::Message)] pub struct ShowMnemonicRequest {} #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, Copy, PartialEq, ::prost::Message)] @@ -1829,7 +1891,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, 28" + 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, 29, 30" )] pub request: ::core::option::Option, } @@ -1892,6 +1954,10 @@ pub mod request { Cardano(super::CardanoRequest), #[prost(message, tag = "28")] Bip85(super::Bip85Request), + #[prost(message, tag = "29")] + Unlock(super::UnlockRequest), + #[prost(message, tag = "30")] + UnlockHostInfo(super::UnlockHostInfoRequest), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -1899,7 +1965,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, 16" + tags = "1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17" )] pub response: ::core::option::Option, } @@ -1939,5 +2005,7 @@ pub mod response { Cardano(super::CardanoResponse), #[prost(message, tag = "16")] Bip85(super::Bip85Response), + #[prost(message, tag = "17")] + UnlockHostInfo(super::UnlockRequestHostInfoResponse), } } diff --git a/src/rust/bitbox02-rust/src/workflow/unlock.rs b/src/rust/bitbox02-rust/src/workflow/unlock.rs index 86a6ee4cf..2306025a2 100644 --- a/src/rust/bitbox02-rust/src/workflow/unlock.rs +++ b/src/rust/bitbox02-rust/src/workflow/unlock.rs @@ -20,34 +20,41 @@ use bitbox02::keystore; pub use password::CanCancel; +use alloc::string::String; + /// Confirm the entered mnemonic passphrase with the user. Returns true if the user confirmed it, /// false if the user rejected it. -async fn confirm_mnemonic_passphrase(passphrase: &str) -> Result<(), confirm::UserAbort> { +pub async fn confirm_mnemonic_passphrase(passphrase: &str) -> Result<(), confirm::UserAbort> { // Accept empty passphrase without confirmation. if passphrase.is_empty() { + confirm::confirm(&confirm::Params { + title: "", + body: "Proceed with\nempty passphrase?", + accept_is_nextarrow: true, + ..Default::default() + }) + .await?; return Ok(()); } - let params = confirm::Params { + confirm::confirm(&confirm::Params { title: "", body: "You will be asked to\nvisually confirm your\npassphrase now.", accept_only: true, accept_is_nextarrow: true, ..Default::default() - }; - - confirm::confirm(¶ms).await?; + }) + .await?; - let params = confirm::Params { + confirm::confirm(&confirm::Params { title: "Confirm", body: passphrase, font: bitbox02::ui::Font::Password11X12, scrollable: true, longtouch: true, ..Default::default() - }; - - confirm::confirm(¶ms).await + }) + .await } pub enum UnlockError { @@ -93,9 +100,22 @@ pub async fn unlock_keystore( } } +/// Prompts the user to enter the optional passphrase on the device and returns the entered +/// passphrase. +pub async fn enter_mnemonic_passphrase_on_device() -> Result>, ()> +{ + Ok(Some( + password::enter("Optional passphrase", true, password::CanCancel::No) + .await + .expect("not cancelable"), + )) +} + /// Performs the BIP39 keystore unlock, including unlock animation. If the optional passphrase /// feature is enabled, the user will be asked for the passphrase. -pub async fn unlock_bip39() { +pub async fn unlock_bip39( + get_mnemonic_passphrase: impl AsyncFn() -> Result>, E>, +) -> Result<(), E> { // Empty passphrase by default. let mut mnemonic_passphrase = zeroize::Zeroizing::new("".into()); @@ -103,13 +123,12 @@ pub async fn unlock_bip39() { if bitbox02::memory::is_mnemonic_passphrase_enabled() { // Loop until the user confirms. loop { - mnemonic_passphrase = - password::enter("Optional passphrase", true, password::CanCancel::No) - .await - .expect("not cancelable"); + if let Some(passphrase) = get_mnemonic_passphrase().await? { + mnemonic_passphrase = passphrase; - if let Ok(()) = confirm_mnemonic_passphrase(mnemonic_passphrase.as_str()).await { - break; + if let Ok(()) = confirm_mnemonic_passphrase(mnemonic_passphrase.as_str()).await { + break; + } } status("Please try again", false).await; @@ -120,19 +139,19 @@ pub async fn unlock_bip39() { if result.is_err() { abort("bip39 unlock failed"); } + Ok(()) } /// Invokes the unlock workflow. This function does not finish until the keystore is unlocked, or /// the device is reset due to too many failed unlock attempts. /// -/// If the optional passphrase feature is enabled, the passphrase will also be entered by the -/// user. Otherwise, the empty "" passphrase is used by default. +/// If the optional passphrase feature is enabled, the passphrase will be fetched using the +/// callback. Otherwise, the empty "" passphrase is used by default. /// /// Returns Ok on success, Err if the device cannot be unlocked because it was not initialized. -pub async fn unlock() -> Result<(), ()> { - if !bitbox02::memory::is_initialized() { - return Err(()); - } +pub async fn unlock( + get_mnemonic_passphrase: impl AsyncFn() -> Result>, E>, +) -> Result<(), E> { if !bitbox02::keystore::is_locked() { return Ok(()); } @@ -143,6 +162,5 @@ pub async fn unlock() -> Result<(), ()> { .is_err() {} - unlock_bip39().await; - Ok(()) + unlock_bip39(get_mnemonic_passphrase).await } diff --git a/src/rust/bitbox02/src/ui/types.rs b/src/rust/bitbox02/src/ui/types.rs index d4972f21e..75a19fbd8 100644 --- a/src/rust/bitbox02/src/ui/types.rs +++ b/src/rust/bitbox02/src/ui/types.rs @@ -24,6 +24,10 @@ pub use bitbox02_sys::trinary_choice_t as TrinaryChoice; #[cfg_attr(any(feature = "testing", feature = "c-unit-testing"), allow(dead_code))] pub(crate) const MAX_LABEL_SIZE: usize = bitbox02_sys::MAX_LABEL_SIZE as _; +// Deduct one, as the C const includes the null terminator. +#[cfg_attr(any(feature = "testing", feature = "c-unit-testing"), allow(dead_code))] +pub const INPUT_STRING_MAX_SIZE: usize = bitbox02_sys::INPUT_STRING_MAX_SIZE as usize - 1; + #[derive(Default)] pub enum Font { #[default] diff --git a/src/rust/bitbox02/src/ui/ui.rs b/src/rust/bitbox02/src/ui/ui.rs index 9301fba29..b1ac95292 100644 --- a/src/rust/bitbox02/src/ui/ui.rs +++ b/src/rust/bitbox02/src/ui/ui.rs @@ -15,7 +15,7 @@ pub use super::types::{ AcceptRejectCb, ConfirmParams, ContinueCancelCb, Font, MenuParams, SelectWordCb, TrinaryChoice, - TrinaryChoiceCb, TrinaryInputStringParams, + TrinaryChoiceCb, TrinaryInputStringParams, INPUT_STRING_MAX_SIZE, }; use util::c_types::{c_char, c_void}; diff --git a/src/rust/bitbox02/src/ui/ui_stub.rs b/src/rust/bitbox02/src/ui/ui_stub.rs index 226c2260e..e8d306685 100644 --- a/src/rust/bitbox02/src/ui/ui_stub.rs +++ b/src/rust/bitbox02/src/ui/ui_stub.rs @@ -16,7 +16,7 @@ pub use super::types::{ AcceptRejectCb, ConfirmParams, ContinueCancelCb, Font, MenuParams, SelectWordCb, TrinaryChoice, - TrinaryChoiceCb, TrinaryInputStringParams, + TrinaryChoiceCb, TrinaryInputStringParams, INPUT_STRING_MAX_SIZE, }; use core::marker::PhantomData; diff --git a/src/rust/bitbox02/src/ui/ui_stub_c_unit_tests.rs b/src/rust/bitbox02/src/ui/ui_stub_c_unit_tests.rs index acc76c5c1..1866a0379 100644 --- a/src/rust/bitbox02/src/ui/ui_stub_c_unit_tests.rs +++ b/src/rust/bitbox02/src/ui/ui_stub_c_unit_tests.rs @@ -16,7 +16,7 @@ pub use super::types::{ AcceptRejectCb, ConfirmParams, ContinueCancelCb, Font, MenuParams, SelectWordCb, TrinaryChoice, - TrinaryChoiceCb, TrinaryInputStringParams, + TrinaryChoiceCb, TrinaryInputStringParams, INPUT_STRING_MAX_SIZE, }; use core::marker::PhantomData;