Skip to content

Commit

Permalink
bitbox02 library: Update to v6.3.0
Browse files Browse the repository at this point in the history
Signed-off-by: asi345 <inanata15@gmail.com>
  • Loading branch information
asi345 committed Sep 25, 2024
1 parent fedc41c commit 000e5c5
Show file tree
Hide file tree
Showing 14 changed files with 428 additions and 156 deletions.
2 changes: 1 addition & 1 deletion hwilib/devices/bitbox02_lib/bitbox02/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from __future__ import print_function
import sys

__version__ = "6.2.0"
__version__ = "6.3.0"

if sys.version_info.major != 3 or sys.version_info.minor < 6:
print(
Expand Down
157 changes: 123 additions & 34 deletions hwilib/devices/bitbox02_lib/bitbox02/bitbox02.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,25 @@

from .secp256k1 import antiklepto_host_commit, antiklepto_verify

from ..communication.generated import hww_pb2 as hww
from ..communication.generated import eth_pb2 as eth
from ..communication.generated import btc_pb2 as btc
from ..communication.generated import cardano_pb2 as cardano
from ..communication.generated import mnemonic_pb2 as mnemonic
from ..communication.generated import bitbox02_system_pb2 as bitbox02_system
from ..communication.generated import backup_commands_pb2 as backup
from ..communication.generated import common_pb2 as common
from ..communication.generated import keystore_pb2 as keystore
from ..communication.generated import antiklepto_pb2 as antiklepto

# pylint: disable=unused-import
# We export it in __init__.py
from ..communication.generated import system_pb2 as system
try:
from ..communication.generated import hww_pb2 as hww
from ..communication.generated import eth_pb2 as eth
from ..communication.generated import btc_pb2 as btc
from ..communication.generated import cardano_pb2 as cardano
from ..communication.generated import mnemonic_pb2 as mnemonic
from ..communication.generated import bitbox02_system_pb2 as bitbox02_system
from ..communication.generated import backup_commands_pb2 as backup
from ..communication.generated import common_pb2 as common
from ..communication.generated import keystore_pb2 as keystore
from ..communication.generated import antiklepto_pb2 as antiklepto
import google.protobuf.empty_pb2

# pylint: disable=unused-import
# We export it in __init__.py
from ..communication.generated import system_pb2 as system
except ModuleNotFoundError:
print("Run `make py` to generate the protobuf messages")
sys.exit()

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

def bip85_bip39(self) -> None:
"""Invokes the BIP85-BIP39 workflow on the device"""
self._require_atleast(semver.VersionInfo(9, 18, 0))

# pylint: disable=no-member
request = hww.Request()
request.bip85.CopyFrom(
keystore.BIP85Request(
bip39=google.protobuf.empty_pb2.Empty(),
)
)
response = self._msg_query(request, expected_response="bip85").bip85
assert response.WhichOneof("app") == "bip39"

def bip85_ln(self) -> bytes:
"""
Generates and returns a mnemonic for a hot Lightning wallet from the device using BIP-85.
"""
self._require_atleast(semver.VersionInfo(9, 17, 0))

# Only account_number=0 is allowed for now.
account_number = 0

# pylint: disable=no-member
request = hww.Request()
request.bip85.CopyFrom(
keystore.BIP85Request(
ln=keystore.BIP85Request.AppLn(account_number=account_number),
)
)
response = self._msg_query(request, expected_response="bip85").bip85
assert response.WhichOneof("app") == "ln"
return response.ln

def enable_mnemonic_passphrase(self) -> None:
"""
Enable the bip39 passphrase.
Expand Down Expand Up @@ -753,28 +792,17 @@ def eth_sign(self, transaction: bytes, keypath: Sequence[int], chain_id: int = 1
"""
transaction should be given as a full rlp encoded eth transaction.
"""
nonce, gas_price, gas_limit, recipient, value, data, _, _, _ = rlp.decode(transaction)
request = eth.ETHRequest()
# pylint: disable=no-member
request.sign.CopyFrom(
eth.ETHSignRequest(
coin=self._eth_coin(chain_id),
chain_id=chain_id,
keypath=keypath,
nonce=nonce,
gas_price=gas_price,
gas_limit=gas_limit,
recipient=recipient,
value=value,
data=data,
)
)
is_eip1559 = transaction.startswith(b"\x02")

supports_antiklepto = self.version >= semver.VersionInfo(9, 5, 0)
if supports_antiklepto:
def handle_antiklepto(request: eth.ETHRequest) -> bytes:
host_nonce = os.urandom(32)
if is_eip1559:
request.sign_eip1559.host_nonce_commitment.commitment = antiklepto_host_commit(
host_nonce
)
else:
request.sign.host_nonce_commitment.commitment = antiklepto_host_commit(host_nonce)

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

return signature

if is_eip1559:
self._require_atleast(semver.VersionInfo(9, 16, 0))
(
decoded_chain_id,
nonce,
priority_fee,
max_fee,
gas_limit,
recipient,
value,
data,
_,
_,
_,
) = rlp.decode(transaction[1:])
decoded_chain_id_int = int.from_bytes(decoded_chain_id, byteorder="big")
if decoded_chain_id_int != chain_id:
raise Exception(
f"chainID argument ({chain_id}) does not match chainID encoded in transaction ({decoded_chain_id_int})"
)
request = eth.ETHRequest()
# pylint: disable=no-member
request.sign_eip1559.CopyFrom(
eth.ETHSignEIP1559Request(
chain_id=chain_id,
keypath=keypath,
nonce=nonce,
max_priority_fee_per_gas=priority_fee,
max_fee_per_gas=max_fee,
gas_limit=gas_limit,
recipient=recipient,
value=value,
data=data,
)
)
return handle_antiklepto(request)

nonce, gas_price, gas_limit, recipient, value, data, _, _, _ = rlp.decode(transaction)
request = eth.ETHRequest()
# pylint: disable=no-member
request.sign.CopyFrom(
eth.ETHSignRequest(
coin=self._eth_coin(chain_id),
chain_id=chain_id,
keypath=keypath,
nonce=nonce,
gas_price=gas_price,
gas_limit=gas_limit,
recipient=recipient,
value=value,
data=data,
)
)

supports_antiklepto = self.version >= semver.VersionInfo(9, 5, 0)
if supports_antiklepto:
return handle_antiklepto(request)

return self._eth_msg_query(request, expected_response="sign").sign.signature

def eth_sign_msg(self, msg: bytes, keypath: Sequence[int], chain_id: int = 1) -> bytes:
Expand Down Expand Up @@ -945,7 +1031,10 @@ def get_value(
return value
if typ.type == eth.ETHSignTypedMessageRequest.DataType.UINT:
if isinstance(value, str):
value = int(value)
if value[:2].lower() == "0x":
value = int(value[2:], 16)
else:
value = int(value)
assert isinstance(value, int)
return value.to_bytes(typ.size, "big")
if typ.type == eth.ETHSignTypedMessageRequest.DataType.INT:
Expand Down
36 changes: 25 additions & 11 deletions hwilib/devices/bitbox02_lib/communication/bitbox_api_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,12 @@
from .communication import TransportLayer
from .devices import BITBOX02MULTI, BITBOX02BTC

from .generated import hww_pb2 as hww
from .generated import system_pb2 as system
try:
from .generated import hww_pb2 as hww
from .generated import system_pb2 as system
except ModuleNotFoundError:
print("Run `make py` to generate the protobuf messages")
sys.exit()


HWW_CMD = 0x80 + 0x40 + 0x01
Expand Down Expand Up @@ -522,24 +526,34 @@ class BitBoxCommonAPI:

# pylint: disable=too-many-public-methods,too-many-arguments
def __init__(
self, transport: TransportLayer, device_info: DeviceInfo, noise_config: BitBoxNoiseConfig
self,
transport: TransportLayer,
device_info: Optional[DeviceInfo],
noise_config: BitBoxNoiseConfig,
):
"""
Can raise LibraryVersionOutdatedException. check_min_version() should be called following
the instantiation.
If device_info is None, it is infered using the OP_INFO API call, available since
firmware version v5.0.0.
"""
self.debug = False
serial_number = device_info["serial_number"]

if device_info["product_string"] == BITBOX02MULTI:
self.edition = BitBox02Edition.MULTI
elif device_info["product_string"] == BITBOX02BTC:
self.edition = BitBox02Edition.BTCONLY
if device_info is not None:
version = device_info["serial_number"]
if device_info["product_string"] == BITBOX02MULTI:
edition = BitBox02Edition.MULTI
elif device_info["product_string"] == BITBOX02BTC:
edition = BitBox02Edition.BTCONLY
else:
version, _, edition, _ = self.get_info(transport)

self.version = parse_device_version(serial_number)
if self.version is None:
self.edition = edition
try:
self.version = parse_device_version(version)
except:
transport.close()
raise ValueError(f"Could not parse version from {serial_number}")
raise

# Delete the prelease part, as it messes with the comparison (e.g. 3.0.0-pre < 3.0.0 is
# True, but the 3.0.0-pre has already the same API breaking changes like 3.0.0...).
Expand Down
6 changes: 3 additions & 3 deletions hwilib/devices/bitbox02_lib/communication/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,9 @@ def get_any_bitbox02_bootloader() -> DeviceInfo:
return devices[0]


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

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

0 comments on commit 000e5c5

Please sign in to comment.