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 2314d0567..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(), } @@ -119,12 +126,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 +418,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..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; @@ -94,11 +95,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 +115,60 @@ 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), + Request::Unlock(_) => matches!( + state, + State::InitializedAndLocked | State::InitializedAndUnlocked + ), + // Streamed asynchronously using the `next_request()` primitive. + Request::UnlockHostInfo(_) => false, } } @@ -184,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/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/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/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-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 79e93d3e8..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}; @@ -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..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; @@ -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..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; @@ -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; }