Skip to content

support BitBox02 Nova by updating bitbox02 lib to v7.0.0 #789

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions hwilib/devices/bitbox02.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,10 @@ def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain
bb02 = None
with handle_errors(common_err_msgs["enumerate"], d_data):
bb02 = client.init(expect_initialized=None)
version, platform, edition, unlocked = bitbox02.BitBox02.get_info(
version, platform, edition, unlocked, _ = bitbox02.BitBox02.get_info(
client.transport
)
if platform != Platform.BITBOX02:
if platform not in (Platform.BITBOX02, Platform.BITBOX02PLUS):
client.close()
continue
if edition not in (BitBox02Edition.MULTI, BitBox02Edition.BTCONLY):
Expand All @@ -211,9 +211,15 @@ def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain
"type": "bitbox02",
"path": path,
"model": {
BitBox02Edition.MULTI: "bitbox02_multi",
BitBox02Edition.BTCONLY: "bitbox02_btconly",
}[edition],
Platform.BITBOX02: {
BitBox02Edition.MULTI: "bitbox02_multi",
BitBox02Edition.BTCONLY: "bitbox02_btconly",
},
Platform.BITBOX02PLUS: {
BitBox02Edition.MULTI: "bitbox02_nova_multi",
BitBox02Edition.BTCONLY: "bitbox02_nova_btconly",
},
}[platform][edition],
"needs_pin_sent": False,
"needs_passphrase_sent": False,
}
Expand Down
6 changes: 3 additions & 3 deletions hwilib/devices/bitbox02_lib/README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Python BitBox02 Library

This is a slightly modified version of the official [bitbox02](https://github.com/digitalbitbox/bitbox02-firmware/tree/master/py/bitbox02) library.
This is a slightly modified version of the official [bitbox02](https://github.com/BitBoxSwiss/bitbox02-firmware/tree/master/py/bitbox02) library.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the Github repo moved without a redirect. I checked on https://bitbox.swiss/dev/ that the new org account is correct.

Copy link
Contributor Author

@benma benma Jul 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


This was made at commit [5b004e84e2928545db881e380c8ae8782743f5b2](https://github.com/digitalbitbox/bitbox02-firmware/commit/5b004e84e2928545db881e380c8ae8782743f5b2)
This was made at tag [py-bitbox02-7.0,0](https://github.com/BitBoxSwiss/bitbox02-firmware/tree/py-bitbox02-7.0.0)

## Changes

- Use our own _base58 rather than external base58 library
- Use relative imports between bitbox02 and communication instead of standard imports that require module installation

See commit ac1d5184f1d7c34630b7eb02d1ce5a7b1e16dc61 for the modifications made
See commit 753a9d0c6eec9d7446793a4ce2330fb975d58684 for the modifications made
4 changes: 2 additions & 2 deletions hwilib/devices/bitbox02_lib/bitbox02/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
# 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.
""" Library to interact with a BitBox02 device. """
"""Library to interact with a BitBox02 device."""

from __future__ import print_function
import sys

__version__ = "6.3.0"
__version__ = "7.0.0"

if sys.version_info.major != 3 or sys.version_info.minor < 6:
print(
Expand Down
126 changes: 112 additions & 14 deletions hwilib/devices/bitbox02_lib/bitbox02/bitbox02.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@
from ..communication.generated import common_pb2 as common
from ..communication.generated import keystore_pb2 as keystore
from ..communication.generated import antiklepto_pb2 as antiklepto
from ..communication.generated import bluetooth_pb2 as bluetooth
import google.protobuf.empty_pb2

# pylint: disable=unused-import
# pylint: disable=unused-import,ungrouped-imports
# We export it in __init__.py
from ..communication.generated import system_pb2 as system
except ModuleNotFoundError:
Expand Down Expand Up @@ -116,18 +117,31 @@ class BTCInputType(TypedDict):

class BTCOutputInternal:
# TODO: Use NamedTuple, but not playing well with protobuf types.
"""
Internal transaction output (output belongs to BitBox).
"""

def __init__(self, keypath: Sequence[int], value: int, script_config_index: int):
def __init__(
self,
keypath: Sequence[int],
value: int,
script_config_index: int,
output_script_config_index: Optional[int] = None,
):
"""
keypath: keypath to the change output.
"""
self.keypath = keypath
self.value = value
self.script_config_index = script_config_index
self.output_script_config_index = output_script_config_index


class BTCOutputExternal:
# TODO: Use NamedTuple, but not playing well with protobuf types.
"""
External transaction output.
"""

def __init__(self, output_type: "btc.BTCOutputType.V", output_payload: bytes, value: int):
self.type = output_type
Expand Down Expand Up @@ -161,6 +175,14 @@ def device_info(self) -> Dict[str, Any]:
}
if self.version >= semver.VersionInfo(9, 6, 0):
result["securechip_model"] = response.device_info.securechip_model
if response.device_info.bluetooth is not None:
result["bluetooth"] = {
"firmware_hash": response.device_info.bluetooth.firmware_hash,
"firmware_version": response.device_info.bluetooth.firmware_version,
"enabled": response.device_info.bluetooth.enabled,
}
else:
result["bluetooth"] = None

return result

Expand Down Expand Up @@ -390,13 +412,14 @@ def btc_sign(
version: int = 1,
locktime: int = 0,
format_unit: "btc.BTCSignInitRequest.FormatUnit.V" = btc.BTCSignInitRequest.FormatUnit.DEFAULT,
output_script_configs: Optional[Sequence[btc.BTCScriptConfigWithKeypath]] = None,
) -> Sequence[Tuple[int, bytes]]:
"""
coin: the first element of all provided keypaths must match the coin:
- BTC: 0 + HARDENED
- Testnets: 1 + HARDENED
- LTC: 2 + HARDENED
script_configs: types of all inputs and change outputs. The first element of all provided
script_configs: types of all inputs and outputs belonging to the same account (change or non-change). The first element of all provided
keypaths must match this type:
- SCRIPT_P2PKH: 44 + HARDENED
- SCRIPT_P2WPKH_P2SH: 49 + HARDENED
Expand All @@ -407,6 +430,8 @@ def btc_sign(
outputs: transaction outputs. Can be an external output
(BTCOutputExternal) or an internal output for change (BTCOutputInternal).
version, locktime: reserved for future use.
format_unit: defines in which unit amounts will be displayed
output_script_configs: script types for outputs belonging to the same keystore
Returns: list of (input index, signature) tuples.
Raises Bitbox02Exception with ERR_USER_ABORT on user abort.
"""
Expand All @@ -418,6 +443,10 @@ def btc_sign(
if any(map(is_taproot, script_configs)):
self._require_atleast(semver.VersionInfo(9, 10, 0))

if output_script_configs:
# Attaching output info supported since v9.22.0.
self._require_atleast(semver.VersionInfo(9, 22, 0))

supports_antiklepto = self.version >= semver.VersionInfo(9, 4, 0)

sigs: List[Tuple[int, bytes]] = []
Expand All @@ -433,6 +462,7 @@ def btc_sign(
num_outputs=len(outputs),
locktime=locktime,
format_unit=format_unit,
output_script_configs=output_script_configs,
)
)
next_response = self._msg_query(request, expected_response="btc_sign_next").btc_sign_next
Expand Down Expand Up @@ -552,12 +582,16 @@ def btc_sign(

request = hww.Request()
if isinstance(tx_output, BTCOutputInternal):
if tx_output.output_script_config_index is not None:
# Attaching output info supported since v9.22.0.
self._require_atleast(semver.VersionInfo(9, 22, 0))
request.btc_sign_output.CopyFrom(
btc.BTCSignOutputRequest(
ours=True,
value=tx_output.value,
keypath=tx_output.keypath,
script_config_index=tx_output.script_config_index,
output_script_config_index=tx_output.output_script_config_index,
)
)
elif isinstance(tx_output, BTCOutputExternal):
Expand Down Expand Up @@ -588,6 +622,8 @@ def btc_sign_msg(
# pylint: disable=no-member

self._require_atleast(semver.VersionInfo(9, 2, 0))
if coin in (btc.TBTC, btc.RBTC):
self._require_atleast(semver.VersionInfo(9, 23, 0))

request = btc.BTCRequest()
request.sign_message.CopyFrom(
Expand Down Expand Up @@ -648,16 +684,6 @@ def insert_sdcard(self) -> None:
)
self._msg_query(request, expected_response="success")

def remove_sdcard(self) -> None:
# pylint: disable=no-member
request = hww.Request()
request.insert_remove_sdcard.CopyFrom(
bitbox02_system.InsertRemoveSDCardRequest(
action=bitbox02_system.InsertRemoveSDCardRequest.REMOVE_CARD
)
)
self._msg_query(request, expected_response="success")

def root_fingerprint(self) -> bytes:
"""
Get the root fingerprint from the bitbox02
Expand Down Expand Up @@ -788,10 +814,18 @@ def eth_pub(
)
return self._eth_msg_query(request, expected_response="pub").pub.pub

def eth_sign(self, transaction: bytes, keypath: Sequence[int], chain_id: int = 1) -> bytes:
def eth_sign(
self,
transaction: bytes,
keypath: Sequence[int],
address_case: eth.ETHAddressCase.ValueType = eth.ETH_ADDRESS_CASE_MIXED,
chain_id: int = 1,
) -> bytes:
"""
transaction should be given as a full rlp encoded eth transaction.
"""
# pylint: disable=no-member

is_eip1559 = transaction.startswith(b"\x02")

def handle_antiklepto(request: eth.ETHRequest) -> bytes:
Expand Down Expand Up @@ -853,6 +887,7 @@ def handle_antiklepto(request: eth.ETHRequest) -> bytes:
recipient=recipient,
value=value,
data=data,
address_case=address_case,
)
)
return handle_antiklepto(request)
Expand All @@ -871,6 +906,7 @@ def handle_antiklepto(request: eth.ETHRequest) -> bytes:
recipient=recipient,
value=value,
data=data,
address_case=address_case,
)
)

Expand Down Expand Up @@ -1176,7 +1212,69 @@ def cardano_address(self, address: cardano.CardanoAddressRequest) -> str:
def cardano_sign_transaction(
self, transaction: cardano.CardanoSignTransactionRequest
) -> cardano.CardanoSignTransactionResponse:
if transaction.tag_cbor_sets:
self._require_atleast(semver.VersionInfo(9, 22, 0))
request = cardano.CardanoRequest(sign_transaction=transaction)
return self._cardano_msg_query(
request, expected_response="sign_transaction"
).sign_transaction

def _bluetooth_msg_query(
self, bluetooth_request: bluetooth.BluetoothRequest, expected_response: Optional[str] = None
) -> bluetooth.BluetoothResponse:
"""
Same as _msg_query, but one nesting deeper for bluetooth messages.
"""
# pylint: disable=no-member
request = hww.Request()
request.bluetooth.CopyFrom(bluetooth_request)
bluetooth_response = self._msg_query(request, expected_response="bluetooth").bluetooth
if (
expected_response is not None
and bluetooth_response.WhichOneof("response") != expected_response
):
raise Exception(
"Unexpected response: {}, expected: {}".format(
bluetooth_response.WhichOneof("response"), expected_response
)
)
return bluetooth_response

def bluetooth_upgrade(self, firmware: bytes) -> None:
"""
Install the given Bluetooth firmware.
"""
# pylint: disable=no-member
request = bluetooth.BluetoothRequest()
request.upgrade_init.CopyFrom(
bluetooth.BluetoothUpgradeInitRequest(firmware_length=len(firmware))
)

response = self._bluetooth_msg_query(request)
while True:
response_type = response.WhichOneof("response")
if response_type == "request_chunk":
chunk_response = response.request_chunk
request = bluetooth.BluetoothRequest()
request.chunk.CopyFrom(
bluetooth.BluetoothChunkRequest(
data=firmware[
chunk_response.offset : chunk_response.offset + chunk_response.length
]
),
)
response = self._bluetooth_msg_query(request)
elif response_type == "success":
break
else:
raise Exception(f"Unexpected response: f{response_type}")

def bluetooth_toggle_enabled(self) -> None:
"""
Enable/disable Bluetooth in non-volatile storage
"""
# pylint: disable=no-member
request = bluetooth.BluetoothRequest()
request.toggle_enabled.CopyFrom(bluetooth.BluetoothToggleEnabledRequest())

self._bluetooth_msg_query(request, expected_response="success")
Loading
Loading