Skip to content

Commit c820c2f

Browse files
committed
Merge #741: Add BitBox02 Simulator
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
2 parents fedc41c + 3c3a185 commit c820c2f

30 files changed

+753
-216
lines changed

.github/actions/install-sim/action.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ runs:
3838
apt-get install -y libusb-1.0-0
3939
tar -xvf mcu.tar.gz
4040
41+
- if: inputs.device == 'bitbox02'
42+
shell: bash
43+
run: |
44+
apt-get update
45+
apt-get install -y libusb-1.0-0 docker.io
46+
tar -xvf bitbox02.tar.gz
47+
4148
- if: inputs.device == 'jade'
4249
shell: bash
4350
run: |

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ jobs:
153153
- { name: 'jade', archive: 'jade', paths: 'test/work/jade/simulator' }
154154
- { name: 'ledger', archive: 'speculos', paths: 'test/work/speculos' }
155155
- { name: 'keepkey', archive: 'keepkey-firmware', paths: 'test/work/keepkey-firmware/bin' }
156+
- { name: 'bitbox02', archive: 'bitbox02', paths: 'test/work/bitbox02-firmware/build-build/bin/simulator' }
156157

157158
steps:
158159
- uses: actions/checkout@v4
@@ -219,6 +220,7 @@ jobs:
219220
- 'ledger'
220221
- 'ledger-legacy'
221222
- 'keepkey'
223+
- 'bitbox02'
222224
script:
223225
- name: 'Wheel'
224226
install: 'pip install dist/*.whl'
@@ -289,6 +291,7 @@ jobs:
289291
- 'ledger'
290292
- 'ledger-legacy'
291293
- 'keepkey'
294+
- 'bitbox02'
292295
interface: [ 'library', 'cli', 'stdin' ]
293296

294297
container: python:${{ matrix.python-version }}

ci/build_bitbox02.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
docker volume rm bitbox02_volume || true
2+
docker volume create bitbox02_volume
3+
CONTAINER_VERSION=$(curl https://raw.githubusercontent.com/BitBoxSwiss/bitbox02-firmware/master/.containerversion)
4+
docker pull shiftcrypto/firmware_v2:$CONTAINER_VERSION
5+
docker run -i --rm -v bitbox02_volume:/bitbox02-firmware shiftcrypto/firmware_v2:$CONTAINER_VERSION bash -c \
6+
"cd /bitbox02-firmware && \
7+
git clone --recursive https://github.com/BitBoxSwiss/bitbox02-firmware.git . && \
8+
git config --global --add safe.directory ./ && \
9+
make -j simulator"

ci/cirrus.Dockerfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ RUN protoc --version
5959
# docker build -f ci/cirrus.Dockerfile -t hwi_test .
6060
# docker run -it --entrypoint /bin/bash hwi_test
6161
# cd test; poetry run ./run_tests.py --ledger --coldcard --interface=cli --device-only
62+
# For BitBox02:
63+
# docker build -f ci/cirrus.Dockerfile -t hwi_test .
64+
# ./ci/build_bitbox02.sh
65+
# docker run -it -v bitbox02_volume:/test/work/bitbox02-firmware --name hwi --entrypoint /bin/bash hwi_test
66+
# cd test; poetry run ./run_tests.py --bitbox02 --interface=cli --device-only
6267
####################
6368

6469
####################

hwilib/devices/bitbox02.py

Lines changed: 77 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import base64
2020
import builtins
2121
import sys
22+
import socket
2223
from functools import wraps
2324

2425
from .._base58 import decode_check, encode_check
@@ -79,6 +80,8 @@
7980
BitBoxNoiseConfig,
8081
)
8182

83+
SIMULATOR_PATH = "127.0.0.1:15423"
84+
8285
class BitBox02Error(UnavailableActionError):
8386
def __init__(self, msg: str):
8487
"""
@@ -178,10 +181,15 @@ def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain
178181
Enumerate all BitBox02 devices. Bootloaders excluded.
179182
"""
180183
result = []
181-
for device_info in devices.get_any_bitbox02s():
182-
path = device_info["path"].decode()
183-
client = Bitbox02Client(path)
184-
client.set_noise_config(SilentNoiseConfig())
184+
devs = [device_info["path"].decode() for device_info in devices.get_any_bitbox02s()]
185+
if allow_emulators:
186+
devs.append(SIMULATOR_PATH)
187+
for path in devs:
188+
client = Bitbox02Client(path=path)
189+
if allow_emulators and client.simulator and not client.simulator.connected:
190+
continue
191+
if path != SIMULATOR_PATH:
192+
client.set_noise_config(SilentNoiseConfig())
185193
d_data: Dict[str, object] = {}
186194
bb02 = None
187195
with handle_errors(common_err_msgs["enumerate"], d_data):
@@ -252,9 +260,31 @@ def func(*args, **kwargs): # type: ignore
252260
raise exc
253261
except FirmwareVersionOutdatedException as exc:
254262
raise DeviceNotReadyError(str(exc))
263+
except ValueError as e:
264+
raise BadArgumentError(str(e))
255265

256266
return cast(T, func)
257267

268+
class BitBox02Simulator():
269+
def __init__(self) -> None:
270+
self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
271+
ip, port = SIMULATOR_PATH.split(":")
272+
self.connected = True
273+
try:
274+
self.client_socket.connect((ip, int(port)))
275+
except:
276+
self.connected = False
277+
278+
def write(self, data: bytes) -> None:
279+
# Messages from client are always prefixed with HID report ID(0x00), which is not expected by the simulator.
280+
self.client_socket.send(data[1:])
281+
282+
def read(self, size: int, timeout_ms: int) -> bytes:
283+
res = self.client_socket.recv(64)
284+
return res
285+
286+
def close(self) -> None:
287+
self.client_socket.close()
258288

259289
# This class extends the HardwareWalletClient for BitBox02 specific things
260290
class Bitbox02Client(HardwareWalletClient):
@@ -267,56 +297,56 @@ def __init__(self, path: str, password: Optional[str] = None, expert: bool = Fal
267297
"The BitBox02 does not accept a passphrase from the host. Please enable the passphrase option and enter the passphrase on the device during unlock."
268298
)
269299
super().__init__(path, password=password, expert=expert, chain=chain)
270-
271-
hid_device = hid.device()
272-
hid_device.open_path(path.encode())
273-
self.transport = u2fhid.U2FHid(hid_device)
300+
self.simulator = None
301+
self.noise_config: BitBoxNoiseConfig = BitBoxNoiseConfig()
302+
303+
if path != SIMULATOR_PATH:
304+
hid_device = hid.device()
305+
hid_device.open_path(path.encode())
306+
self.transport = u2fhid.U2FHid(hid_device)
307+
self.noise_config = CLINoiseConfig()
308+
else:
309+
self.simulator = BitBox02Simulator()
310+
if self.simulator.connected:
311+
self.transport = u2fhid.U2FHid(self.simulator)
274312
self.device_path = path
275313

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

279-
self.noise_config: BitBoxNoiseConfig = CLINoiseConfig()
280-
281317
def set_noise_config(self, noise_config: BitBoxNoiseConfig) -> None:
282318
self.noise_config = noise_config
283319

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

288-
for device_info in devices.get_any_bitbox02s():
289-
if device_info["path"].decode() != self.device_path:
290-
continue
291-
292-
bb02 = bitbox02.BitBox02(
293-
transport=self.transport,
294-
device_info=device_info,
295-
noise_config=self.noise_config,
296-
)
297-
try:
298-
bb02.check_min_version()
299-
except FirmwareVersionOutdatedException as exc:
300-
sys.stderr.write("WARNING: {}\n".format(exc))
301-
raise
302-
self.bb02 = bb02
303-
is_initialized = bb02.device_info()["initialized"]
304-
if expect_initialized is not None:
305-
if expect_initialized:
306-
if not is_initialized:
307-
raise HWWError(
308-
"The BitBox02 must be initialized first.",
309-
DEVICE_NOT_INITIALIZED,
310-
)
311-
elif is_initialized:
312-
raise UnavailableActionError(
313-
"The BitBox02 must be wiped before setup."
324+
bb02 = bitbox02.BitBox02(
325+
transport=self.transport,
326+
# Passing None as device_info means the device will be queried for the relevant device info.
327+
device_info=None,
328+
noise_config=self.noise_config,
329+
)
330+
try:
331+
bb02.check_min_version()
332+
except FirmwareVersionOutdatedException as exc:
333+
sys.stderr.write("WARNING: {}\n".format(exc))
334+
raise
335+
self.bb02 = bb02
336+
is_initialized = bb02.device_info()["initialized"]
337+
if expect_initialized is not None:
338+
if expect_initialized:
339+
if not is_initialized:
340+
raise HWWError(
341+
"The BitBox02 must be initialized first.",
342+
DEVICE_NOT_INITIALIZED,
314343
)
344+
elif is_initialized:
345+
raise UnavailableActionError(
346+
"The BitBox02 must be wiped before setup."
347+
)
315348

316-
return bb02
317-
raise Exception(
318-
"Could not find the hid device info for path {}".format(self.device_path)
319-
)
349+
return bb02
320350

321351
def close(self) -> None:
322352
self.transport.close()
@@ -883,9 +913,13 @@ def setup_device(
883913

884914
if label:
885915
bb02.set_device_name(label)
886-
if not bb02.set_password():
887-
return False
888-
return bb02.create_backup()
916+
if self.device_path != SIMULATOR_PATH:
917+
if not bb02.set_password():
918+
return False
919+
return bb02.create_backup()
920+
else:
921+
bb02.restore_from_mnemonic()
922+
return True
889923

890924
@bitbox02_exception
891925
def wipe_device(self) -> bool:

hwilib/devices/bitbox02_lib/bitbox02/__init__.py

Lines changed: 1 addition & 1 deletion
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(

0 commit comments

Comments
 (0)