Skip to content

Commit

Permalink
Merge #741: Add BitBox02 Simulator
Browse files Browse the repository at this point in the history
3c3a185 bitbox02: add bitbox02 simulator and tests (asi345)
000e5c5 bitbox02 library: Update to v6.3.0 (asi345)

Pull request description:

  Recently, a simulator for BitBox02 was implemented. Therefore, we also wanted to add this to HWI repository for automated tests and ease of reference in the future.

ACKs for top commit:
  achow101:
    ACK 3c3a185

Tree-SHA512: 79dd9f65c126dda11c595f6593b00e96e2bfae3dc72bd84e8c1f2c74133985a6ac0330927eaaba54130e245ed607aca7829c9b7a56671dc193b7e32c279d6c27
  • Loading branch information
achow101 committed Dec 4, 2024
2 parents fedc41c + 3c3a185 commit c820c2f
Show file tree
Hide file tree
Showing 30 changed files with 753 additions and 216 deletions.
7 changes: 7 additions & 0 deletions .github/actions/install-sim/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ runs:
apt-get install -y libusb-1.0-0
tar -xvf mcu.tar.gz
- if: inputs.device == 'bitbox02'
shell: bash
run: |
apt-get update
apt-get install -y libusb-1.0-0 docker.io
tar -xvf bitbox02.tar.gz
- if: inputs.device == 'jade'
shell: bash
run: |
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ jobs:
- { name: 'jade', archive: 'jade', paths: 'test/work/jade/simulator' }
- { name: 'ledger', archive: 'speculos', paths: 'test/work/speculos' }
- { name: 'keepkey', archive: 'keepkey-firmware', paths: 'test/work/keepkey-firmware/bin' }
- { name: 'bitbox02', archive: 'bitbox02', paths: 'test/work/bitbox02-firmware/build-build/bin/simulator' }

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -219,6 +220,7 @@ jobs:
- 'ledger'
- 'ledger-legacy'
- 'keepkey'
- 'bitbox02'
script:
- name: 'Wheel'
install: 'pip install dist/*.whl'
Expand Down Expand Up @@ -289,6 +291,7 @@ jobs:
- 'ledger'
- 'ledger-legacy'
- 'keepkey'
- 'bitbox02'
interface: [ 'library', 'cli', 'stdin' ]

container: python:${{ matrix.python-version }}
Expand Down
9 changes: 9 additions & 0 deletions ci/build_bitbox02.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
docker volume rm bitbox02_volume || true
docker volume create bitbox02_volume
CONTAINER_VERSION=$(curl https://raw.githubusercontent.com/BitBoxSwiss/bitbox02-firmware/master/.containerversion)
docker pull shiftcrypto/firmware_v2:$CONTAINER_VERSION
docker run -i --rm -v bitbox02_volume:/bitbox02-firmware shiftcrypto/firmware_v2:$CONTAINER_VERSION bash -c \
"cd /bitbox02-firmware && \
git clone --recursive https://github.com/BitBoxSwiss/bitbox02-firmware.git . && \
git config --global --add safe.directory ./ && \
make -j simulator"
5 changes: 5 additions & 0 deletions ci/cirrus.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ RUN protoc --version
# docker build -f ci/cirrus.Dockerfile -t hwi_test .
# docker run -it --entrypoint /bin/bash hwi_test
# cd test; poetry run ./run_tests.py --ledger --coldcard --interface=cli --device-only
# For BitBox02:
# docker build -f ci/cirrus.Dockerfile -t hwi_test .
# ./ci/build_bitbox02.sh
# docker run -it -v bitbox02_volume:/test/work/bitbox02-firmware --name hwi --entrypoint /bin/bash hwi_test
# cd test; poetry run ./run_tests.py --bitbox02 --interface=cli --device-only
####################

####################
Expand Down
120 changes: 77 additions & 43 deletions hwilib/devices/bitbox02.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import base64
import builtins
import sys
import socket
from functools import wraps

from .._base58 import decode_check, encode_check
Expand Down Expand Up @@ -79,6 +80,8 @@
BitBoxNoiseConfig,
)

SIMULATOR_PATH = "127.0.0.1:15423"

class BitBox02Error(UnavailableActionError):
def __init__(self, msg: str):
"""
Expand Down Expand Up @@ -178,10 +181,15 @@ def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain
Enumerate all BitBox02 devices. Bootloaders excluded.
"""
result = []
for device_info in devices.get_any_bitbox02s():
path = device_info["path"].decode()
client = Bitbox02Client(path)
client.set_noise_config(SilentNoiseConfig())
devs = [device_info["path"].decode() for device_info in devices.get_any_bitbox02s()]
if allow_emulators:
devs.append(SIMULATOR_PATH)
for path in devs:
client = Bitbox02Client(path=path)
if allow_emulators and client.simulator and not client.simulator.connected:
continue
if path != SIMULATOR_PATH:
client.set_noise_config(SilentNoiseConfig())
d_data: Dict[str, object] = {}
bb02 = None
with handle_errors(common_err_msgs["enumerate"], d_data):
Expand Down Expand Up @@ -252,9 +260,31 @@ def func(*args, **kwargs): # type: ignore
raise exc
except FirmwareVersionOutdatedException as exc:
raise DeviceNotReadyError(str(exc))
except ValueError as e:
raise BadArgumentError(str(e))

return cast(T, func)

class BitBox02Simulator():
def __init__(self) -> None:
self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ip, port = SIMULATOR_PATH.split(":")
self.connected = True
try:
self.client_socket.connect((ip, int(port)))
except:
self.connected = False

def write(self, data: bytes) -> None:
# Messages from client are always prefixed with HID report ID(0x00), which is not expected by the simulator.
self.client_socket.send(data[1:])

def read(self, size: int, timeout_ms: int) -> bytes:
res = self.client_socket.recv(64)
return res

def close(self) -> None:
self.client_socket.close()

# This class extends the HardwareWalletClient for BitBox02 specific things
class Bitbox02Client(HardwareWalletClient):
Expand All @@ -267,56 +297,56 @@ def __init__(self, path: str, password: Optional[str] = None, expert: bool = Fal
"The BitBox02 does not accept a passphrase from the host. Please enable the passphrase option and enter the passphrase on the device during unlock."
)
super().__init__(path, password=password, expert=expert, chain=chain)

hid_device = hid.device()
hid_device.open_path(path.encode())
self.transport = u2fhid.U2FHid(hid_device)
self.simulator = None
self.noise_config: BitBoxNoiseConfig = BitBoxNoiseConfig()

if path != SIMULATOR_PATH:
hid_device = hid.device()
hid_device.open_path(path.encode())
self.transport = u2fhid.U2FHid(hid_device)
self.noise_config = CLINoiseConfig()
else:
self.simulator = BitBox02Simulator()
if self.simulator.connected:
self.transport = u2fhid.U2FHid(self.simulator)
self.device_path = path

# use self.init() to access self.bb02.
self.bb02: Optional[bitbox02.BitBox02] = None

self.noise_config: BitBoxNoiseConfig = CLINoiseConfig()

def set_noise_config(self, noise_config: BitBoxNoiseConfig) -> None:
self.noise_config = noise_config

def init(self, expect_initialized: Optional[bool] = True) -> bitbox02.BitBox02:
if self.bb02 is not None:
return self.bb02

for device_info in devices.get_any_bitbox02s():
if device_info["path"].decode() != self.device_path:
continue

bb02 = bitbox02.BitBox02(
transport=self.transport,
device_info=device_info,
noise_config=self.noise_config,
)
try:
bb02.check_min_version()
except FirmwareVersionOutdatedException as exc:
sys.stderr.write("WARNING: {}\n".format(exc))
raise
self.bb02 = bb02
is_initialized = bb02.device_info()["initialized"]
if expect_initialized is not None:
if expect_initialized:
if not is_initialized:
raise HWWError(
"The BitBox02 must be initialized first.",
DEVICE_NOT_INITIALIZED,
)
elif is_initialized:
raise UnavailableActionError(
"The BitBox02 must be wiped before setup."
bb02 = bitbox02.BitBox02(
transport=self.transport,
# Passing None as device_info means the device will be queried for the relevant device info.
device_info=None,
noise_config=self.noise_config,
)
try:
bb02.check_min_version()
except FirmwareVersionOutdatedException as exc:
sys.stderr.write("WARNING: {}\n".format(exc))
raise
self.bb02 = bb02
is_initialized = bb02.device_info()["initialized"]
if expect_initialized is not None:
if expect_initialized:
if not is_initialized:
raise HWWError(
"The BitBox02 must be initialized first.",
DEVICE_NOT_INITIALIZED,
)
elif is_initialized:
raise UnavailableActionError(
"The BitBox02 must be wiped before setup."
)

return bb02
raise Exception(
"Could not find the hid device info for path {}".format(self.device_path)
)
return bb02

def close(self) -> None:
self.transport.close()
Expand Down Expand Up @@ -883,9 +913,13 @@ def setup_device(

if label:
bb02.set_device_name(label)
if not bb02.set_password():
return False
return bb02.create_backup()
if self.device_path != SIMULATOR_PATH:
if not bb02.set_password():
return False
return bb02.create_backup()
else:
bb02.restore_from_mnemonic()
return True

@bitbox02_exception
def wipe_device(self) -> bool:
Expand Down
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
Loading

0 comments on commit c820c2f

Please sign in to comment.