Skip to content

Commit 25d390b

Browse files
committed
bitbox02 library: Update to v6.3.0
Signed-off-by: asi345 <inanata15@gmail.com>
1 parent 9fbe6be commit 25d390b

14 files changed

+428
-156
lines changed

hwilib/devices/bitbox02_lib/bitbox02/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from __future__ import print_function
1717
import sys
1818

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

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

hwilib/devices/bitbox02_lib/bitbox02/bitbox02.py

+123-34
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,25 @@
3232

3333
from .secp256k1 import antiklepto_host_commit, antiklepto_verify
3434

35-
from ..communication.generated import hww_pb2 as hww
36-
from ..communication.generated import eth_pb2 as eth
37-
from ..communication.generated import btc_pb2 as btc
38-
from ..communication.generated import cardano_pb2 as cardano
39-
from ..communication.generated import mnemonic_pb2 as mnemonic
40-
from ..communication.generated import bitbox02_system_pb2 as bitbox02_system
41-
from ..communication.generated import backup_commands_pb2 as backup
42-
from ..communication.generated import common_pb2 as common
43-
from ..communication.generated import keystore_pb2 as keystore
44-
from ..communication.generated import antiklepto_pb2 as antiklepto
45-
46-
# pylint: disable=unused-import
47-
# We export it in __init__.py
48-
from ..communication.generated import system_pb2 as system
35+
try:
36+
from ..communication.generated import hww_pb2 as hww
37+
from ..communication.generated import eth_pb2 as eth
38+
from ..communication.generated import btc_pb2 as btc
39+
from ..communication.generated import cardano_pb2 as cardano
40+
from ..communication.generated import mnemonic_pb2 as mnemonic
41+
from ..communication.generated import bitbox02_system_pb2 as bitbox02_system
42+
from ..communication.generated import backup_commands_pb2 as backup
43+
from ..communication.generated import common_pb2 as common
44+
from ..communication.generated import keystore_pb2 as keystore
45+
from ..communication.generated import antiklepto_pb2 as antiklepto
46+
import google.protobuf.empty_pb2
47+
48+
# pylint: disable=unused-import
49+
# We export it in __init__.py
50+
from ..communication.generated import system_pb2 as system
51+
except ModuleNotFoundError:
52+
print("Run `make py` to generate the protobuf messages")
53+
sys.exit()
4954

5055
try:
5156
# Optional rlp dependency only needed to sign ethereum transactions.
@@ -674,6 +679,40 @@ def electrum_encryption_key(self, keypath: Sequence[int]) -> str:
674679
)
675680
return self._msg_query(request).electrum_encryption_key.key
676681

682+
def bip85_bip39(self) -> None:
683+
"""Invokes the BIP85-BIP39 workflow on the device"""
684+
self._require_atleast(semver.VersionInfo(9, 18, 0))
685+
686+
# pylint: disable=no-member
687+
request = hww.Request()
688+
request.bip85.CopyFrom(
689+
keystore.BIP85Request(
690+
bip39=google.protobuf.empty_pb2.Empty(),
691+
)
692+
)
693+
response = self._msg_query(request, expected_response="bip85").bip85
694+
assert response.WhichOneof("app") == "bip39"
695+
696+
def bip85_ln(self) -> bytes:
697+
"""
698+
Generates and returns a mnemonic for a hot Lightning wallet from the device using BIP-85.
699+
"""
700+
self._require_atleast(semver.VersionInfo(9, 17, 0))
701+
702+
# Only account_number=0 is allowed for now.
703+
account_number = 0
704+
705+
# pylint: disable=no-member
706+
request = hww.Request()
707+
request.bip85.CopyFrom(
708+
keystore.BIP85Request(
709+
ln=keystore.BIP85Request.AppLn(account_number=account_number),
710+
)
711+
)
712+
response = self._msg_query(request, expected_response="bip85").bip85
713+
assert response.WhichOneof("app") == "ln"
714+
return response.ln
715+
677716
def enable_mnemonic_passphrase(self) -> None:
678717
"""
679718
Enable the bip39 passphrase.
@@ -753,28 +792,17 @@ def eth_sign(self, transaction: bytes, keypath: Sequence[int], chain_id: int = 1
753792
"""
754793
transaction should be given as a full rlp encoded eth transaction.
755794
"""
756-
nonce, gas_price, gas_limit, recipient, value, data, _, _, _ = rlp.decode(transaction)
757-
request = eth.ETHRequest()
758-
# pylint: disable=no-member
759-
request.sign.CopyFrom(
760-
eth.ETHSignRequest(
761-
coin=self._eth_coin(chain_id),
762-
chain_id=chain_id,
763-
keypath=keypath,
764-
nonce=nonce,
765-
gas_price=gas_price,
766-
gas_limit=gas_limit,
767-
recipient=recipient,
768-
value=value,
769-
data=data,
770-
)
771-
)
795+
is_eip1559 = transaction.startswith(b"\x02")
772796

773-
supports_antiklepto = self.version >= semver.VersionInfo(9, 5, 0)
774-
if supports_antiklepto:
797+
def handle_antiklepto(request: eth.ETHRequest) -> bytes:
775798
host_nonce = os.urandom(32)
799+
if is_eip1559:
800+
request.sign_eip1559.host_nonce_commitment.commitment = antiklepto_host_commit(
801+
host_nonce
802+
)
803+
else:
804+
request.sign.host_nonce_commitment.commitment = antiklepto_host_commit(host_nonce)
776805

777-
request.sign.host_nonce_commitment.commitment = antiklepto_host_commit(host_nonce)
778806
signer_commitment = self._eth_msg_query(
779807
request, expected_response="antiklepto_signer_commitment"
780808
).antiklepto_signer_commitment.commitment
@@ -792,6 +820,64 @@ def eth_sign(self, transaction: bytes, keypath: Sequence[int], chain_id: int = 1
792820

793821
return signature
794822

823+
if is_eip1559:
824+
self._require_atleast(semver.VersionInfo(9, 16, 0))
825+
(
826+
decoded_chain_id,
827+
nonce,
828+
priority_fee,
829+
max_fee,
830+
gas_limit,
831+
recipient,
832+
value,
833+
data,
834+
_,
835+
_,
836+
_,
837+
) = rlp.decode(transaction[1:])
838+
decoded_chain_id_int = int.from_bytes(decoded_chain_id, byteorder="big")
839+
if decoded_chain_id_int != chain_id:
840+
raise Exception(
841+
f"chainID argument ({chain_id}) does not match chainID encoded in transaction ({decoded_chain_id_int})"
842+
)
843+
request = eth.ETHRequest()
844+
# pylint: disable=no-member
845+
request.sign_eip1559.CopyFrom(
846+
eth.ETHSignEIP1559Request(
847+
chain_id=chain_id,
848+
keypath=keypath,
849+
nonce=nonce,
850+
max_priority_fee_per_gas=priority_fee,
851+
max_fee_per_gas=max_fee,
852+
gas_limit=gas_limit,
853+
recipient=recipient,
854+
value=value,
855+
data=data,
856+
)
857+
)
858+
return handle_antiklepto(request)
859+
860+
nonce, gas_price, gas_limit, recipient, value, data, _, _, _ = rlp.decode(transaction)
861+
request = eth.ETHRequest()
862+
# pylint: disable=no-member
863+
request.sign.CopyFrom(
864+
eth.ETHSignRequest(
865+
coin=self._eth_coin(chain_id),
866+
chain_id=chain_id,
867+
keypath=keypath,
868+
nonce=nonce,
869+
gas_price=gas_price,
870+
gas_limit=gas_limit,
871+
recipient=recipient,
872+
value=value,
873+
data=data,
874+
)
875+
)
876+
877+
supports_antiklepto = self.version >= semver.VersionInfo(9, 5, 0)
878+
if supports_antiklepto:
879+
return handle_antiklepto(request)
880+
795881
return self._eth_msg_query(request, expected_response="sign").sign.signature
796882

797883
def eth_sign_msg(self, msg: bytes, keypath: Sequence[int], chain_id: int = 1) -> bytes:
@@ -945,7 +1031,10 @@ def get_value(
9451031
return value
9461032
if typ.type == eth.ETHSignTypedMessageRequest.DataType.UINT:
9471033
if isinstance(value, str):
948-
value = int(value)
1034+
if value[:2].lower() == "0x":
1035+
value = int(value[2:], 16)
1036+
else:
1037+
value = int(value)
9491038
assert isinstance(value, int)
9501039
return value.to_bytes(typ.size, "big")
9511040
if typ.type == eth.ETHSignTypedMessageRequest.DataType.INT:

hwilib/devices/bitbox02_lib/communication/bitbox_api_protocol.py

+25-11
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,12 @@
3333
from .communication import TransportLayer
3434
from .devices import BITBOX02MULTI, BITBOX02BTC
3535

36-
from .generated import hww_pb2 as hww
37-
from .generated import system_pb2 as system
36+
try:
37+
from .generated import hww_pb2 as hww
38+
from .generated import system_pb2 as system
39+
except ModuleNotFoundError:
40+
print("Run `make py` to generate the protobuf messages")
41+
sys.exit()
3842

3943

4044
HWW_CMD = 0x80 + 0x40 + 0x01
@@ -522,24 +526,34 @@ class BitBoxCommonAPI:
522526

523527
# pylint: disable=too-many-public-methods,too-many-arguments
524528
def __init__(
525-
self, transport: TransportLayer, device_info: DeviceInfo, noise_config: BitBoxNoiseConfig
529+
self,
530+
transport: TransportLayer,
531+
device_info: Optional[DeviceInfo],
532+
noise_config: BitBoxNoiseConfig,
526533
):
527534
"""
528535
Can raise LibraryVersionOutdatedException. check_min_version() should be called following
529536
the instantiation.
537+
If device_info is None, it is infered using the OP_INFO API call, available since
538+
firmware version v5.0.0.
530539
"""
531540
self.debug = False
532-
serial_number = device_info["serial_number"]
533541

534-
if device_info["product_string"] == BITBOX02MULTI:
535-
self.edition = BitBox02Edition.MULTI
536-
elif device_info["product_string"] == BITBOX02BTC:
537-
self.edition = BitBox02Edition.BTCONLY
542+
if device_info is not None:
543+
version = device_info["serial_number"]
544+
if device_info["product_string"] == BITBOX02MULTI:
545+
edition = BitBox02Edition.MULTI
546+
elif device_info["product_string"] == BITBOX02BTC:
547+
edition = BitBox02Edition.BTCONLY
548+
else:
549+
version, _, edition, _ = self.get_info(transport)
538550

539-
self.version = parse_device_version(serial_number)
540-
if self.version is None:
551+
self.edition = edition
552+
try:
553+
self.version = parse_device_version(version)
554+
except:
541555
transport.close()
542-
raise ValueError(f"Could not parse version from {serial_number}")
556+
raise
543557

544558
# Delete the prelease part, as it messes with the comparison (e.g. 3.0.0-pre < 3.0.0 is
545559
# True, but the 3.0.0-pre has already the same API breaking changes like 3.0.0...).

hwilib/devices/bitbox02_lib/communication/devices.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,9 @@ def get_any_bitbox02_bootloader() -> DeviceInfo:
157157
return devices[0]
158158

159159

160-
def parse_device_version(serial_number: str) -> semver.VersionInfo:
161-
match = re.search(r"v([0-9]+\.[0-9]+\.[0-9]+.*)", serial_number)
160+
def parse_device_version(version: str) -> semver.VersionInfo:
161+
match = re.search(r"v([0-9]+\.[0-9]+\.[0-9]+.*)", version)
162162
if match is None:
163-
raise Exception(f"Could not parse version string from serial_number: {serial_number}")
163+
raise ValueError(f"Could not parse version string from string: {version}")
164164

165165
return semver.VersionInfo.parse(match.group(1))

0 commit comments

Comments
 (0)