Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(devices): add onekey support
Browse files Browse the repository at this point in the history
somebodyLi committed Dec 5, 2023

Verified

This commit was signed with the committer’s verified signature.
somebodyLi Ritchie
1 parent 7fd2cea commit 80ca9df
Showing 7 changed files with 759 additions and 7 deletions.
3 changes: 2 additions & 1 deletion hwilib/devices/__init__.py
Original file line number Diff line number Diff line change
@@ -5,5 +5,6 @@
'digitalbitbox',
'coldcard',
'bitbox02',
'jade'
'jade',
'onekey'
]
362 changes: 362 additions & 0 deletions hwilib/devices/onekey.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,362 @@
# type: ignore
""""
OneKey Devices
**************
"""


import sys
from ..common import Chain
from ..errors import (
DEVICE_NOT_INITIALIZED,
DeviceNotReadyError,
common_err_msgs,
handle_errors,
)
from .trezorlib import protobuf, debuglink
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,
)

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


DEFAULT_MAPPING.register(OnekeyFeatures)

USB_IDS = {(0x1209, 0x4F4A), (0x1209, 0x4F4B), }

ONEKEY_LEGACY = TrezorModel(
name="1",
minimum_version=(2, 11, 0),
vendors=VENDORS,
usb_ids=USB_IDS,
default_mapping=DEFAULT_MAPPING,
)

ONEKEY_TOUCH = TrezorModel(
name="T",
minimum_version=(4, 2, 0),
vendors=VENDORS,
usb_ids=USB_IDS,
default_mapping=DEFAULT_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 _refresh_features(self: object, features: Features) -> None:
"""Update internal fields based on passed-in Features message."""
if not self.model:
self.model = model_by_name(features.model or "1")
if self.model is None:
raise RuntimeError("Unsupported OneKey model")

if features.vendor not in self.model.vendors:
raise RuntimeError("Unsupported device")
self.features = features
self.version = (*map(int, self.features.onekey_version.split(".")), )
self.check_firmware_version(warn_only=True)
if self.features.session_id is not None:
self.session_id = self.features.session_id
self.features.session_id = None

def button_request(self: object, code: Optional[int]) -> None:
if not self.prompt_shown:
print("Please confirm action on your OneKey device", file=sys.stderr)
if not self.always_prompt:
self.prompt_shown = True


# ===============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)
self.client._refresh_features = MethodType(_refresh_features, self.client)
if not isinstance(self.client.ui, debuglink.DebugUI):
self.client.ui.button_request = MethodType(button_request, self.client.ui)
self.type = "OneKey"


def enumerate(
password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN
) -> List[Dict[str, Any]]:
results = []
devs = webusb.WebUsbTransport.enumerate(usb_ids=USB_IDS)
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
15 changes: 15 additions & 0 deletions hwilib/udev/51-onekey.rules
Original file line number Diff line number Diff line change
@@ -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"
49 changes: 45 additions & 4 deletions test/README.md
Original file line number Diff line number Diff line change
@@ -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`.
@@ -21,15 +24,15 @@ It also tests usage with `bitcoind`.
It uses the [Espressif fork of the Qemu emulator](https://github.com/espressif/qemu.git).
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, 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`, and `work/test/bitcoin` respectively.
`setup_environment.sh` will build the Trezor emulator, the Onekey emulator, the Coldcard simulator, the Keepkey emulator, the Digital Bitbox simulator, the Jade emulator, and `bitcoind`.
if run in the `test/` directory, these will be built in `work/test/trezor-firmware`, `work/test/onekey-firmware`, `work/test/firmware`, `work/test/keepkey-firmware`, `work/test/mcu`, 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`, and `--bitcoind`.
These are `--trezor-1`, `--trezor-t`, `--onekey-1`, `--onekey-t`, `--coldcard`, `--keepkey`, `--bitbox01`, `--jade`, 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, 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`, and `test/test_digitalbitbox.py` can be disabled.
`test_trezor.py`, `test_onekey.py`, `test_coldcard.py`, `test_keepkey.py`, `test_jade.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/`.

@@ -73,6 +76,44 @@ $ pipenv install
$ pipenv run script/cibuild
```

## Onekey emulator

### Dependencies

In order to build the Onekey emulator, the [Nix](https://nixos.org) will need to be installed:

```
sh <(curl -L https://nixos.org/nix/install)
```

### 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
$ nix-shell
$ poetry install
$ export EMULATOR=1 DEBUG_LINK=1
$ poetry run script/setup
$ poetry run script/cibuild
```
For the Onekey Touch emulator:
```
$ git checkout touch
$ cd onekey-firmware
$ nix-shell
$ poetry install
$ cd core
$ poetry run make build_unix
```

## Coldcard simulator

### Dependencies
23 changes: 21 additions & 2 deletions test/run_tests.py
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@
from test_digitalbitbox import digitalbitbox_test_suite
from test_keepkey import keepkey_test_suite
from test_jade import jade_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')
@@ -27,6 +28,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')
@@ -53,6 +62,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('--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('--coldcard-path', dest='coldcard_path', help='Path to Coldcar simulator', default='work/firmware/unix/headless.py')
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')
@@ -65,7 +76,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)
parser.set_defaults(trezor_1=None, trezor_t=None, coldcard=None, keepkey=None, bitbox01=None, ledger=None, ledger_legacy=None, jade=None, onekey_1=None, onekey_t=None)

args = parser.parse_args()

@@ -86,6 +97,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
@@ -96,14 +109,16 @@
# 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
args.ledger = False if args.ledger is None else args.ledger
args.ledger_legacy = False if args.ledger_legacy is None else args.ledger_legacy
args.jade = False if args.jade is None else args.jade

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:
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:
# Start bitcoind
bitcoind = Bitcoind.create(args.bitcoind)

@@ -115,6 +130,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:
59 changes: 59 additions & 0 deletions test/setup_environment.sh
Original file line number Diff line number Diff line change
@@ -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
@@ -47,6 +55,8 @@ while [[ $# -gt 0 ]]; do
build_keepkey=1
build_jade=1
build_bitcoind=1
build_onekey_1=1
build_onekey_t=1
shift
;;
esac
@@ -115,6 +125,55 @@ 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 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 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
255 changes: 255 additions & 0 deletions test/test_onekey.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
#! /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 _refresh_features
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

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)
client._refresh_features = MethodType(_refresh_features, 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 + ['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))

0 comments on commit 80ca9df

Please sign in to comment.