From be0301076c068d564603ebc4fb99099fe4c8856d Mon Sep 17 00:00:00 2001 From: Ritchie <32901980+somebodyLi@users.noreply.github.com> Date: Mon, 27 Mar 2023 10:14:51 +0800 Subject: [PATCH] feat(devices): add onekey support --- .github/actions/install-sim/action.yml | 9 +- .github/workflows/ci.yml | 6 + hwilib/devices/__init__.py | 3 +- hwilib/devices/keepkey.py | 3 +- hwilib/devices/onekey.py | 347 +++++++++++++++++++++++++ hwilib/devices/trezor.py | 12 +- hwilib/devices/trezorlib/client.py | 26 +- hwilib/udev/51-onekey.rules | 15 ++ test/README.md | 56 +++- test/run_tests.py | 23 +- test/setup_environment.sh | 61 +++++ test/test_onekey.py | 257 ++++++++++++++++++ 12 files changed, 795 insertions(+), 23 deletions(-) create mode 100644 hwilib/devices/onekey.py create mode 100644 hwilib/udev/51-onekey.rules create mode 100755 test/test_onekey.py diff --git a/.github/actions/install-sim/action.yml b/.github/actions/install-sim/action.yml index abfbded13..a57b00562 100644 --- a/.github/actions/install-sim/action.yml +++ b/.github/actions/install-sim/action.yml @@ -17,7 +17,14 @@ runs: apt-get update apt-get install -y libsdl2-image-2.0-0 libusb-1.0-0 tar -xvf trezor-firmware.tar.gz - + + - if: startsWith(inputs.device, 'onekey-') + shell: bash + run: | + apt-get update + apt-get install -y libsdl2-image-2.0-0 libusb-1.0-0 + tar -xvf onekey-firmware.tar.gz + - if: inputs.device == 'coldcard' shell: bash run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bbf858dea..06018e5f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -154,6 +154,8 @@ jobs: - { 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' } + - { name: 'onekey-1', archive: 'onekey-firmware', paths: 'test/work/onekey-firmware' } + - { name: 'onekey-t', archive: 'onekey-firmware', paths: 'test/work/onekey-firmware' } steps: - uses: actions/checkout@v4 @@ -221,6 +223,8 @@ jobs: - 'ledger-legacy' - 'keepkey' - 'bitbox02' + - 'onekey-1' + - 'onekey-t' script: - name: 'Wheel' install: 'pip install dist/*.whl' @@ -292,6 +296,8 @@ jobs: - 'ledger-legacy' - 'keepkey' - 'bitbox02' + - 'onekey-1' + - 'onekey-t' interface: [ 'library', 'cli', 'stdin' ] container: python:${{ matrix.python-version }} diff --git a/hwilib/devices/__init__.py b/hwilib/devices/__init__.py index 77fa0ffba..946a6db58 100644 --- a/hwilib/devices/__init__.py +++ b/hwilib/devices/__init__.py @@ -5,5 +5,6 @@ 'digitalbitbox', 'coldcard', 'bitbox02', - 'jade' + 'jade', + 'onekey' ] diff --git a/hwilib/devices/keepkey.py b/hwilib/devices/keepkey.py index 21845e955..72e572873 100644 --- a/hwilib/devices/keepkey.py +++ b/hwilib/devices/keepkey.py @@ -160,8 +160,7 @@ def __init__(self, path: str, password: Optional[str] = None, expert: bool = Fal if path.startswith("udp"): model.default_mapping.register(KeepkeyDebugLinkState) - super(KeepkeyClient, self).__init__(path, password, expert, chain, KEEPKEY_HID_IDS, KEEPKEY_WEBUSB_IDS, KEEPKEY_SIMULATOR_PATH, model) - self.type = 'Keepkey' + super(KeepkeyClient, self).__init__(path, password, expert, chain, KEEPKEY_HID_IDS, KEEPKEY_WEBUSB_IDS, KEEPKEY_SIMULATOR_PATH, model, device_type="Keepkey") def can_sign_taproot(self) -> bool: """ diff --git a/hwilib/devices/onekey.py b/hwilib/devices/onekey.py new file mode 100644 index 000000000..677cdd895 --- /dev/null +++ b/hwilib/devices/onekey.py @@ -0,0 +1,347 @@ +# type: ignore +"""" +OneKey Devices +************** +""" + + +from ..common import Chain +from ..errors import ( + DEVICE_NOT_INITIALIZED, + DeviceNotReadyError, + common_err_msgs, + handle_errors, +) +from .trezorlib import protobuf +from .trezorlib.transport import ( + udp, + webusb, +) +from .trezor import TrezorClient +from .trezorlib.mapping import DEFAULT_MAPPING +from .trezorlib.messages import ( + BackupType, + Capability, + Features, + SafetyCheckLevel, +) +from types import MethodType +from .trezorlib.models import TrezorModel +from typing import ( + Any, + Dict, + List, + Optional, + Sequence, +) +import copy + +py_enumerate = enumerate # Need to use the enumerate built-in but there's another function already named that + +VENDORS = ("onekey.so", ) + + +class OnekeyFeatures(Features): + MESSAGE_WIRE_TYPE = 17 + FIELDS = { + 1: protobuf.Field("vendor", "string", repeated=False, required=False), + 2: protobuf.Field("major_version", "uint32", repeated=False, required=True), + 3: protobuf.Field("minor_version", "uint32", repeated=False, required=True), + 4: protobuf.Field("patch_version", "uint32", repeated=False, required=True), + 5: protobuf.Field("bootloader_mode", "bool", repeated=False, required=False), + 6: protobuf.Field("device_id", "string", repeated=False, required=False), + 7: protobuf.Field("pin_protection", "bool", repeated=False, required=False), + 8: protobuf.Field( + "passphrase_protection", "bool", repeated=False, required=False + ), + 9: protobuf.Field("language", "string", repeated=False, required=False), + 10: protobuf.Field("label", "string", repeated=False, required=False), + 12: protobuf.Field("initialized", "bool", repeated=False, required=False), + 13: protobuf.Field("revision", "bytes", repeated=False, required=False), + 14: protobuf.Field("bootloader_hash", "bytes", repeated=False, required=False), + 15: protobuf.Field("imported", "bool", repeated=False, required=False), + 16: protobuf.Field("unlocked", "bool", repeated=False, required=False), + 17: protobuf.Field( + "_passphrase_cached", "bool", repeated=False, required=False + ), + 18: protobuf.Field("firmware_present", "bool", repeated=False, required=False), + 19: protobuf.Field("needs_backup", "bool", repeated=False, required=False), + 20: protobuf.Field("flags", "uint32", repeated=False, required=False), + 21: protobuf.Field("model", "string", repeated=False, required=False), + 22: protobuf.Field("fw_major", "uint32", repeated=False, required=False), + 23: protobuf.Field("fw_minor", "uint32", repeated=False, required=False), + 24: protobuf.Field("fw_patch", "uint32", repeated=False, required=False), + 25: protobuf.Field("fw_vendor", "string", repeated=False, required=False), + 27: protobuf.Field("unfinished_backup", "bool", repeated=False, required=False), + 28: protobuf.Field("no_backup", "bool", repeated=False, required=False), + 29: protobuf.Field("recovery_mode", "bool", repeated=False, required=False), + 30: protobuf.Field("capabilities", "Capability", repeated=True, required=False), + 31: protobuf.Field("backup_type", "BackupType", repeated=False, required=False), + 32: protobuf.Field("sd_card_present", "bool", repeated=False, required=False), + 33: protobuf.Field("sd_protection", "bool", repeated=False, required=False), + 34: protobuf.Field( + "wipe_code_protection", "bool", repeated=False, required=False + ), + 35: protobuf.Field("session_id", "bytes", repeated=False, required=False), + 36: protobuf.Field( + "passphrase_always_on_device", "bool", repeated=False, required=False + ), + 37: protobuf.Field( + "safety_checks", "SafetyCheckLevel", repeated=False, required=False + ), + 38: protobuf.Field( + "auto_lock_delay_ms", "uint32", repeated=False, required=False + ), + 39: protobuf.Field( + "display_rotation", "uint32", repeated=False, required=False + ), + 40: protobuf.Field( + "experimental_features", "bool", repeated=False, required=False + ), + 500: protobuf.Field("offset", "uint32", repeated=False, required=False), + 501: protobuf.Field("ble_name", "string", repeated=False, required=False), + 502: protobuf.Field("ble_ver", "string", repeated=False, required=False), + 503: protobuf.Field("ble_enable", "bool", repeated=False, required=False), + 504: protobuf.Field("se_enable", "bool", repeated=False, required=False), + 506: protobuf.Field("se_ver", "string", repeated=False, required=False), + 507: protobuf.Field("backup_only", "bool", repeated=False, required=False), + 508: protobuf.Field("onekey_version", "string", repeated=False, required=False), + 509: protobuf.Field("onekey_serial", "string", repeated=False, required=False), + 510: protobuf.Field( + "bootloader_version", "string", repeated=False, required=False + ), + 511: protobuf.Field("serial_no", "string", repeated=False, required=False), + 519: protobuf.Field( + "boardloader_version", "string", repeated=False, required=False + ), + } + + def __init__( + self, + *, + major_version: "int", + minor_version: "int", + patch_version: "int", + capabilities: Optional[Sequence["Capability"]] = None, + vendor: Optional["str"] = None, + bootloader_mode: Optional["bool"] = None, + device_id: Optional["str"] = None, + pin_protection: Optional["bool"] = None, + passphrase_protection: Optional["bool"] = None, + language: Optional["str"] = None, + label: Optional["str"] = None, + initialized: Optional["bool"] = None, + revision: Optional["bytes"] = None, + bootloader_hash: Optional["bytes"] = None, + imported: Optional["bool"] = None, + unlocked: Optional["bool"] = None, + _passphrase_cached: Optional["bool"] = None, + firmware_present: Optional["bool"] = None, + needs_backup: Optional["bool"] = None, + flags: Optional["int"] = None, + model: Optional["str"] = None, + fw_major: Optional["int"] = None, + fw_minor: Optional["int"] = None, + fw_patch: Optional["int"] = None, + fw_vendor: Optional["str"] = None, + unfinished_backup: Optional["bool"] = None, + no_backup: Optional["bool"] = None, + recovery_mode: Optional["bool"] = None, + backup_type: Optional["BackupType"] = None, + sd_card_present: Optional["bool"] = None, + sd_protection: Optional["bool"] = None, + wipe_code_protection: Optional["bool"] = None, + session_id: Optional["bytes"] = None, + passphrase_always_on_device: Optional["bool"] = None, + safety_checks: Optional["SafetyCheckLevel"] = None, + auto_lock_delay_ms: Optional["int"] = None, + display_rotation: Optional["int"] = None, + experimental_features: Optional["bool"] = None, + offset: Optional["int"] = None, + ble_name: Optional["str"] = None, + ble_ver: Optional["str"] = None, + ble_enable: Optional["bool"] = None, + se_enable: Optional["bool"] = None, + se_ver: Optional["str"] = None, + backup_only: Optional["bool"] = None, + onekey_version: Optional["str"] = None, + onekey_serial: Optional["str"] = None, + bootloader_version: Optional["str"] = None, + serial_no: Optional["str"] = None, + boardloader_version: Optional["str"] = None, + ) -> None: + self.capabilities: Sequence["Capability"] = ( + capabilities if capabilities is not None else [] + ) + self.major_version = major_version + self.minor_version = minor_version + self.patch_version = patch_version + self.vendor = vendor + self.bootloader_mode = bootloader_mode + self.device_id = device_id + self.pin_protection = pin_protection + self.passphrase_protection = passphrase_protection + self.language = language + self.label = label + self.initialized = initialized + self.revision = revision + self.bootloader_hash = bootloader_hash + self.imported = imported + self.unlocked = unlocked + self._passphrase_cached = _passphrase_cached + self.firmware_present = firmware_present + self.needs_backup = needs_backup + self.flags = flags + self.model = model + self.fw_major = fw_major + self.fw_minor = fw_minor + self.fw_patch = fw_patch + self.fw_vendor = fw_vendor + self.unfinished_backup = unfinished_backup + self.no_backup = no_backup + self.recovery_mode = recovery_mode + self.backup_type = backup_type + self.sd_card_present = sd_card_present + self.sd_protection = sd_protection + self.wipe_code_protection = wipe_code_protection + self.session_id = session_id + self.passphrase_always_on_device = passphrase_always_on_device + self.safety_checks = safety_checks + self.auto_lock_delay_ms = auto_lock_delay_ms + self.display_rotation = display_rotation + self.experimental_features = experimental_features + self.offset = offset + self.ble_name = ble_name + self.ble_ver = ble_ver + self.ble_enable = ble_enable + self.se_enable = se_enable + self.se_ver = se_ver + self.backup_only = backup_only + self.onekey_version = onekey_version + self.onekey_serial = onekey_serial + self.bootloader_version = bootloader_version + self.serial_no = serial_no + self.boardloader_version = boardloader_version + +ONEKEY_MAPPING = copy.deepcopy(DEFAULT_MAPPING) +ONEKEY_MAPPING.register(OnekeyFeatures) + +USB_IDS = {(0x1209, 0x4F4A), (0x1209, 0x4F4B), } + +ONEKEY_LEGACY = TrezorModel( + name="1", + internal_name="onekey_1", + minimum_version=(2, 11, 0), + vendors=VENDORS, + usb_ids=USB_IDS, + default_mapping=ONEKEY_MAPPING, +) + +ONEKEY_TOUCH = TrezorModel( + name="T", + internal_name="onekey_t", + minimum_version=(4, 2, 0), + vendors=VENDORS, + usb_ids=USB_IDS, + default_mapping=ONEKEY_MAPPING, +) + +ONEKEYS = (ONEKEY_LEGACY, ONEKEY_TOUCH) + + +def model_by_name(name: str) -> Optional[TrezorModel]: + for model in ONEKEYS: + if model.name == name: + return model + return None + + +# ===============overwrite methods for onekey device begin============ + +def retrieval_version(self: object): + version = (*map(int, self.features.onekey_version.split(".")), ) + return version + +def ensure_model(self: object, features): + assert self.model is not None, "Unsupported OneKey model" + # Correct the correct model + self.model = model_by_name(features.model or "1") + +# ===============overwrite methods for onekey device end============ + +ONEKEY_EMULATOR_PATH = "127.0.0.1:54935" +class OnekeyClient(TrezorClient): + def __init__( + self, + path: str, + password: Optional[str] = None, + expert: bool = False, + chain: Chain = Chain.MAIN, + ) -> None: + super().__init__(path, password, expert, chain, webusb_ids=USB_IDS, sim_path=ONEKEY_EMULATOR_PATH, model=ONEKEY_LEGACY, device_type="OneKey") + self.client.retrieval_version = MethodType(retrieval_version, self.client) + self.client.ensure_model = MethodType(ensure_model, self.client) + +def enumerate( + password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN, allow_emulators: bool = False +) -> List[Dict[str, Any]]: + results = [] + devs = webusb.WebUsbTransport.enumerate(usb_ids=USB_IDS) + if allow_emulators: + devs.extend(udp.UdpTransport.enumerate(path=ONEKEY_EMULATOR_PATH)) + for dev in devs: + d_data: Dict[str, Any] = {} + + d_data["type"] = "onekey" + d_data["path"] = dev.get_path() + client = None + with handle_errors(common_err_msgs["enumerate"], d_data): + client = OnekeyClient(d_data["path"], password) + try: + client._prepare_device() + except TypeError: + continue + if not client.client.features.onekey_version or client.client.features.vendor not in VENDORS: + continue + + d_data["label"] = client.client.features.label + d_data["model"] = "onekey_" + client.client.features.model.lower() + if d_data["path"].startswith("udp:"): + d_data["model"] += "_simulator" + + d_data["needs_pin_sent"] = ( + client.client.features.pin_protection + and not client.client.features.unlocked + ) + if client.client.features.model == "1": + d_data[ + "needs_passphrase_sent" + ] = ( + client.client.features.passphrase_protection + ) # always need the passphrase sent for Trezor One if it has passphrase protection enabled + else: + d_data["needs_passphrase_sent"] = False + if d_data["needs_pin_sent"]: + raise DeviceNotReadyError( + "OneKey is locked. Unlock by using 'promptpin' and then 'sendpin'." + ) + if d_data["needs_passphrase_sent"] and password is None: + d_data["warnings"] = [ + [ + 'Passphrase protection enabled but passphrase was not provided. Using default passphrase of the empty string ("")' + ] + ] + if client.client.features.initialized: + d_data["fingerprint"] = client.get_master_fingerprint().hex() + d_data[ + "needs_passphrase_sent" + ] = False # Passphrase is always needed for the above to have worked, so it's already sent + else: + d_data["error"] = "Not initialized" + d_data["code"] = DEVICE_NOT_INITIALIZED + + if client: + client.close() + + results.append(d_data) + return results diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index bd5a79733..43694c454 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -229,16 +229,17 @@ def get_word(type: messages.WordRequestType) -> str: class PassphraseUI: - def __init__(self, passphrase: str) -> None: + def __init__(self, passphrase: str, device_type: str) -> None: self.passphrase = passphrase self.pinmatrix_shown = False self.prompt_shown = False self.always_prompt = False self.return_passphrase = True + self.device_type = device_type def button_request(self, code: Optional[int]) -> None: if not self.prompt_shown: - print("Please confirm action on your Trezor device", file=sys.stderr) + print(f"Please confirm action on your {self.device_type} device", file=sys.stderr) if not self.always_prompt: self.prompt_shown = True @@ -288,7 +289,8 @@ def __init__( hid_ids: Set[Tuple[int, int]] = HID_IDS, webusb_ids: Set[Tuple[int, int]] = WEBUSB_IDS, sim_path: str = SIMULATOR_PATH, - model: Optional[TrezorModel] = None + model: Optional[TrezorModel] = None, + device_type: str = "Trezor" ) -> None: if password is None: password = "" @@ -301,14 +303,14 @@ def __init__( self.simulator = True self.client.use_passphrase(password) else: - self.client = Trezor(transport=transport, ui=PassphraseUI(password), model=model, _init_device=False) + self.client = Trezor(transport=transport, ui=PassphraseUI(password, device_type), model=model, _init_device=False) # if it wasn't able to find a client, throw an error if not self.client: raise IOError("no Device") self.password = password - self.type = 'Trezor' + self.type = device_type def _prepare_device(self) -> None: self.coin_name = 'Bitcoin' if self.chain == Chain.MAIN else 'Testnet' diff --git a/hwilib/devices/trezorlib/client.py b/hwilib/devices/trezorlib/client.py index 5048340dd..d76d7446f 100644 --- a/hwilib/devices/trezorlib/client.py +++ b/hwilib/devices/trezorlib/client.py @@ -17,7 +17,7 @@ import logging import os import warnings -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any, Optional, Tuple from mnemonic import Mnemonic @@ -259,25 +259,31 @@ def call(self, msg: "MessageType", check_fw: bool = True) -> "MessageType": raise exceptions.TrezorFailure(resp) else: return resp + + def retrieval_version(self) -> Tuple[int, int, int]: - def _refresh_features(self, features: messages.Features) -> None: - """Update internal fields based on passed-in Features message.""" - + version = ( + self.features.major_version, + self.features.minor_version, + self.features.patch_version, + ) + return version + + def ensure_model(self, features): if not self.model: # Trezor Model One bootloader 1.8.0 or older does not send model name self.model = models.by_name(features.model or "1") if self.model is None: raise RuntimeError("Unsupported Trezor model") + + def _refresh_features(self, features: messages.Features) -> None: + """Update internal fields based on passed-in Features message.""" + self.ensure_model(features) if features.vendor not in self.model.vendors: raise RuntimeError("Unsupported device") - self.features = features - self.version = ( - self.features.major_version, - self.features.minor_version, - self.features.patch_version, - ) + self.version = self.retrieval_version() self.check_firmware_version(warn_only=True) if self.features.session_id is not None: self.session_id = self.features.session_id diff --git a/hwilib/udev/51-onekey.rules b/hwilib/udev/51-onekey.rules new file mode 100644 index 000000000..63ec567e0 --- /dev/null +++ b/hwilib/udev/51-onekey.rules @@ -0,0 +1,15 @@ +# OneKey: Hold your own key +# https://onekey.so/ +# +# Put this file into /etc/udev/rules.d +# +# If you are creating a distribution package, +# put this into /usr/lib/udev/rules.d or /lib/udev/rules.d +# depending on your distribution + +# OneKey +# onekey boot +SUBSYSTEM=="usb", ATTR{idVendor}=="1209", ATTR{idProduct}=="4F4A", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="onekey%n" +# onekey firmware +SUBSYSTEM=="usb", ATTR{idVendor}=="1209", ATTR{idProduct}=="4F4B", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="onekey%n" +KERNEL=="hidraw*", ATTRS{idVendor}=="1209", ATTRS{idProduct}=="4F4B", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl" diff --git a/test/README.md b/test/README.md index c3efb9262..bc807955c 100644 --- a/test/README.md +++ b/test/README.md @@ -11,6 +11,9 @@ It implements all of the [BIP 174 serialization test vectors](https://github.com - `test_trezor.py` tests the command line interface and the Trezor implementation. It uses the [Trezor One firmware emulator](https://github.com/trezor/trezor-firmware/blob/master/docs/legacy/index.md#local-development-build) and [Trezor Model T firmware emulator](https://github.com/trezor/trezor-firmware/blob/master/docs/core/emulator/index.md). It also tests usage with `bitcoind`. +- `test_onekey.py` tests the command line interface and the Onekey implementation. +It uses the [Onekey Legacy firmware emulator](https://github.com/OnekeyHQ/firmware/blob/bixin_dev/docs/legacy/index.md#local-development-build) and [OneKey Touch emulator](https://github.com/OnekeyHQ/firmware/blob/touch/docs/core/emulator/index.md). +It also tests usage with `bitcoind`. - `test_keepkey.py` tests the command line interface and the Keepkey implementation. It uses the [Keepkey firmware emulator](https://github.com/keepkey/keepkey-firmware/blob/master/docs/Build.md). It also tests usage with `bitcoind`. @@ -27,12 +30,12 @@ It also tests usage with `bitcoind`. `setup_environment.sh` will build the Trezor emulator, the Coldcard simulator, the Keepkey emulator, the Digital Bitbox simulator, the Jade emulator, the BitBox02 simulator and `bitcoind`. if run in the `test/` directory, these will be built in `work/test/trezor-firmware`, `work/test/firmware`, `work/test/keepkey-firmware`, `work/test/mcu`, `work/test/bitbox02-firmware` and `work/test/bitcoin` respectively. In order to build each simulator/emulator, you will need to use command line arguments. -These are `--trezor-1`, `--trezor-t`, `--coldcard`, `--keepkey`, `--bitbox01`, `--jade`, `--bitbox02` and `--bitcoind`. +These are `--trezor-1`, `--trezor-t`, `--onekey-1`, `--onekey-t`, `--coldcard`, `--keepkey`, `--bitbox01`, `--jade`, `--bitbox02` and `--bitcoind`. If an environment variable is not present or not set, then the simulator/emulator or bitcoind that it guards will not be built. `run_tests.py` runs the tests. If run from the `test/` directory, it will be able to find the Trezor emulator, Coldcard simulator, Keepkey emulator, Digital Bitbox simulator, Jade emulator, BitBox02 simulator and bitcoind. Otherwise the paths to those will need to be specified on the command line. -`test_trezor.py`, `test_coldcard.py`, `test_keepkey.py`, `test_jade.py`, `test_bitbox02.py` and `test/test_digitalbitbox.py` can be disabled. +`test_trezor.py`, `test_onekey.py`, `test_coldcard.py`, `test_keepkey.py`, `test_jade.py`, `test_bitbox02.py` and `test/test_digitalbitbox.py` can be disabled. If you are building the Trezor emulator, the Coldcard simulator, the Keepkey emulator, the Jade emulator, the Digital Bitbox simulator, and `bitcoind` without `setup_environment.sh`, then you will need to make `work/` inside of `test/`. @@ -76,6 +79,55 @@ $ pipenv install $ pipenv run script/cibuild ``` +## Onekey emulator + +### Dependencies + +In order to build the Onekey emulator, the following packages will need to be installed: + +``` +build-essential curl git python3 python3-pip libsdl2-dev libsdl2-image-dev gcc-arm-none-eabi libnewlib-arm-none-eabi gcc-multilib +``` + +For onekey Touch `Rust` needs to be installed: + +``` +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` +### Building + +Clone the repository: + +``` +$ git clone --recursive https://github.com/OneKeyHQ/firmware.git onekey-firmware +``` + +For the Onekey Legacy firmware emulator: + +``` +$ git checkout bixin_dev +$ cd onekey-firmware +$ poetry install +$ export EMULATOR=1 DEBUG_LINK=1 +$ poetry run script/setup +$ poetry run script/cibuild +``` + +For the Onekey Touch emulator: + +``` +$ rustup update +$ rustup toolchain uninstall nightly +$ rustup toolchain install nightly +$ rustup default nightly +$ cd onekey-firmware +$ git checkout touch +$ git submodule update --init --recursive vendor/lvgl_mp +$ poetry install +$ cd core +$ poetry run make build_unix +``` + ## Coldcard simulator ### Dependencies diff --git a/test/run_tests.py b/test/run_tests.py index cd502a6c0..577c20d19 100755 --- a/test/run_tests.py +++ b/test/run_tests.py @@ -17,6 +17,7 @@ from test_keepkey import keepkey_test_suite from test_jade import jade_test_suite from test_bitbox02 import bitbox02_test_suite +from test_onekey import onekey_test_suite from test_udevrules import TestUdevRulesInstaller parser = argparse.ArgumentParser(description='Setup the testing environment and run automated tests') @@ -28,6 +29,14 @@ trezor_t_group.add_argument('--no-trezor-t', dest='trezor_t', help='Do not run Trezor T test with emulator', action='store_false') trezor_t_group.add_argument('--trezor-t', dest='trezor_t', help='Run Trezor T test with emulator', action='store_true') +onekey_group = parser.add_mutually_exclusive_group() +onekey_group.add_argument('--no-onekey-1', dest='onekey_1', help='Do not run OneKey Legacy test with emulator', action='store_false') +onekey_group.add_argument('--onekey-1', dest='onekey_1', help='Run OneKey Legacy test with emulator', action='store_true') + +onekey_t_group = parser.add_mutually_exclusive_group() +onekey_t_group.add_argument('--no-onekey-t', dest='onekey_t', help='Do not run OneKey Touch test with emulator', action='store_false') +onekey_t_group.add_argument('--onekey-t', dest='onekey_t', help='Run OneKey Touch test with emulator', action='store_true') + coldcard_group = parser.add_mutually_exclusive_group() coldcard_group.add_argument('--no-coldcard', dest='coldcard', help='Do not run Coldcard test with simulator', action='store_false') coldcard_group.add_argument('--coldcard', dest='coldcard', help='Run Coldcard test with simulator', action='store_true') @@ -59,6 +68,8 @@ parser.add_argument('--trezor-1-path', dest='trezor_1_path', help='Path to Trezor 1 emulator', default='work/trezor-firmware/legacy/firmware/trezor.elf') parser.add_argument('--trezor-t-path', dest='trezor_t_path', help='Path to Trezor T emulator', default='work/trezor-firmware/core/emu.sh') parser.add_argument('--coldcard-path', dest='coldcard_path', help='Path to Coldcard simulator', default='work/firmware/unix/simulator.py') +parser.add_argument('--onekey-1-path', dest='onekey_1_path', help='Path to OneKey Legacy emulator', default='work/onekey-firmware/legacy/firmware/onekey_emu.elf') +parser.add_argument('--onekey-t-path', dest='onekey_t_path', help='Path to OneKey Touch emulator', default='work/onekey-firmware/core/emu.sh') parser.add_argument('--keepkey-path', dest='keepkey_path', help='Path to Keepkey emulator', default='work/keepkey-firmware/bin/kkemu') parser.add_argument('--bitbox01-path', dest='bitbox01_path', help='Path to Digital Bitbox simulator', default='work/mcu/build/bin/simulator') parser.add_argument('--ledger-path', dest='ledger_path', help='Path to Ledger emulator', default='work/speculos/speculos.py') @@ -71,7 +82,7 @@ parser.add_argument("--device-only", help="Only run device tests", action="store_true") -parser.set_defaults(trezor_1=None, trezor_t=None, coldcard=None, keepkey=None, bitbox01=None, ledger=None, ledger_legacy=None, jade=None, bitbox02=None) +parser.set_defaults(trezor_1=None, trezor_t=None, coldcard=None, keepkey=None, bitbox01=None, ledger=None, ledger_legacy=None, jade=None, bitbox02=None, onekey_1=None, onekey_t=None) args = parser.parse_args() @@ -92,6 +103,8 @@ # Default all true unless overridden args.trezor_1 = True if args.trezor_1 is None else args.trezor_1 args.trezor_t = True if args.trezor_t is None else args.trezor_t + args.onekey_1 = True if args.onekey_1 is None else args.onekey_1 + args.onekey_t = True if args.onekey_t is None else args.onekey_t args.coldcard = True if args.coldcard is None else args.coldcard args.keepkey = True if args.keepkey is None else args.keepkey args.bitbox01 = True if args.bitbox01 is None else args.bitbox01 @@ -103,6 +116,8 @@ # Default all false unless overridden args.trezor_1 = False if args.trezor_1 is None else args.trezor_1 args.trezor_t = False if args.trezor_t is None else args.trezor_t + args.onekey_1 = False if args.onekey_1 is None else args.onekey_1 + args.onekey_t = False if args.onekey_t is None else args.onekey_t args.coldcard = False if args.coldcard is None else args.coldcard args.keepkey = False if args.keepkey is None else args.keepkey args.bitbox01 = False if args.bitbox01 is None else args.bitbox01 @@ -111,7 +126,7 @@ args.jade = False if args.jade is None else args.jade args.bitbox02 = False if args.bitbox02 is None else args.bitbox02 -if args.trezor_1 or args.trezor_t or args.coldcard or args.ledger or args.ledger_legacy or args.keepkey or args.bitbox01 or args.jade or args.bitbox02: +if args.trezor_1 or args.trezor_t or args.onekey_1 or args.onekey_t or args.coldcard or args.ledger or args.ledger_legacy or args.keepkey or args.bitbox01 or args.jade or args.bitbox02: # Start bitcoind bitcoind = Bitcoind.create(args.bitcoind) @@ -123,6 +138,10 @@ success &= trezor_test_suite(args.trezor_1_path, bitcoind, args.interface, '1') if success and args.trezor_t: success &= trezor_test_suite(args.trezor_t_path, bitcoind, args.interface, 't') + if success and args.onekey_1: + success &= onekey_test_suite(args.onekey_1_path, bitcoind, args.interface, '1') + if success and args.onekey_t: + success &= onekey_test_suite(args.onekey_t_path, bitcoind, args.interface, 't') if success and args.keepkey: success &= keepkey_test_suite(args.keepkey_path, bitcoind, args.interface) if success and args.ledger: diff --git a/test/setup_environment.sh b/test/setup_environment.sh index d4f1463d7..7fd94481c 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -10,6 +10,14 @@ while [[ $# -gt 0 ]]; do build_trezor_t=1 shift ;; + --onekey-1) + build_onekey_1=1 + shift + ;; + --onekey-t) + build_onekey_t=1 + shift + ;; --coldcard) build_coldcard=1 shift @@ -52,6 +60,8 @@ while [[ $# -gt 0 ]]; do build_jade=1 build_bitbox02=1 build_bitcoind=1 + build_onekey_1=1 + build_onekey_t=1 shift ;; esac @@ -123,6 +133,57 @@ if [[ -n ${build_trezor_1} || -n ${build_trezor_t} ]]; then cd .. fi +if [[ -n ${build_onekey_1} || -n ${build_onekey_t} ]]; then + # Clone onekey-firmware if it doesn't exist, or update it if it does + if [ ! -d "onekey-firmware" ]; then + git clone --recursive https://github.com/OneKeyHQ/firmware.git onekey-firmware + cd onekey-firmware + else + cd onekey-firmware + git fetch + fi + git config pull.rebase true + # # Remove .venv so that poetry can symlink everything correctly + find . -type d -name ".venv" -exec rm -rf {} + + + if [[ -n ${build_onekey_1} ]]; then + # Build trezor one emulator. This is pretty fast, so rebuilding every time is ok + # But there should be some caching that makes this faster + git checkout bixin_dev + git checkout . + git pull origin bixin_dev + poetry lock --no-update + poetry install + poetry run pip install protobuf==3.20.0 + export EMULATOR=1 DEBUG_LINK=1 TREZOR_TRANSPORT_V1=1 + poetry run legacy/script/setup + poetry run legacy/script/cibuild + # Delete any emulator.img file + find . -name "emulator.img" -exec rm {} \; + fi + + if [[ -n ${build_onekey_t} ]]; then + rustup update + rustup toolchain uninstall nightly + rustup toolchain install nightly + rustup default nightly + # Build trezor t emulator. This is pretty fast, so rebuilding every time is ok + # But there should be some caching that makes this faster + git checkout touch + git checkout . + git pull origin touch + git submodule update --init --recursive vendor/lvgl_mp + poetry lock --no-update + poetry install + cd core + poetry run make build_unix + # Delete any emulator.img file + find . -name "onekey.flash" -exec rm {} \; + cd .. + fi + cd .. +fi + if [[ -n ${build_coldcard} ]]; then # Clone coldcard firmware if it doesn't exist, or update it if it does coldcard_setup_needed=false diff --git a/test/test_onekey.py b/test/test_onekey.py new file mode 100755 index 000000000..eec24df09 --- /dev/null +++ b/test/test_onekey.py @@ -0,0 +1,257 @@ +#! /usr/bin/env python3 + +import argparse +import atexit +import json +import os +import shlex +import signal +import socket +import subprocess +import sys +import time +import unittest + +from hwilib.devices.trezorlib.transport.udp import UdpTransport +from hwilib.devices.trezorlib.debuglink import TrezorClientDebugLink, load_device_by_mnemonic +from hwilib.devices.trezorlib import device +from hwilib.devices.onekey import retrieval_version, ensure_model, ONEKEY_LEGACY +from test_device import ( + Bitcoind, + DeviceEmulator, + DeviceTestCase, + TestDeviceConnect, + TestGetKeypool, + TestGetDescriptors, + TestDisplayAddress, + TestSignMessage, + TestSignTx, +) + +from hwilib._cli import process_commands + +from types import MethodType + +ONEKEY_MODELS = {'1', 't'} + +def get_pin(self, code=None): + if self.pin: + return self.debuglink.encode_pin(self.pin) + else: + return self.debuglink.read_pin_encoded() + +DEFAULT_UDP_PORT = 54935 + +class OnkeyEmulator(DeviceEmulator): + def __init__(self, path, model): + assert model in ONEKEY_MODELS + self.emulator_path = path + self.emulator_proc = None + self.model = model + self.emulator_log = None + try: + os.unlink('onekey-{}-emulator.stdout'.format(self.model)) + except FileNotFoundError: + pass + self.type = f"onekey_{model}" + self.path = "udp:127.0.0.1:54935" + self.fingerprint = '95d8f670' + self.master_xpub = "tpubDCknDegFqAdP4V2AhHhs635DPe8N1aTjfKE9m2UFbdej8zmeNbtqDzK59SxnsYSRSx5uS3AujbwgANUiAk4oHmDNUKoGGkWWUY6c48WgjEx" + self.password = "" + self.supports_ms_display = True + self.supports_xpub_ms_display = True + self.supports_unsorted_ms = True + self.supports_taproot = True + self.strict_bip48 = True + self.include_xpubs = False + self.supports_device_multiple_multisig = True + self.supports_legacy = True + + def start(self): + super().start() + self.emulator_log = open('onekey-{}-emulator.stdout'.format(self.model), 'a') + # Start the Onekey emulator + self.emulator_proc = subprocess.Popen(['./' + os.path.basename(self.emulator_path)], cwd=os.path.dirname(self.emulator_path), stdout=self.emulator_log, env={'SDL_VIDEODRIVER': 'dummy', 'PYOPT': '0'}, shell=True, preexec_fn=os.setsid) + # Wait for emulator to be up + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.connect(('127.0.0.1', DEFAULT_UDP_PORT)) + sock.settimeout(0) + while True: + try: + time.sleep(1) + sock.sendall(b"PINGPING") + r = sock.recv(8) + if r == b"PONGPONG": + break + except Exception: + time.sleep(1) + # Setup the emulator + wirelink = UdpTransport.enumerate("127.0.0.1:54935")[0] + client = TrezorClientDebugLink(wirelink, model=ONEKEY_LEGACY) + client.retrieval_version = MethodType(retrieval_version, client) + client.ensure_model = MethodType(ensure_model, client) + client.init_device() + device.wipe(client) + load_device_by_mnemonic(client=client, mnemonic='alcohol woman abuse must during monitor noble actual mixed trade anger aisle', pin='', passphrase_protection=False, label='test') # From Trezor device tests + atexit.register(self.stop) + return client + + def stop(self): + super().stop() + if self.emulator_proc.poll() is None: + os.killpg(os.getpgid(self.emulator_proc.pid), signal.SIGTERM) + os.waitpid(self.emulator_proc.pid, 0) + + # Clean up emulator image + if self.model == 't': + emulator_img = "/var/tmp/onekey.flash" + else: # self.model == '1' + emulator_img = os.path.dirname(self.emulator_path) + "/emulator.img" + + if os.path.isfile(emulator_img): + os.unlink(emulator_img) + + if self.emulator_log is not None: + self.emulator_log.close() + self.emulator_log = None + + # Wait a second for everything to be cleaned up before going to the next test + time.sleep(1) + + atexit.unregister(self.stop) + +class OnekeyTestCase(unittest.TestCase): + def __init__(self, emulator, interface='library', methodName='runTest'): + super(OnekeyTestCase, self).__init__(methodName) + self.emulator = emulator + self.interface = interface + + @staticmethod + def parameterize(testclass, emulator, interface='library'): + testloader = unittest.TestLoader() + testnames = testloader.getTestCaseNames(testclass) + suite = unittest.TestSuite() + for name in testnames: + suite.addTest(testclass(emulator, interface, name)) + return suite + + def do_command(self, args): + cli_args = [] + for arg in args: + cli_args.append(shlex.quote(arg)) + if self.interface == 'cli': + proc = subprocess.Popen(['hwi ' + ' '.join(cli_args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True) + result = proc.communicate() + return json.loads(result[0].decode()) + elif self.interface == 'bindist': + proc = subprocess.Popen(['../dist/hwi ' + ' '.join(cli_args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True) + result = proc.communicate() + return json.loads(result[0].decode()) + elif self.interface == 'stdin': + input_str = '\n'.join(args) + '\n' + proc = subprocess.Popen(['hwi', '--stdin'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) + result = proc.communicate(input_str.encode()) + return json.loads(result[0].decode()) + else: + return process_commands(args) + + def __str__(self): + return 'onekey_{}: {}'.format(self.emulator.model, super().__str__()) + + def __repr__(self): + return 'onekey_{}: {}'.format(self.emulator.model, super().__repr__()) + + def setUp(self): + self.client = self.emulator.start() + + def tearDown(self): + self.emulator.stop() + +# OneKey specific getxpub test because this requires device specific thing to set xprvs +class TestOnekeyGetxpub(OnekeyTestCase): + def test_getxpub(self): + with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data/bip32_vectors.json'), encoding='utf-8') as f: + vectors = json.load(f) + for vec in vectors: + with self.subTest(vector=vec): + # Setup with mnemonic + device.wipe(self.client) + load_device_by_mnemonic(client=self.client, mnemonic=vec['mnemonic'], pin='', passphrase_protection=False, label='test', language='english') + + # Test getmasterxpub + gmxp_res = self.do_command(['-t', 'onekey', '-d', "udp:127.0.0.1:54935", 'getmasterxpub', "--addr-type", "legacy"]) + self.assertEqual(gmxp_res['xpub'], vec['master_xpub']) + + # Test the path derivs + for path_vec in vec['vectors']: + gxp_res = self.do_command(['-t', 'onekey', '-d', "udp:127.0.0.1:54935", 'getxpub', path_vec['path']]) + self.assertEqual(gxp_res['xpub'], path_vec['xpub']) + + def test_expert_getxpub(self): + result = self.do_command(['-t', 'onekey', '-d', "udp:127.0.0.1:54935", '--expert', 'getxpub', 'm/44h/0h/0h/3']) + self.assertEqual(result['xpub'], 'xpub6FMafWAi3n3ET2rU5yQr16UhRD1Zx4dELmcEw3NaYeBaNnipcr2zjzYp1sNdwR3aTN37hxAqRWQ13AWUZr6L9jc617mU6EvgYXyBjXrEhgr') + self.assertFalse(result['testnet']) + self.assertFalse(result['private']) + self.assertEqual(result['depth'], 4) + self.assertEqual(result['parent_fingerprint'], 'f7e318db') + self.assertEqual(result['child_num'], 3) + self.assertEqual(result['chaincode'], '95a7fb33c4f0896f66045cd7f45ed49a9e72372d2aed204ad0149c39b7b17905') + self.assertEqual(result['pubkey'], '022e6d9c18e5a837e802fb09abe00f787c8ccb0fc489c6ec5dc2613d930efd7eae') + +class TestOnekeyLabel(OnekeyTestCase): + def setUp(self): + self.client = self.emulator.start() + self.dev_args = ['-t', 'onekey', '-d', "udp:127.0.0.1:54935"] + + def test_label(self): + result = self.do_command(self.dev_args + ["--emulators", "enumerate"]) + for dev in result: + if dev['type'] == 'onekey' and dev['path'] == "udp:127.0.0.1:54935": + self.assertEqual(dev['label'], 'test') + break + else: + self.fail("Did not enumerate device") + +def onekey_test_suite(emulator, bitcoind, interface, model): + assert model in ONEKEY_MODELS + # Redirect stderr to /dev/null as it's super spammy + sys.stderr = open(os.devnull, 'w') + + dev_emulator = OnkeyEmulator(emulator, model) + signtx_cases = [ + (["legacy"], ["legacy"], False, True), + (["segwit"], ["segwit"], False, True), + (["tap"], [], False, True), + (["legacy", "segwit"], ["legacy", "segwit"], False, True), + (["legacy", "segwit", "tap"], ["legacy", "segwit"], False, True), + ] + # Generic Device tests + suite = unittest.TestSuite() + suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, bitcoind, emulator=dev_emulator, interface=interface, detect_type="onekey")) + suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, bitcoind, emulator=dev_emulator, interface=interface, detect_type=f"onekey_{model}_simulator")) + suite.addTest(DeviceTestCase.parameterize(TestGetDescriptors, bitcoind, emulator=dev_emulator, interface=interface)) + suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, bitcoind, emulator=dev_emulator, interface=interface)) + if model == 't': + suite.addTest(DeviceTestCase.parameterize(TestSignTx, bitcoind, emulator=dev_emulator, interface=interface, signtx_cases=signtx_cases)) + suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, bitcoind, emulator=dev_emulator, interface=interface)) + suite.addTest(DeviceTestCase.parameterize(TestSignMessage, bitcoind, emulator=dev_emulator, interface=interface)) + suite.addTest(OnekeyTestCase.parameterize(TestOnekeyLabel, emulator=dev_emulator, interface=interface)) + suite.addTest(OnekeyTestCase.parameterize(TestOnekeyGetxpub, emulator=dev_emulator, interface=interface)) + result = unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite) + sys.stderr = sys.__stderr__ + return result.wasSuccessful() + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Test Onekey implementation') + parser.add_argument('emulator', help='Path to the Onekey emulator') + parser.add_argument('bitcoind', help='Path to bitcoind binary') + parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli', 'bindist'], default='library') + group = parser.add_argument_group() + group.add_argument('--model_1', help='The emulator is for the Onekey legacy', action='store_const', const='1', dest='model') + group.add_argument('--model_t', help='The emulator is for the Onekey Touch', action='store_const', const='t', dest='model') + args = parser.parse_args() + + # Start bitcoind + bitcoind = Bitcoind.create(args.bitcoind) + + sys.exit(not onekey_test_suite(args.emulator, bitcoind, args.interface, args.model))