Skip to content

Commit 1401ee1

Browse files
benmaSjors
authored andcommitted
support BitBox02 Nova by updating bitbox02 lib to v7.0.0
This adds support for BitBox02 Nova. It has the same API has BitBox02.
1 parent baf16ca commit 1401ee1

27 files changed

+1595
-1201
lines changed

hwilib/devices/bitbox02.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -194,10 +194,10 @@ def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain
194194
bb02 = None
195195
with handle_errors(common_err_msgs["enumerate"], d_data):
196196
bb02 = client.init(expect_initialized=None)
197-
version, platform, edition, unlocked = bitbox02.BitBox02.get_info(
197+
version, platform, edition, unlocked, _ = bitbox02.BitBox02.get_info(
198198
client.transport
199199
)
200-
if platform != Platform.BITBOX02:
200+
if platform not in (Platform.BITBOX02, Platform.BITBOX02PLUS):
201201
client.close()
202202
continue
203203
if edition not in (BitBox02Edition.MULTI, BitBox02Edition.BTCONLY):
@@ -211,9 +211,15 @@ def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain
211211
"type": "bitbox02",
212212
"path": path,
213213
"model": {
214-
BitBox02Edition.MULTI: "bitbox02_multi",
215-
BitBox02Edition.BTCONLY: "bitbox02_btconly",
216-
}[edition],
214+
Platform.BITBOX02: {
215+
BitBox02Edition.MULTI: "bitbox02_multi",
216+
BitBox02Edition.BTCONLY: "bitbox02_btconly",
217+
},
218+
Platform.BITBOX02PLUS: {
219+
BitBox02Edition.MULTI: "bitbox02_nova_multi",
220+
BitBox02Edition.BTCONLY: "bitbox02_nova_btconly",
221+
},
222+
}[platform][edition],
217223
"needs_pin_sent": False,
218224
"needs_passphrase_sent": False,
219225
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# Python BitBox02 Library
22

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

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

77
## Changes
88

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

12-
See commit ac1d5184f1d7c34630b7eb02d1ce5a7b1e16dc61 for the modifications made
12+
See commit 753a9d0c6eec9d7446793a4ce2330fb975d58684 for the modifications made

hwilib/devices/bitbox02_lib/bitbox02/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
""" Library to interact with a BitBox02 device. """
14+
"""Library to interact with a BitBox02 device."""
1515

1616
from __future__ import print_function
1717
import sys
1818

19-
__version__ = "6.3.0"
19+
__version__ = "7.0.0"
2020

2121
if sys.version_info.major != 3 or sys.version_info.minor < 6:
2222
print(

hwilib/devices/bitbox02_lib/bitbox02/bitbox02.py

Lines changed: 112 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,10 @@
4343
from ..communication.generated import common_pb2 as common
4444
from ..communication.generated import keystore_pb2 as keystore
4545
from ..communication.generated import antiklepto_pb2 as antiklepto
46+
from ..communication.generated import bluetooth_pb2 as bluetooth
4647
import google.protobuf.empty_pb2
4748

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

117118
class BTCOutputInternal:
118119
# TODO: Use NamedTuple, but not playing well with protobuf types.
120+
"""
121+
Internal transaction output (output belongs to BitBox).
122+
"""
119123

120-
def __init__(self, keypath: Sequence[int], value: int, script_config_index: int):
124+
def __init__(
125+
self,
126+
keypath: Sequence[int],
127+
value: int,
128+
script_config_index: int,
129+
output_script_config_index: Optional[int] = None,
130+
):
121131
"""
122132
keypath: keypath to the change output.
123133
"""
124134
self.keypath = keypath
125135
self.value = value
126136
self.script_config_index = script_config_index
137+
self.output_script_config_index = output_script_config_index
127138

128139

129140
class BTCOutputExternal:
130141
# TODO: Use NamedTuple, but not playing well with protobuf types.
142+
"""
143+
External transaction output.
144+
"""
131145

132146
def __init__(self, output_type: "btc.BTCOutputType.V", output_payload: bytes, value: int):
133147
self.type = output_type
@@ -161,6 +175,14 @@ def device_info(self) -> Dict[str, Any]:
161175
}
162176
if self.version >= semver.VersionInfo(9, 6, 0):
163177
result["securechip_model"] = response.device_info.securechip_model
178+
if response.device_info.bluetooth is not None:
179+
result["bluetooth"] = {
180+
"firmware_hash": response.device_info.bluetooth.firmware_hash,
181+
"firmware_version": response.device_info.bluetooth.firmware_version,
182+
"enabled": response.device_info.bluetooth.enabled,
183+
}
184+
else:
185+
result["bluetooth"] = None
164186

165187
return result
166188

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

446+
if output_script_configs:
447+
# Attaching output info supported since v9.22.0.
448+
self._require_atleast(semver.VersionInfo(9, 22, 0))
449+
421450
supports_antiklepto = self.version >= semver.VersionInfo(9, 4, 0)
422451

423452
sigs: List[Tuple[int, bytes]] = []
@@ -433,6 +462,7 @@ def btc_sign(
433462
num_outputs=len(outputs),
434463
locktime=locktime,
435464
format_unit=format_unit,
465+
output_script_configs=output_script_configs,
436466
)
437467
)
438468
next_response = self._msg_query(request, expected_response="btc_sign_next").btc_sign_next
@@ -552,12 +582,16 @@ def btc_sign(
552582

553583
request = hww.Request()
554584
if isinstance(tx_output, BTCOutputInternal):
585+
if tx_output.output_script_config_index is not None:
586+
# Attaching output info supported since v9.22.0.
587+
self._require_atleast(semver.VersionInfo(9, 22, 0))
555588
request.btc_sign_output.CopyFrom(
556589
btc.BTCSignOutputRequest(
557590
ours=True,
558591
value=tx_output.value,
559592
keypath=tx_output.keypath,
560593
script_config_index=tx_output.script_config_index,
594+
output_script_config_index=tx_output.output_script_config_index,
561595
)
562596
)
563597
elif isinstance(tx_output, BTCOutputExternal):
@@ -588,6 +622,8 @@ def btc_sign_msg(
588622
# pylint: disable=no-member
589623

590624
self._require_atleast(semver.VersionInfo(9, 2, 0))
625+
if coin in (btc.TBTC, btc.RBTC):
626+
self._require_atleast(semver.VersionInfo(9, 23, 0))
591627

592628
request = btc.BTCRequest()
593629
request.sign_message.CopyFrom(
@@ -648,16 +684,6 @@ def insert_sdcard(self) -> None:
648684
)
649685
self._msg_query(request, expected_response="success")
650686

651-
def remove_sdcard(self) -> None:
652-
# pylint: disable=no-member
653-
request = hww.Request()
654-
request.insert_remove_sdcard.CopyFrom(
655-
bitbox02_system.InsertRemoveSDCardRequest(
656-
action=bitbox02_system.InsertRemoveSDCardRequest.REMOVE_CARD
657-
)
658-
)
659-
self._msg_query(request, expected_response="success")
660-
661687
def root_fingerprint(self) -> bytes:
662688
"""
663689
Get the root fingerprint from the bitbox02
@@ -788,10 +814,18 @@ def eth_pub(
788814
)
789815
return self._eth_msg_query(request, expected_response="pub").pub.pub
790816

791-
def eth_sign(self, transaction: bytes, keypath: Sequence[int], chain_id: int = 1) -> bytes:
817+
def eth_sign(
818+
self,
819+
transaction: bytes,
820+
keypath: Sequence[int],
821+
address_case: eth.ETHAddressCase.ValueType = eth.ETH_ADDRESS_CASE_MIXED,
822+
chain_id: int = 1,
823+
) -> bytes:
792824
"""
793825
transaction should be given as a full rlp encoded eth transaction.
794826
"""
827+
# pylint: disable=no-member
828+
795829
is_eip1559 = transaction.startswith(b"\x02")
796830

797831
def handle_antiklepto(request: eth.ETHRequest) -> bytes:
@@ -853,6 +887,7 @@ def handle_antiklepto(request: eth.ETHRequest) -> bytes:
853887
recipient=recipient,
854888
value=value,
855889
data=data,
890+
address_case=address_case,
856891
)
857892
)
858893
return handle_antiklepto(request)
@@ -871,6 +906,7 @@ def handle_antiklepto(request: eth.ETHRequest) -> bytes:
871906
recipient=recipient,
872907
value=value,
873908
data=data,
909+
address_case=address_case,
874910
)
875911
)
876912

@@ -1176,7 +1212,69 @@ def cardano_address(self, address: cardano.CardanoAddressRequest) -> str:
11761212
def cardano_sign_transaction(
11771213
self, transaction: cardano.CardanoSignTransactionRequest
11781214
) -> cardano.CardanoSignTransactionResponse:
1215+
if transaction.tag_cbor_sets:
1216+
self._require_atleast(semver.VersionInfo(9, 22, 0))
11791217
request = cardano.CardanoRequest(sign_transaction=transaction)
11801218
return self._cardano_msg_query(
11811219
request, expected_response="sign_transaction"
11821220
).sign_transaction
1221+
1222+
def _bluetooth_msg_query(
1223+
self, bluetooth_request: bluetooth.BluetoothRequest, expected_response: Optional[str] = None
1224+
) -> bluetooth.BluetoothResponse:
1225+
"""
1226+
Same as _msg_query, but one nesting deeper for bluetooth messages.
1227+
"""
1228+
# pylint: disable=no-member
1229+
request = hww.Request()
1230+
request.bluetooth.CopyFrom(bluetooth_request)
1231+
bluetooth_response = self._msg_query(request, expected_response="bluetooth").bluetooth
1232+
if (
1233+
expected_response is not None
1234+
and bluetooth_response.WhichOneof("response") != expected_response
1235+
):
1236+
raise Exception(
1237+
"Unexpected response: {}, expected: {}".format(
1238+
bluetooth_response.WhichOneof("response"), expected_response
1239+
)
1240+
)
1241+
return bluetooth_response
1242+
1243+
def bluetooth_upgrade(self, firmware: bytes) -> None:
1244+
"""
1245+
Install the given Bluetooth firmware.
1246+
"""
1247+
# pylint: disable=no-member
1248+
request = bluetooth.BluetoothRequest()
1249+
request.upgrade_init.CopyFrom(
1250+
bluetooth.BluetoothUpgradeInitRequest(firmware_length=len(firmware))
1251+
)
1252+
1253+
response = self._bluetooth_msg_query(request)
1254+
while True:
1255+
response_type = response.WhichOneof("response")
1256+
if response_type == "request_chunk":
1257+
chunk_response = response.request_chunk
1258+
request = bluetooth.BluetoothRequest()
1259+
request.chunk.CopyFrom(
1260+
bluetooth.BluetoothChunkRequest(
1261+
data=firmware[
1262+
chunk_response.offset : chunk_response.offset + chunk_response.length
1263+
]
1264+
),
1265+
)
1266+
response = self._bluetooth_msg_query(request)
1267+
elif response_type == "success":
1268+
break
1269+
else:
1270+
raise Exception(f"Unexpected response: f{response_type}")
1271+
1272+
def bluetooth_toggle_enabled(self) -> None:
1273+
"""
1274+
Enable/disable Bluetooth in non-volatile storage
1275+
"""
1276+
# pylint: disable=no-member
1277+
request = bluetooth.BluetoothRequest()
1278+
request.toggle_enabled.CopyFrom(bluetooth.BluetoothToggleEnabledRequest())
1279+
1280+
self._bluetooth_msg_query(request, expected_response="success")

0 commit comments

Comments
 (0)