diff --git a/.flake8 b/.flake8 index 3d38c0f09..f3ca52d7d 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,4 @@ [flake8] -exclude = *.pyc,__pycache__,hwilib/devices/ledger_bitcoin/,hwilib/devices/btchip,hwilib/devices/ckcc/,hwilib/devices/jadepy/,hwilib/devices/trezorlib/,test/work/,hwilib/ui,hwilib/devices/bitbox02_lib +exclude = *.pyc,__pycache__,hwilib/devices/ledger_bitcoin/,hwilib/devices/btchip,hwilib/devices/ckcc/,hwilib/devices/jadepy/,hwilib/devices/trezorlib/,test/work/,hwilib/ui,hwilib/devices/bitbox02_lib,hwilib/devices/cypherock_sdk/app_btc/proto/generated/,hwilib/devices/cypherock_sdk/app_manager/proto/generated/,hwilib/devices/cypherock_sdk/core/encoders/proto/generated/ ignore = E261,E302,E305,E501,E722,W5,E231 per-file-ignores = setup.py:E122 diff --git a/docs/devices/index.rst b/docs/devices/index.rst index 533a6b45f..c61d15882 100644 --- a/docs/devices/index.rst +++ b/docs/devices/index.rst @@ -108,3 +108,5 @@ Device APIs :members: .. automodule:: hwilib.devices.jade :members: +.. automodule:: hwilib.devices.cypherock + :members: diff --git a/hwilib/_gui.py b/hwilib/_gui.py index 389a29e21..9a06ea9b4 100644 --- a/hwilib/_gui.py +++ b/hwilib/_gui.py @@ -417,7 +417,7 @@ def get_client_and_device_info(self, index): self.client.set_noise_config(BitBox02NoiseConfig()) self.ui.setpass_button.setEnabled(self.device_info['type'] != 'bitbox02') - self.ui.signmsg_button.setEnabled(True) + self.ui.signmsg_button.setEnabled(self.device_info['type'] != 'cypherock') self.ui.toggle_passphrase_button.setEnabled(self.device_info['type'] in ('trezor', 'keepkey', 'bitbox02', )) self.get_device_info() diff --git a/hwilib/devices/__init__.py b/hwilib/devices/__init__.py index 77fa0ffba..ced3d1091 100644 --- a/hwilib/devices/__init__.py +++ b/hwilib/devices/__init__.py @@ -5,5 +5,6 @@ 'digitalbitbox', 'coldcard', 'bitbox02', - 'jade' + 'jade', + 'cypherock' ] diff --git a/hwilib/devices/cypherock.py b/hwilib/devices/cypherock.py new file mode 100644 index 000000000..ee4b93cc1 --- /dev/null +++ b/hwilib/devices/cypherock.py @@ -0,0 +1,391 @@ +""" +Cypherock X1 +************ +""" + +from typing import ( + Any, + Dict, + List, + Optional, + Union +) + +from .cypherock_sdk.app_btc import BtcApp +from .cypherock_sdk.app_btc.operations import GetPublicKeyParams, GetXpubsParams, SignTxnParams +from .cypherock_sdk.app_btc.operations.types import SignTxnInputData, SignTxnOutputData, SignTxnTxnData +from .cypherock_sdk.app_manager import ManagerApp +from .cypherock_sdk.hw_hid import get_available_devices, DeviceConnection +from .cypherock_sdk.interfaces import IDevice + +from .. import _base58 as base58 +from ..common import AddressType, Chain, hash256, sha256 +from ..descriptor import MultisigDescriptor +from ..errors import BadArgumentError, DeviceConnectionError, UnavailableActionError, common_err_msgs, handle_errors +from ..hwwclient import HardwareWalletClient +from ..key import ExtendedKey, is_standard_path, parse_path +from ..psbt import PSBT +from .._script import is_p2sh, is_p2wsh, is_witness, parse_multisig +from .._serialize import ser_uint256 + +py_enumerate = enumerate # Need to use the enumerate built-in but there's another function already named that + + +def convert_xpub_to_standard(xpub: str, chain: Chain = Chain.MAIN) -> str: + """Convert any xpub format to standard xpub format.""" + decoded = base58.decode(xpub) + + if chain == Chain.MAIN: + standard_version = ExtendedKey.MAINNET_PUBLIC + else: + standard_version = ExtendedKey.TESTNET_PUBLIC + + decoded_with_standard_version = standard_version + decoded[4:-4] + checksum = hash256(decoded_with_standard_version)[:4] + standard_xpub = base58.encode(decoded_with_standard_version + checksum) + + return standard_xpub + +def get_master_fingerprint_from_device(manager_app: ManagerApp) -> bytes: + device_info = manager_app.get_device_info() + return sha256(device_info.device_serial)[:4] + +class CypherockClient(HardwareWalletClient): + """ + The `CypherockClient` is a `HardwareWalletClient` for interacting with Cypherock X1 devices. + """ + + def __init__(self, path: str, password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN) -> None: + super(CypherockClient, self).__init__(path, password, expert, chain) + + all_devices = get_available_devices() + for device in all_devices: + if device["path"] == path: + self.device: IDevice = device + break + else: + raise DeviceConnectionError(f"Device not found: {path}") + + try: + self.connection = DeviceConnection.connect(self.device) + self.manager_app = ManagerApp.create(self.connection) + self.btc_app = BtcApp.create(self.connection) + self.wallet = self.manager_app.select_wallet().wallet + self.master_fingerprint: Optional(bytes) = None + except Exception as e: + raise DeviceConnectionError(f"Failed to connect to Cypherock X1: {e}") + + def get_master_fingerprint(self) -> bytes: + if self.master_fingerprint is None: + self.master_fingerprint = get_master_fingerprint_from_device(self.manager_app) + return self.master_fingerprint + + def get_pubkey_at_path(self, path: str) -> ExtendedKey: + """ + Get the public key at the BIP 32 derivation path. + The Cypherock X1 only supports BIP 32 derivation paths of at least depth 3. + + :param path: The BIP 32 derivation path + :return: The extended public key + """ + parsed_path = parse_path(path) + if len(parsed_path) < 3: + raise BadArgumentError(f"The Cypherock X1 only supports BIP 32 derivation paths of at least depth 3, but {path} has depth {len(parsed_path)}") + + params = GetXpubsParams(wallet_id=self.wallet.id, derivation_paths=[{"path": parsed_path[0:3]}]) + response = self.btc_app.get_xpubs(params) + standard_xpub = convert_xpub_to_standard(response.xpubs[0]) + return ExtendedKey.deserialize(standard_xpub).derive_pub_path(parsed_path[3:]) + + def sign_tx(self, tx: PSBT) -> PSBT: + """ + Sign a transaction with the Cypherock X1. + :param tx: The transaction to sign + :return: The signed transaction + + :note: It only supports legacy singlesig transactions for now. + """ + master_fp = self.get_master_fingerprint() + + # Determine derivation path from first input + derivation_path = None + for psbt_in in tx.inputs: + for _, keypath in psbt_in.hd_keypaths.items(): + if keypath.fingerprint == master_fp and len(keypath.path) >= 3: + # Extract purpose/coin/account (first 3 elements, hardened) + derivation_path = keypath.path[:3] + break + if derivation_path: + break + + if not derivation_path: + raise ValueError("Could not determine derivation path from PSBT") + + script_addrtype = None + txn_inputs = [] + for i, psbt_in in py_enumerate(tx.inputs): + # Get previous txid and output index + prev_txid = ser_uint256(tx.tx.vin[i].prevout.hash) + prev_index = tx.tx.vin[i].prevout.n + + # Get previous transaction + if psbt_in.non_witness_utxo: + prev_tx = psbt_in.non_witness_utxo + if prev_index >= len(prev_tx.vout): + raise ValueError(f"Invalid prev_index {prev_index} for input {i}, length of vout is {len(prev_tx.vout)}") + prev_txn_bytes = prev_tx.serialize() + prev_out = prev_tx.vout[prev_index] + else: + raise ValueError(f"Input {i} missing full previous transaction data") + + # Get value and script_pub_key from previous output + value = prev_out.nValue + script_pub_key = prev_out.scriptPubKey + sequence = psbt_in.sequence + + p2sh = False + if is_p2sh(script_pub_key): + if len(psbt_in.redeem_script) == 0: + continue + script_pub_key = psbt_in.redeem_script + p2sh = True + + is_wit, wit_ver, _ = is_witness(script_pub_key) + + curr_script_addrtype = AddressType.LEGACY + if is_wit: + if p2sh: + if wit_ver == 0: + curr_script_addrtype = AddressType.SH_WIT + else: + raise BadArgumentError("Cannot have witness v1+ in p2sh") + else: + if wit_ver == 0: + curr_script_addrtype = AddressType.WIT + elif wit_ver == 1: + curr_script_addrtype = AddressType.TAP + else: + continue + + if script_addrtype is None: + script_addrtype = curr_script_addrtype + elif script_addrtype != curr_script_addrtype: + raise BadArgumentError("Cypherock X1 does not support inputs with different script address types yet") + + # Check if P2WSH + if is_p2wsh(script_pub_key): + if len(psbt_in.witness_script) == 0: + continue + script_pub_key = psbt_in.witness_script + + multisig = parse_multisig(script_pub_key) + if multisig: + raise BadArgumentError("Cypherock X1 does not support multisig yet") + + # Extract change_index and address_index from derivation path + change_index = None + address_index = None + for _, keypath in psbt_in.hd_keypaths.items(): + if keypath.fingerprint == master_fp: + if not is_standard_path(keypath.path, curr_script_addrtype, Chain.MAIN): + raise BadArgumentError(f"Cypherock X1 requires BIP 44 standard paths, but {keypath.path} is not a standard path") + change_index = keypath.path[3] + address_index = keypath.path[4] + break + if change_index is None or address_index is None: + raise BadArgumentError("Could not determine change_index and address_index from derivation path") + + txn_inputs.append(SignTxnInputData( + prev_txn_id=prev_txid, + prev_index=prev_index, + value=value, + script_pub_key=script_pub_key, + change_index=change_index, + address_index=address_index, + prev_txn=prev_txn_bytes, + sequence=sequence, + )) + + # Convert PSBT outputs (similar logic for outputs) + txn_outputs = [] + for i, psbt_out in py_enumerate(tx.outputs): + output = tx.tx.vout[i] + value = output.nValue + script_pub_key = output.scriptPubKey + + # Determine if change output (check hd_keypaths) + is_change = False + address_index = None + for _, keypath in psbt_out.hd_keypaths.items(): + if keypath.fingerprint == master_fp: + path = keypath.path + if len(path) >= 5: + # If change index (path[3]) is 1, it's a change output + is_change = path[3] == 1 + if is_change: + address_index = path[4] + + txn_outputs.append(SignTxnOutputData( + value=value, + script_pub_key=script_pub_key, + is_change=is_change, + address_index=address_index, + )) + + # Create transaction data + txn_data = SignTxnTxnData( + inputs=txn_inputs, + outputs=txn_outputs, + locktime=tx.tx.nLockTime if hasattr(tx.tx, 'nLockTime') else None, + hash_type=tx.inputs[0].sighash if tx.inputs and tx.inputs[0].sighash else None, + ) + + # Sign transaction + params = SignTxnParams( + wallet_id=self.wallet.id, + derivation_path=derivation_path, + txn=txn_data, + ) + result = self.btc_app.sign_txn(params) + + # Add signatures back to PSBT + for i, signature in py_enumerate(result.signatures): + psbt_in = tx.inputs[i] + + utxo = None + if psbt_in.witness_utxo: + utxo = psbt_in.witness_utxo + if psbt_in.non_witness_utxo: + assert psbt_in.prev_out is not None + utxo = psbt_in.non_witness_utxo.vout[psbt_in.prev_out] + assert utxo is not None + + is_wit, wit_ver, _ = utxo.is_witness() + + if is_wit and wit_ver >= 1: + # TODO: Deal with script path signatures + # For now, assume key path signature + psbt_in.tap_key_sig = signature[:64] + else: + # Find the pubkey that matches our derivation path + for pubkey, keypath in psbt_in.hd_keypaths.items(): + if keypath.fingerprint == master_fp: + # signature is script sig, extract der from it + der_sig = signature[1: signature[2] + 3] + psbt_in.partial_sigs[pubkey] = der_sig + break + + return tx + + def sign_message(self, message: Union[str, bytes], keypath: str) -> str: + """ + Cypherock X1 does not support signing messages yet. + + :raises UnavailableActionError: this function is unavailable for now + """ + raise UnavailableActionError('The Cypherock X1 does not support signing messages yet') + + def display_singlesig_address( + self, + keypath: str, + addr_type: AddressType, + ) -> str: + parsed_path = parse_path(keypath) + if not is_standard_path(parsed_path, addr_type, self.chain): + raise BadArgumentError(f"Cypherock X1 requires BIP 44 standard paths, but {keypath} is not a standard path") + + params = GetPublicKeyParams(wallet_id=self.wallet.id, derivation_path=parsed_path) + response = self.btc_app.get_public_key(params) + return response.address + + def display_multisig_address( + self, + addr_type: AddressType, + multisig: MultisigDescriptor, + ) -> str: + """ + Cypherock X1 does not support multisig addresses yet. + + :raises UnavailableActionError: this function is unavailable for now + """ + raise UnavailableActionError('The Cypherock X1 does not support multisig addresses yet') + + def setup_device(self, label: str = "", passphrase: str = "") -> bool: + """ + Cypherock X1 does not support setup via software. + + :raises UnavailableActionError: Always, this function is unavailable + """ + raise UnavailableActionError('The Cypherock X1 does not support software setup') + + def wipe_device(self) -> bool: + """ + Cypherock X1 does not support wiping via software. + + :raises UnavailableActionError: Always, this function is unavailable + """ + raise UnavailableActionError('The Cypherock X1 does not support wiping via software') + + def restore_device(self, label: str = "", word_count: int = 24) -> bool: + """ + Cypherock X1 does not support restoring via software. + + :raises UnavailableActionError: Always, this function is unavailable + """ + raise UnavailableActionError('The Cypherock X1 does not support restoring via software') + + def backup_device(self, label: str = "", passphrase: str = "") -> bool: + """ + Cypherock X1 does not support backing up via software. + + :raises UnavailableActionError: Always, this function is unavailable + """ + raise UnavailableActionError('The Cypherock X1 does not support creating a backup via software') + + def close(self) -> None: + self.connection.destroy() + + def prompt_pin(self) -> bool: + """ + Cypherock X1 does not need a PIN sent from the host. + + :raises UnavailableActionError: Always, this function is unavailable + """ + raise UnavailableActionError('The Cypherock X1 does not need a PIN sent from the host') + + def send_pin(self, pin: str) -> bool: + """ + Cypherock X1 does not need a PIN sent from the host. + + :raises UnavailableActionError: Always, this function is unavailable + """ + raise UnavailableActionError('The Cypherock X1 does not need a PIN sent from the host') + + def toggle_passphrase(self) -> bool: + """ + Cypherock X1 does not support toggling passphrase from the host. + + :raises UnavailableActionError: Always, this function is unavailable + """ + raise UnavailableActionError('The Cypherock X1 does not support toggling passphrase from the host') + + def can_sign_taproot(self) -> bool: + """ + Cypherock X1 supports Taproot if the Firmware version is greater than or equal to 0.6.3089 + + :returns: True if Firmware version is greater than or equal to 0.6.3089, False otherwise. + """ + return True + +def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN, allow_emulators: bool = False) -> List[Dict[str, Any]]: + devices = get_available_devices() + + results = [] + for device in devices: + with handle_errors(common_err_msgs["enumerate"], device): + connection = DeviceConnection.connect(device) + device["fingerprint"] = get_master_fingerprint_from_device(ManagerApp.create(connection)).hex() + connection.destroy() + results.append(device) + + return results diff --git a/hwilib/devices/cypherock_sdk/README.md b/hwilib/devices/cypherock_sdk/README.md new file mode 100644 index 000000000..cbc035f87 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/README.md @@ -0,0 +1,3 @@ +# Cypherock Python SDK + +This is a modified/stripped down version of the [Cypherock Python SDK](https://github.com/Cypherock/sdk-python) which in itself is the converted version of the official [Cypherock Typescript SDK](https://github.com/Cypherock/sdk) to communicate with the Cypherock X1 firmware. diff --git a/hwilib/devices/cypherock_sdk/__init__.py b/hwilib/devices/cypherock_sdk/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/hwilib/devices/cypherock_sdk/app_btc/__init__.py b/hwilib/devices/cypherock_sdk/app_btc/__init__.py new file mode 100644 index 000000000..c3464bc42 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_btc/__init__.py @@ -0,0 +1,5 @@ +from .app import BtcApp + +__all__ = [ + "BtcApp", +] diff --git a/hwilib/devices/cypherock_sdk/app_btc/app.py b/hwilib/devices/cypherock_sdk/app_btc/app.py new file mode 100644 index 000000000..48b46ec0f --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_btc/app.py @@ -0,0 +1,94 @@ +from ..interfaces import IDeviceConnection +from ..core import SDK +from . import operations + + +class BtcApp: + """ + Bitcoin application class for Cypherock SDK. + """ + + APPLET_ID = 2 + + def __init__(self, sdk: SDK): + """ + Private constructor. Use create() class method instead. + + Args: + sdk: SDK instance + """ + self._sdk = sdk + + @classmethod + def create(cls, connection: IDeviceConnection) -> "BtcApp": + """ + Create a new BtcApp instance. + + Args: + connection: Device connection instance + + Returns: + BtcApp instance + """ + sdk = SDK.create(connection, cls.APPLET_ID) + return cls(sdk) + + def get_public_key( + self, params: operations.GetPublicKeyParams + ) -> operations.GetPublicKeyResult: + """ + Get public key from device. + + Args: + params: Parameters for getting public key + + Returns: + Public key result + """ + return self._sdk.run_operation( + lambda: operations.get_public_key(self._sdk, params) + ) + + def get_xpubs( + self, params: operations.GetXpubsParams + ) -> operations.GetXpubsResultResponse: + """ + Get extended public keys from device. + + Args: + params: Parameters for getting xpubs + + Returns: + Extended public keys result + """ + return self._sdk.run_operation( + lambda: operations.get_xpubs(self._sdk, params) + ) + + def sign_txn( + self, params: operations.SignTxnParams + ) -> operations.SignTxnResult: + """ + Sign Bitcoin transaction on device. + + Args: + params: Parameters for signing transaction + + Returns: + Sign transaction result + """ + return self._sdk.run_operation( + lambda: operations.sign_txn(self._sdk, params) + ) + + def destroy(self) -> None: + """ + Destroy the SDK instance and cleanup resources. + """ + return self._sdk.destroy() + + def abort(self) -> None: + """ + Send abort signal to device. + """ + self._sdk.send_abort() diff --git a/hwilib/devices/cypherock_sdk/app_btc/operations/__init__.py b/hwilib/devices/cypherock_sdk/app_btc/operations/__init__.py new file mode 100644 index 000000000..c8b57cd08 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_btc/operations/__init__.py @@ -0,0 +1,33 @@ +from .get_public_key import ( + get_public_key, + GetPublicKeyEvent, + GetPublicKeyParams, + GetPublicKeyResult, +) +from .get_xpubs import ( + get_xpubs, + GetXpubsEvent, + GetXpubsParams, +) +from .types import GetXpubsResultResponse +from .sign_txn import ( + sign_txn, + SignTxnEvent, + SignTxnParams, + SignTxnResult, +) + +__all__ = [ + "get_public_key", + "get_xpubs", + "sign_txn", + "GetPublicKeyEvent", + "GetPublicKeyParams", + "GetPublicKeyResult", + "GetXpubsEvent", + "GetXpubsParams", + "GetXpubsResultResponse", + "SignTxnEvent", + "SignTxnParams", + "SignTxnResult", +] diff --git a/hwilib/devices/cypherock_sdk/app_btc/operations/get_public_key/__init__.py b/hwilib/devices/cypherock_sdk/app_btc/operations/get_public_key/__init__.py new file mode 100644 index 000000000..4e3370f1f --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_btc/operations/get_public_key/__init__.py @@ -0,0 +1,92 @@ +from ....core.types import ISDK +from ....common_utils import assert_condition, create_status_listener +from ....errors import DeviceAppError, DeviceAppErrorType +from ...proto.generated.btc.get_public_key_pb2 import GetPublicKeyStatus +from ...proto.generated.common_pb2 import SeedGenerationStatus +from ...utils import ( + assert_or_throw_invalid_result, + OperationHelper, + assert_derivation_path, +) +from .types import ( + GetPublicKeyEvent, + GetPublicKeyParams, + GetPublicKeyResult, +) + +__all__ = [ + "get_public_key", + "GetPublicKeyEvent", + "GetPublicKeyParams", + "GetPublicKeyResult", +] + +import logging +logger = logging.getLogger(__name__) + + +def get_public_key( + sdk: ISDK, + params: GetPublicKeyParams, +) -> GetPublicKeyResult: + """ + Get public key from device. + + Args: + sdk: SDK instance + params: Parameters including wallet_id, derivation_path, and optional on_event handler + + Returns: + Result containing public_key + + Raises: + AssertionError: If parameters are invalid + """ + assert_condition(params, "params should be defined") + assert_condition(params.wallet_id, "wallet_id should be defined") + assert_derivation_path(params.derivation_path) + assert_condition( + len(params.derivation_path) == 5, + "derivation_path should be of depth 5", + ) + + status_listener = create_status_listener( + { + "enums": GetPublicKeyEvent, + "operationEnums": GetPublicKeyStatus, + "seedGenerationEnums": SeedGenerationStatus, + "onEvent": params.on_event, + "logger": logger, + } + ) + on_status = status_listener["onStatus"] + force_status_update = status_listener["forceStatusUpdate"] + + helper = OperationHelper( + sdk=sdk, + query_key="get_public_key", + result_key="get_public_key", + on_status=on_status, + ) + + helper.send_query( + { + "initiate": { + "wallet_id": params.wallet_id, + "derivation_path": params.derivation_path, + } + } + ) + + result = helper.wait_for_result() + assert_or_throw_invalid_result(result.result) + + force_status_update(GetPublicKeyEvent.VERIFY) + + if not result.result.public_key or len(result.result.public_key) == 0: + raise DeviceAppError(DeviceAppErrorType.INVALID_MSG_FROM_DEVICE) + + return GetPublicKeyResult( + public_key=result.result.public_key, + address=result.result.address, + ) diff --git a/hwilib/devices/cypherock_sdk/app_btc/operations/get_public_key/types.py b/hwilib/devices/cypherock_sdk/app_btc/operations/get_public_key/types.py new file mode 100644 index 000000000..610c56672 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_btc/operations/get_public_key/types.py @@ -0,0 +1,33 @@ +from typing import Callable, Optional, List +from dataclasses import dataclass +from enum import IntEnum + + +class GetPublicKeyEvent(IntEnum): + """Events that can occur during get public key operation.""" + + INIT = 0 + CONFIRM = 1 + PASSPHRASE = 2 + PIN_CARD = 3 + VERIFY = 4 + + +GetPublicKeyEventHandler = Callable[[GetPublicKeyEvent], None] + + +@dataclass +class GetPublicKeyParams: + """Parameters for get public key operation.""" + + wallet_id: bytes + derivation_path: List[int] + on_event: Optional[GetPublicKeyEventHandler] = None + + +@dataclass +class GetPublicKeyResult: + """Result of get public key operation.""" + + public_key: bytes + address: str diff --git a/hwilib/devices/cypherock_sdk/app_btc/operations/get_xpubs/__init__.py b/hwilib/devices/cypherock_sdk/app_btc/operations/get_xpubs/__init__.py new file mode 100644 index 000000000..2ede27b83 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_btc/operations/get_xpubs/__init__.py @@ -0,0 +1,84 @@ +from ....core.types import ISDK +from ....common_utils import assert_condition, create_status_listener +from ...proto.generated.btc.get_xpubs_pb2 import GetXpubsStatus, GetXpubsResultResponse +from ...proto.generated.common_pb2 import SeedGenerationStatus +from ...utils import ( + assert_or_throw_invalid_result, + OperationHelper, + assert_derivation_path, +) +from .types import GetXpubsEvent, GetXpubsParams + +import logging +logger = logging.getLogger(__name__) + +__all__ = ["get_xpubs", "GetXpubsEvent", "GetXpubsParams"] + + +def get_xpubs( + sdk: ISDK, + params: GetXpubsParams, +) -> GetXpubsResultResponse: + """ + Get extended public keys from device. + Direct port of TypeScript getXpubs function. + + Args: + sdk: SDK instance + params: Parameters including wallet_id, derivation_paths, and optional on_event handler + + Returns: + Result containing list of xpubs + + Raises: + AssertionError: If parameters are invalid + """ + assert_condition(params, "params should be defined") + assert_condition(params.derivation_paths, "derivation_paths should be defined") + assert_condition(params.wallet_id, "wallet_id should be defined") + assert_condition( + len(params.derivation_paths) > 0, + "derivation_paths should not be empty", + ) + for derivation_path in params.derivation_paths: + assert_derivation_path(derivation_path["path"]) + assert_condition( + all(len(path["path"]) == 3 for path in params.derivation_paths), + "derivation_paths should be of depth 3", + ) + + status_listener = create_status_listener( + { + "enums": GetXpubsEvent, + "operationEnums": GetXpubsStatus, + "seedGenerationEnums": SeedGenerationStatus, + "onEvent": params.on_event, + "logger": logger, + } + ) + on_status = status_listener["onStatus"] + force_status_update = status_listener["forceStatusUpdate"] + + helper = OperationHelper( + sdk=sdk, + query_key="get_xpubs", + result_key="get_xpubs", + on_status=on_status, + ) + + helper.send_query( + { + "initiate": { + "wallet_id": params.wallet_id, + "derivation_paths": params.derivation_paths, + } + } + ) + + result = helper.wait_for_result() + + assert_or_throw_invalid_result(result.result) + + force_status_update(GetXpubsEvent.PIN_CARD) + + return GetXpubsResultResponse(xpubs=result.result.xpubs) diff --git a/hwilib/devices/cypherock_sdk/app_btc/operations/get_xpubs/types.py b/hwilib/devices/cypherock_sdk/app_btc/operations/get_xpubs/types.py new file mode 100644 index 000000000..87f99ac13 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_btc/operations/get_xpubs/types.py @@ -0,0 +1,24 @@ +from typing import Callable, Optional, List +from dataclasses import dataclass +from enum import IntEnum + + +class GetXpubsEvent(IntEnum): + """Events that can occur during get xpubs operation.""" + + INIT = 0 + CONFIRM = 1 + PASSPHRASE = 2 + PIN_CARD = 3 + + +GetXpubsEventHandler = Callable[[GetXpubsEvent], None] + + +@dataclass +class GetXpubsParams: + """Parameters for get xpubs operation.""" + + wallet_id: bytes + derivation_paths: List[dict] + on_event: Optional[GetXpubsEventHandler] = None diff --git a/hwilib/devices/cypherock_sdk/app_btc/operations/sign_txn/__init__.py b/hwilib/devices/cypherock_sdk/app_btc/operations/sign_txn/__init__.py new file mode 100644 index 000000000..5ebbc7a91 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_btc/operations/sign_txn/__init__.py @@ -0,0 +1,162 @@ +from typing import List +from ....core.types import ISDK +from ....common_utils import ( + create_status_listener, +) +from ...proto.generated.btc.sign_txn_pb2 import SignTxnStatus +from ...proto.generated.common_pb2 import SeedGenerationStatus +from ...utils import ( + assert_or_throw_invalid_result, + OperationHelper, +) +from .helpers import assert_sign_txn_params +from .types import SignTxnParams, SignTxnResult, SignTxnEvent +from hwilib.key import AddressType, get_addrtype_from_bip44_purpose + +import logging +logger = logging.getLogger(__name__) + +__all__ = ["sign_txn", "SignTxnEvent", "SignTxnParams", "SignTxnResult"] + + +SIGN_TXN_DEFAULT_PARAMS = { + "version": 2, + "locktime": 0, + "hashtype": 1, + "input": { + "sequence": 0xFFFFFFFF, + }, +} + + +def sign_txn( + sdk: ISDK, + params: SignTxnParams, +) -> SignTxnResult: + """ + Sign Bitcoin transaction on device. + + Args: + sdk: SDK instance + params: Parameters including wallet_id, derivation_path, txn data, and optional on_event handler + + Returns: + Result containing signatures + + Raises: + AssertionError: If parameters are invalid + """ + assert_sign_txn_params(params) + logger.info("Started") + + status_listener = create_status_listener( + { + "enums": SignTxnEvent, + "operationEnums": SignTxnStatus, + "seedGenerationEnums": SeedGenerationStatus, + "onEvent": params.on_event, + "logger": logger, + } + ) + on_status = status_listener["onStatus"] + force_status_update = status_listener["forceStatusUpdate"] + + helper = OperationHelper( + sdk=sdk, + query_key="sign_txn", + result_key="sign_txn", + on_status=on_status, + ) + + helper.send_query( + { + "initiate": { + "wallet_id": params.wallet_id, + "derivation_path": params.derivation_path, + } + } + ) + + result = helper.wait_for_result() + assert_or_throw_invalid_result(result.confirmation) + force_status_update(SignTxnEvent.CONFIRM) + + helper.send_query( + { + "meta": { + "version": SIGN_TXN_DEFAULT_PARAMS["version"], + "locktime": params.txn.locktime or SIGN_TXN_DEFAULT_PARAMS["locktime"], + "input_count": len(params.txn.inputs), + "output_count": len(params.txn.outputs), + "sighash": params.txn.hash_type + or ( + 0 + if get_addrtype_from_bip44_purpose(params.derivation_path[0]) == AddressType.TAP + else SIGN_TXN_DEFAULT_PARAMS["hashtype"] + ), + } + } + ) + result = helper.wait_for_result() + assert_or_throw_invalid_result(result.meta_accepted) + + for input_data in params.txn.inputs: + helper.send_query( + { + "input": { + "prev_txn_hash": input_data.prev_txn_id, + "prev_output_index": input_data.prev_index, + "script_pub_key": input_data.script_pub_key, + "value": input_data.value, + "sequence": input_data.sequence + or SIGN_TXN_DEFAULT_PARAMS["input"]["sequence"], + "change_index": input_data.change_index, + "address_index": input_data.address_index, + } + } + ) + result = helper.wait_for_result() + assert_or_throw_invalid_result(result.input_accepted) + + helper.send_in_chunks( + input_data.prev_txn, + "prev_txn_chunk", + "prev_txn_chunk_accepted", + ) + + for output in params.txn.outputs: + helper.send_query( + { + "output": { + "script_pub_key": output.script_pub_key, + "value": output.value, + "is_change": output.is_change, + "changes_index": output.address_index, + } + } + ) + result = helper.wait_for_result() + assert_or_throw_invalid_result(result.output_accepted) + + signatures: List[bytes] = [] + + for i in range(len(params.txn.inputs)): + helper.send_query( + { + "signature": { + "index": i, + } + } + ) + + result = helper.wait_for_result() + assert_or_throw_invalid_result(result.signature) + + signatures.append(result.signature.signature) + + force_status_update(SignTxnEvent.PIN_CARD) + + logger.info("Completed") + return SignTxnResult( + signatures=signatures, + ) diff --git a/hwilib/devices/cypherock_sdk/app_btc/operations/sign_txn/helpers.py b/hwilib/devices/cypherock_sdk/app_btc/operations/sign_txn/helpers.py new file mode 100644 index 000000000..1f4202297 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_btc/operations/sign_txn/helpers.py @@ -0,0 +1,72 @@ +from ....common_utils import assert_condition, is_hex +from ...utils import assert_derivation_path +from .types import SignTxnParams + + +def assert_sign_txn_params(params: SignTxnParams) -> None: + """ + Assert that sign transaction parameters are valid. + + Args: + params: Parameters to validate + + Raises: + AssertionError: If any parameter is invalid + """ + assert_condition(params, "params should be defined") + assert_condition(params.wallet_id, "wallet_id should be defined") + + assert_derivation_path(params.derivation_path) + assert_condition( + len(params.derivation_path) == 3, + "derivation_path should be of depth 3", + ) + + assert_condition(params.txn, "txn be defined") + assert_condition(params.txn.inputs, "txn.inputs should be defined") + assert_condition(len(params.txn.inputs) > 0, "txn.inputs should not be empty") + assert_condition(params.txn.outputs, "txn.outputs should be defined") + assert_condition(len(params.txn.outputs) > 0, "txn.outputs should not be empty") + + for i, input_data in enumerate(params.txn.inputs): + assert_condition(input_data.value, f"txn.inputs[{i}].value should be defined") + assert_condition( + input_data.script_pub_key, f"txn.inputs[{i}].script_pub_key should be define" + ) + assert_condition( + input_data.change_index, f"txn.inputs[{i}].change_index should be define" + ) + assert_condition( + input_data.address_index, + f"txn.inputs[{i}].address_index should be define", + ) + assert_condition( + input_data.prev_index, f"txn.inputs[{i}].prev_index should be define" + ) + + assert_condition( + input_data.prev_txn_id, f"txn.inputs[{i}].prev_txn_id should not be empty" + ) + assert_condition( + is_hex(input_data.prev_txn_id), + f"txn.inputs[{i}].prev_txn_id should be valid hex string", + ) + + assert_condition( + input_data.prev_txn, f"txn.inputs[{i}].prev_txn should be defined" + ) + + assert_condition( + is_hex(input_data.prev_txn), + f"txn.inputs[{i}].prev_txn should be valid hex string", + ) + + for i, output in enumerate(params.txn.outputs): + assert_condition(output.value, f"txn.outputs[{i}].value should be defined") + assert_condition(output.script_pub_key, f"txn.outputs[{i}].script_pub_key should be define") + + if output.is_change: + assert_condition( + output.address_index, + f"txn.outputs[{i}].address_index should be define when it's a change output", + ) diff --git a/hwilib/devices/cypherock_sdk/app_btc/operations/sign_txn/types.py b/hwilib/devices/cypherock_sdk/app_btc/operations/sign_txn/types.py new file mode 100644 index 000000000..fa9cee2e8 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_btc/operations/sign_txn/types.py @@ -0,0 +1,67 @@ +from typing import Callable, Optional, List +from dataclasses import dataclass +from enum import IntEnum + + +class SignTxnEvent(IntEnum): + """Events that can occur during sign transaction operation.""" + + INIT = 0 + CONFIRM = 1 + VERIFY = 2 + PASSPHRASE = 3 + PIN_CARD = 4 + + +SignTxnEventHandler = Callable[[SignTxnEvent], None] + + +@dataclass +class SignTxnInputData: + """Input data for transaction signing.""" + + prev_txn_id: bytes + prev_index: int + value: int + script_pub_key: bytes + change_index: int + address_index: int + prev_txn: bytes + sequence: Optional[int] = None + + +@dataclass +class SignTxnOutputData: + """Output data for transaction signing.""" + + value: int + script_pub_key: bytes + is_change: bool + address_index: Optional[int] = None + + +@dataclass +class SignTxnTxnData: + """Transaction data for signing.""" + + inputs: List[SignTxnInputData] + outputs: List[SignTxnOutputData] + locktime: Optional[int] = None + hash_type: Optional[int] = None + + +@dataclass +class SignTxnParams: + """Parameters for sign transaction operation.""" + + wallet_id: bytes + derivation_path: List[int] + txn: SignTxnTxnData + on_event: Optional[SignTxnEventHandler] = None + + +@dataclass +class SignTxnResult: + """Result of sign transaction operation.""" + + signatures: List[bytes] diff --git a/hwilib/devices/cypherock_sdk/app_btc/operations/types.py b/hwilib/devices/cypherock_sdk/app_btc/operations/types.py new file mode 100644 index 000000000..29378f081 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_btc/operations/types.py @@ -0,0 +1,39 @@ +from .get_public_key.types import ( + GetPublicKeyEvent, + GetPublicKeyEventHandler, + GetPublicKeyParams, + GetPublicKeyResult, +) +from .get_xpubs.types import ( + GetXpubsEvent, + GetXpubsEventHandler, + GetXpubsParams, +) +from ..proto.generated.btc.get_xpubs_pb2 import GetXpubsResultResponse +from .sign_txn.types import ( + SignTxnEvent, + SignTxnEventHandler, + SignTxnInputData, + SignTxnOutputData, + SignTxnTxnData, + SignTxnParams, + SignTxnResult, +) + +__all__ = [ + "GetPublicKeyEvent", + "GetPublicKeyEventHandler", + "GetPublicKeyParams", + "GetPublicKeyResult", + "GetXpubsEvent", + "GetXpubsEventHandler", + "GetXpubsParams", + "GetXpubsResultResponse", + "SignTxnEvent", + "SignTxnEventHandler", + "SignTxnInputData", + "SignTxnOutputData", + "SignTxnTxnData", + "SignTxnParams", + "SignTxnResult", +] diff --git a/hwilib/devices/cypherock_sdk/app_btc/proto/generated/btc/core_pb2.py b/hwilib/devices/cypherock_sdk/app_btc/proto/generated/btc/core_pb2.py new file mode 100644 index 000000000..ad2444810 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_btc/proto/generated/btc/core_pb2.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: btc/core.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from . import get_public_key_pb2 as btc_dot_get__public__key__pb2 +from . import get_xpubs_pb2 as btc_dot_get__xpubs__pb2 +from . import sign_txn_pb2 as btc_dot_sign__txn__pb2 +from .. import error_pb2 as core_dot_error__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0e\x62tc/core.proto\x12\x03\x62tc\x1a\x18\x62tc/get_public_key.proto\x1a\x13\x62tc/get_xpubs.proto\x1a\x12\x62tc/sign_txn.proto\x1a\x10\x63ore/error.proto\"\x9a\x01\n\x05Query\x12\x32\n\x0eget_public_key\x18\x01 \x01(\x0b\x32\x18.btc.GetPublicKeyRequestH\x00\x12)\n\tget_xpubs\x18\x02 \x01(\x0b\x32\x14.btc.GetXpubsRequestH\x00\x12\'\n\x08sign_txn\x18\x03 \x01(\x0b\x32\x13.btc.SignTxnRequestH\x00\x42\t\n\x07request\"\xcb\x01\n\x06Result\x12\x33\n\x0eget_public_key\x18\x01 \x01(\x0b\x32\x19.btc.GetPublicKeyResponseH\x00\x12*\n\tget_xpubs\x18\x02 \x01(\x0b\x32\x15.btc.GetXpubsResponseH\x00\x12(\n\x08sign_txn\x18\x03 \x01(\x0b\x32\x14.btc.SignTxnResponseH\x00\x12*\n\x0c\x63ommon_error\x18\x04 \x01(\x0b\x32\x12.error.CommonErrorH\x00\x42\n\n\x08responseb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'btc.core_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_QUERY']._serialized_start=109 + _globals['_QUERY']._serialized_end=263 + _globals['_RESULT']._serialized_start=266 + _globals['_RESULT']._serialized_end=469 +# @@protoc_insertion_point(module_scope) diff --git a/hwilib/devices/cypherock_sdk/app_btc/proto/generated/btc/error_pb2.py b/hwilib/devices/cypherock_sdk/app_btc/proto/generated/btc/error_pb2.py new file mode 100644 index 000000000..dbfd05e24 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_btc/proto/generated/btc/error_pb2.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: btc/error.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0f\x62tc/error.proto\x12\x03\x62tc\"C\n\rErrorResponse\x12&\n\tsignError\x18\x01 \x01(\x0e\x32\x11.btc.SigningErrorH\x00\x42\n\n\x08response*A\n\x0cSigningError\x12\x1e\n\x1aSIGNING_ERROR_INVALID_UTXO\x10\x00\x12\x11\n\rSIGNING_ERROR\x10\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'btc.error_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_SIGNINGERROR']._serialized_start=93 + _globals['_SIGNINGERROR']._serialized_end=158 + _globals['_ERRORRESPONSE']._serialized_start=24 + _globals['_ERRORRESPONSE']._serialized_end=91 +# @@protoc_insertion_point(module_scope) diff --git a/hwilib/devices/cypherock_sdk/app_btc/proto/generated/btc/get_public_key_pb2.py b/hwilib/devices/cypherock_sdk/app_btc/proto/generated/btc/get_public_key_pb2.py new file mode 100644 index 000000000..ea45fad17 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_btc/proto/generated/btc/get_public_key_pb2.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: btc/get_public_key.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from .. import error_pb2 as core_dot_error__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18\x62tc/get_public_key.proto\x12\x03\x62tc\x1a\x10\x63ore/error.proto\"H\n\x1aGetPublicKeyIntiateRequest\x12\x11\n\twallet_id\x18\x01 \x01(\x0c\x12\x17\n\x0f\x64\x65rivation_path\x18\x02 \x03(\r\"A\n\x1aGetPublicKeyResultResponse\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\x12\x0f\n\x07\x61\x64\x64ress\x18\x02 \x01(\t\"U\n\x13GetPublicKeyRequest\x12\x33\n\x08initiate\x18\x01 \x01(\x0b\x32\x1f.btc.GetPublicKeyIntiateRequestH\x00\x42\t\n\x07request\"\x81\x01\n\x14GetPublicKeyResponse\x12\x31\n\x06result\x18\x01 \x01(\x0b\x32\x1f.btc.GetPublicKeyResultResponseH\x00\x12*\n\x0c\x63ommon_error\x18\x02 \x01(\x0b\x32\x12.error.CommonErrorH\x00\x42\n\n\x08response*\xa3\x01\n\x12GetPublicKeyStatus\x12\x1e\n\x1aGET_PUBLIC_KEY_STATUS_INIT\x10\x00\x12!\n\x1dGET_PUBLIC_KEY_STATUS_CONFIRM\x10\x01\x12(\n$GET_PUBLIC_KEY_STATUS_SEED_GENERATED\x10\x02\x12 \n\x1cGET_PUBLIC_KEY_STATUS_VERIFY\x10\x03\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'btc.get_public_key_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_GETPUBLICKEYSTATUS']._serialized_start=412 + _globals['_GETPUBLICKEYSTATUS']._serialized_end=575 + _globals['_GETPUBLICKEYINTIATEREQUEST']._serialized_start=51 + _globals['_GETPUBLICKEYINTIATEREQUEST']._serialized_end=123 + _globals['_GETPUBLICKEYRESULTRESPONSE']._serialized_start=125 + _globals['_GETPUBLICKEYRESULTRESPONSE']._serialized_end=190 + _globals['_GETPUBLICKEYREQUEST']._serialized_start=192 + _globals['_GETPUBLICKEYREQUEST']._serialized_end=277 + _globals['_GETPUBLICKEYRESPONSE']._serialized_start=280 + _globals['_GETPUBLICKEYRESPONSE']._serialized_end=409 +# @@protoc_insertion_point(module_scope) diff --git a/hwilib/devices/cypherock_sdk/app_btc/proto/generated/btc/get_xpubs_pb2.py b/hwilib/devices/cypherock_sdk/app_btc/proto/generated/btc/get_xpubs_pb2.py new file mode 100644 index 000000000..55da1ac2d --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_btc/proto/generated/btc/get_xpubs_pb2.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: btc/get_xpubs.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from .. import error_pb2 as core_dot_error__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13\x62tc/get_xpubs.proto\x12\x03\x62tc\x1a\x10\x63ore/error.proto\"%\n\x15GetXpubDerivationPath\x12\x0c\n\x04path\x18\x01 \x03(\r\"a\n\x16GetXpubsIntiateRequest\x12\x11\n\twallet_id\x18\x01 \x01(\x0c\x12\x34\n\x10\x64\x65rivation_paths\x18\x02 \x03(\x0b\x32\x1a.btc.GetXpubDerivationPath\"\x1a\n\x18GetXpubsFetchNextRequest\"\'\n\x16GetXpubsResultResponse\x12\r\n\x05xpubs\x18\x01 \x03(\t\"\x82\x01\n\x0fGetXpubsRequest\x12/\n\x08initiate\x18\x01 \x01(\x0b\x32\x1b.btc.GetXpubsIntiateRequestH\x00\x12\x33\n\nfetch_next\x18\x02 \x01(\x0b\x32\x1d.btc.GetXpubsFetchNextRequestH\x00\x42\t\n\x07request\"y\n\x10GetXpubsResponse\x12-\n\x06result\x18\x01 \x01(\x0b\x32\x1b.btc.GetXpubsResultResponseH\x00\x12*\n\x0c\x63ommon_error\x18\x02 \x01(\x0b\x32\x12.error.CommonErrorH\x00\x42\n\n\x08response*n\n\x0eGetXpubsStatus\x12\x19\n\x15GET_XPUBS_STATUS_INIT\x10\x00\x12\x1c\n\x18GET_XPUBS_STATUS_CONFIRM\x10\x01\x12#\n\x1fGET_XPUBS_STATUS_SEED_GENERATED\x10\x02\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'btc.get_xpubs_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_GETXPUBSSTATUS']._serialized_start=509 + _globals['_GETXPUBSSTATUS']._serialized_end=619 + _globals['_GETXPUBDERIVATIONPATH']._serialized_start=46 + _globals['_GETXPUBDERIVATIONPATH']._serialized_end=83 + _globals['_GETXPUBSINTIATEREQUEST']._serialized_start=85 + _globals['_GETXPUBSINTIATEREQUEST']._serialized_end=182 + _globals['_GETXPUBSFETCHNEXTREQUEST']._serialized_start=184 + _globals['_GETXPUBSFETCHNEXTREQUEST']._serialized_end=210 + _globals['_GETXPUBSRESULTRESPONSE']._serialized_start=212 + _globals['_GETXPUBSRESULTRESPONSE']._serialized_end=251 + _globals['_GETXPUBSREQUEST']._serialized_start=254 + _globals['_GETXPUBSREQUEST']._serialized_end=384 + _globals['_GETXPUBSRESPONSE']._serialized_start=386 + _globals['_GETXPUBSRESPONSE']._serialized_end=507 +# @@protoc_insertion_point(module_scope) diff --git a/hwilib/devices/cypherock_sdk/app_btc/proto/generated/btc/sign_txn_pb2.py b/hwilib/devices/cypherock_sdk/app_btc/proto/generated/btc/sign_txn_pb2.py new file mode 100644 index 000000000..96c5cc422 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_btc/proto/generated/btc/sign_txn_pb2.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: btc/sign_txn.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from .. import error_pb2 as core_dot_error__pb2 +from .. import common_pb2 as core_dot_common__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x12\x62tc/sign_txn.proto\x12\x03\x62tc\x1a\x10\x63ore/error.proto\x1a\x11\x63ore/common.proto\"D\n\x16SignTxnInitiateRequest\x12\x11\n\twallet_id\x18\x01 \x01(\x0c\x12\x17\n\x0f\x64\x65rivation_path\x18\x02 \x03(\r\"\x1d\n\x1bSignTxnConfirmationResponse\"p\n\x0fSignTxnMetadata\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\x13\n\x0binput_count\x18\x02 \x01(\r\x12\x14\n\x0coutput_count\x18\x03 \x01(\r\x12\x10\n\x08locktime\x18\x04 \x01(\r\x12\x0f\n\x07sighash\x18\x05 \x01(\r\"\x19\n\x17SignTxnMetadataAccepted\"\x16\n\x14SignTxnInputAccepted\"\xac\x01\n\x0cSignTxnInput\x12\x15\n\rprev_txn_hash\x18\x02 \x01(\x0c\x12\x19\n\x11prev_output_index\x18\x03 \x01(\r\x12\r\n\x05value\x18\x04 \x01(\x04\x12\x16\n\x0escript_pub_key\x18\x05 \x01(\x0c\x12\x10\n\x08sequence\x18\x06 \x01(\r\x12\x14\n\x0c\x63hange_index\x18\x07 \x01(\r\x12\x15\n\raddress_index\x18\x08 \x01(\rJ\x04\x08\x01\x10\x02\";\n\x0cPrevTxnChunk\x12+\n\rchunk_payload\x18\x01 \x01(\x0b\x32\x14.common.ChunkPayload\";\n\x14PrevTxnChunkAccepted\x12#\n\tchunk_ack\x18\x01 \x01(\x0b\x32\x10.common.ChunkAck\"\x17\n\x15SignTxnOutputAccepted\"w\n\rSignTxnOutput\x12\r\n\x05value\x18\x01 \x01(\x03\x12\x16\n\x0escript_pub_key\x18\x02 \x01(\x0c\x12\x11\n\tis_change\x18\x03 \x01(\x08\x12\x1a\n\rchanges_index\x18\x04 \x01(\rH\x00\x88\x01\x01\x42\x10\n\x0e_changes_index\"\x19\n\x17SignTxnVerifiedResponse\"(\n\x17SignTxnSignatureRequest\x12\r\n\x05index\x18\x01 \x01(\r\"-\n\x18SignTxnSignatureResponse\x12\x11\n\tsignature\x18\x01 \x01(\x0c\"\x9c\x02\n\x0eSignTxnRequest\x12/\n\x08initiate\x18\x01 \x01(\x0b\x32\x1b.btc.SignTxnInitiateRequestH\x00\x12$\n\x04meta\x18\x02 \x01(\x0b\x32\x14.btc.SignTxnMetadataH\x00\x12\"\n\x05input\x18\x03 \x01(\x0b\x32\x11.btc.SignTxnInputH\x00\x12+\n\x0eprev_txn_chunk\x18\x06 \x01(\x0b\x32\x11.btc.PrevTxnChunkH\x00\x12$\n\x06output\x18\x04 \x01(\x0b\x32\x12.btc.SignTxnOutputH\x00\x12\x31\n\tsignature\x18\x05 \x01(\x0b\x32\x1c.btc.SignTxnSignatureRequestH\x00\x42\t\n\x07request\"\x98\x03\n\x0fSignTxnResponse\x12\x38\n\x0c\x63onfirmation\x18\x01 \x01(\x0b\x32 .btc.SignTxnConfirmationResponseH\x00\x12\x35\n\rmeta_accepted\x18\x02 \x01(\x0b\x32\x1c.btc.SignTxnMetadataAcceptedH\x00\x12\x33\n\x0einput_accepted\x18\x03 \x01(\x0b\x32\x19.btc.SignTxnInputAcceptedH\x00\x12<\n\x17prev_txn_chunk_accepted\x18\x07 \x01(\x0b\x32\x19.btc.PrevTxnChunkAcceptedH\x00\x12\x35\n\x0foutput_accepted\x18\x04 \x01(\x0b\x32\x1a.btc.SignTxnOutputAcceptedH\x00\x12\x32\n\tsignature\x18\x05 \x01(\x0b\x32\x1d.btc.SignTxnSignatureResponseH\x00\x12*\n\x0c\x63ommon_error\x18\x06 \x01(\x0b\x32\x12.error.CommonErrorH\x00\x42\n\n\x08response*\x86\x01\n\rSignTxnStatus\x12\x18\n\x14SIGN_TXN_STATUS_INIT\x10\x00\x12\x1b\n\x17SIGN_TXN_STATUS_CONFIRM\x10\x01\x12\x1a\n\x16SIGN_TXN_STATUS_VERIFY\x10\x02\x12\"\n\x1eSIGN_TXN_STATUS_SEED_GENERATED\x10\x03\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'btc.sign_txn_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_SIGNTXNSTATUS']._serialized_start=1588 + _globals['_SIGNTXNSTATUS']._serialized_end=1722 + _globals['_SIGNTXNINITIATEREQUEST']._serialized_start=64 + _globals['_SIGNTXNINITIATEREQUEST']._serialized_end=132 + _globals['_SIGNTXNCONFIRMATIONRESPONSE']._serialized_start=134 + _globals['_SIGNTXNCONFIRMATIONRESPONSE']._serialized_end=163 + _globals['_SIGNTXNMETADATA']._serialized_start=165 + _globals['_SIGNTXNMETADATA']._serialized_end=277 + _globals['_SIGNTXNMETADATAACCEPTED']._serialized_start=279 + _globals['_SIGNTXNMETADATAACCEPTED']._serialized_end=304 + _globals['_SIGNTXNINPUTACCEPTED']._serialized_start=306 + _globals['_SIGNTXNINPUTACCEPTED']._serialized_end=328 + _globals['_SIGNTXNINPUT']._serialized_start=331 + _globals['_SIGNTXNINPUT']._serialized_end=503 + _globals['_PREVTXNCHUNK']._serialized_start=505 + _globals['_PREVTXNCHUNK']._serialized_end=564 + _globals['_PREVTXNCHUNKACCEPTED']._serialized_start=566 + _globals['_PREVTXNCHUNKACCEPTED']._serialized_end=625 + _globals['_SIGNTXNOUTPUTACCEPTED']._serialized_start=627 + _globals['_SIGNTXNOUTPUTACCEPTED']._serialized_end=650 + _globals['_SIGNTXNOUTPUT']._serialized_start=652 + _globals['_SIGNTXNOUTPUT']._serialized_end=771 + _globals['_SIGNTXNVERIFIEDRESPONSE']._serialized_start=773 + _globals['_SIGNTXNVERIFIEDRESPONSE']._serialized_end=798 + _globals['_SIGNTXNSIGNATUREREQUEST']._serialized_start=800 + _globals['_SIGNTXNSIGNATUREREQUEST']._serialized_end=840 + _globals['_SIGNTXNSIGNATURERESPONSE']._serialized_start=842 + _globals['_SIGNTXNSIGNATURERESPONSE']._serialized_end=887 + _globals['_SIGNTXNREQUEST']._serialized_start=890 + _globals['_SIGNTXNREQUEST']._serialized_end=1174 + _globals['_SIGNTXNRESPONSE']._serialized_start=1177 + _globals['_SIGNTXNRESPONSE']._serialized_end=1585 +# @@protoc_insertion_point(module_scope) diff --git a/hwilib/devices/cypherock_sdk/app_btc/proto/generated/common_pb2.py b/hwilib/devices/cypherock_sdk/app_btc/proto/generated/common_pb2.py new file mode 100644 index 000000000..e19454d5c --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_btc/proto/generated/common_pb2.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: core/common.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x11\x63ore/common.proto\x12\x06\x63ommon\"6\n\x07Version\x12\r\n\x05major\x18\x01 \x01(\r\x12\r\n\x05minor\x18\x02 \x01(\r\x12\r\n\x05patch\x18\x03 \x01(\r\"`\n\x0c\x43hunkPayload\x12\r\n\x05\x63hunk\x18\x01 \x01(\x0c\x12\x16\n\x0eremaining_size\x18\x02 \x01(\r\x12\x13\n\x0b\x63hunk_index\x18\x03 \x01(\r\x12\x14\n\x0ctotal_chunks\x18\x04 \x01(\r\"\x1f\n\x08\x43hunkAck\x12\x13\n\x0b\x63hunk_index\x18\x01 \x01(\r*\x83\x01\n\x14SeedGenerationStatus\x12\x1f\n\x1bSEED_GENERATION_STATUS_INIT\x10\x00\x12%\n!SEED_GENERATION_STATUS_PASSPHRASE\x10\x01\x12#\n\x1fSEED_GENERATION_STATUS_PIN_CARD\x10\x02\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'core.common_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_SEEDGENERATIONSTATUS']._serialized_start=217 + _globals['_SEEDGENERATIONSTATUS']._serialized_end=348 + _globals['_VERSION']._serialized_start=29 + _globals['_VERSION']._serialized_end=83 + _globals['_CHUNKPAYLOAD']._serialized_start=85 + _globals['_CHUNKPAYLOAD']._serialized_end=181 + _globals['_CHUNKACK']._serialized_start=183 + _globals['_CHUNKACK']._serialized_end=214 +# @@protoc_insertion_point(module_scope) diff --git a/hwilib/devices/cypherock_sdk/app_btc/proto/generated/error_pb2.py b/hwilib/devices/cypherock_sdk/app_btc/proto/generated/error_pb2.py new file mode 100644 index 000000000..174fa840a --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_btc/proto/generated/error_pb2.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: core/error.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10\x63ore/error.proto\x12\x05\x65rror\"\xd1\x02\n\x0b\x43ommonError\x12\x17\n\runknown_error\x18\x01 \x01(\rH\x00\x12\'\n\x0c\x63orrupt_data\x18\x02 \x01(\x0e\x32\x0f.error.DataFlowH\x00\x12\x1f\n\x15\x64\x65vice_setup_required\x18\x04 \x01(\rH\x00\x12\x31\n\x10wallet_not_found\x18\x0b \x01(\x0e\x32\x15.error.WalletNotFoundH\x00\x12\x39\n\x14wallet_partial_state\x18\x0c \x01(\x0e\x32\x19.error.WalletPartialStateH\x00\x12&\n\ncard_error\x18\x15 \x01(\x0e\x32\x10.error.CardErrorH\x00\x12.\n\x0euser_rejection\x18\x16 \x01(\x0e\x32\x14.error.UserRejectionH\x00\x42\x07\n\x05\x65rrorJ\x04\x08\x03\x10\x04J\x04\x08\x05\x10\x0bJ\x04\x08\r\x10\x15*l\n\x0eWalletNotFound\x12\x1c\n\x18WALLET_NOT_FOUND_UNKNOWN\x10\x00\x12\x1e\n\x1aWALLET_NOT_FOUND_ON_DEVICE\x10\x01\x12\x1c\n\x18WALLET_NOT_FOUND_ON_CARD\x10\x02*\xc3\x01\n\x12WalletPartialState\x12 \n\x1cWALLET_PARTIAL_STATE_UNKNOWN\x10\x00\x12\x1f\n\x1bWALLET_PARTIAL_STATE_LOCKED\x10\x01\x12\x1f\n\x1bWALLET_PARTIAL_STATE_DELETE\x10\x02\x12#\n\x1fWALLET_PARTIAL_STATE_UNVERIFIED\x10\x03\x12$\n WALLET_PARTIAL_STATE_OUT_OF_SYNC\x10\x04*\xa7\x05\n\tCardError\x12\x16\n\x12\x43\x41RD_ERROR_UNKNOWN\x10\x00\x12\x19\n\x15\x43\x41RD_ERROR_NOT_PAIRED\x10\x01\x12%\n!CARD_ERROR_SW_INCOMPATIBLE_APPLET\x10\x03\x12(\n$CARD_ERROR_SW_NULL_POINTER_EXCEPTION\x10\x04\x12\'\n#CARD_ERROR_SW_TRANSACTION_EXCEPTION\x10\x05\x12\x1e\n\x1a\x43\x41RD_ERROR_SW_FILE_INVALID\x10\x06\x12\x33\n/CARD_ERROR_SW_SECURITY_CONDITIONS_NOT_SATISFIED\x10\x07\x12*\n&CARD_ERROR_SW_CONDITIONS_NOT_SATISFIED\x10\x08\x12\x1c\n\x18\x43\x41RD_ERROR_SW_WRONG_DATA\x10\t\x12 \n\x1c\x43\x41RD_ERROR_SW_FILE_NOT_FOUND\x10\n\x12\"\n\x1e\x43\x41RD_ERROR_SW_RECORD_NOT_FOUND\x10\x0b\x12\x1b\n\x17\x43\x41RD_ERROR_SW_FILE_FULL\x10\x0c\x12#\n\x1f\x43\x41RD_ERROR_SW_CORRECT_LENGTH_00\x10\r\x12\x1d\n\x19\x43\x41RD_ERROR_SW_INVALID_INS\x10\x0e\x12\x1c\n\x18\x43\x41RD_ERROR_SW_NOT_PAIRED\x10\x0f\x12\"\n\x1e\x43\x41RD_ERROR_SW_CRYPTO_EXCEPTION\x10\x10\x12#\n\x1f\x43\x41RD_ERROR_POW_SW_WALLET_LOCKED\x10\x11\x12\x1d\n\x19\x43\x41RD_ERROR_SW_INS_BLOCKED\x10\x12\x12!\n\x1d\x43\x41RD_ERROR_SW_OUT_OF_BOUNDARY\x10\x13*L\n\rUserRejection\x12\x1a\n\x16USER_REJECTION_UNKNOWN\x10\x00\x12\x1f\n\x1bUSER_REJECTION_CONFIRMATION\x10\x01*\xe1\x01\n\x08\x44\x61taFlow\x12\x1d\n\x19\x44\x41TA_FLOW_DECODING_FAILED\x10\x00\x12\x1b\n\x17\x44\x41TA_FLOW_INVALID_QUERY\x10\x01\x12\x1b\n\x17\x44\x41TA_FLOW_FIELD_MISSING\x10\x02\x12\x1d\n\x19\x44\x41TA_FLOW_INVALID_REQUEST\x10\x03\x12 \n\x1c\x44\x41TA_FLOW_INACTIVITY_TIMEOUT\x10\x04\x12\x1a\n\x16\x44\x41TA_FLOW_INVALID_DATA\x10\x05\x12\x1f\n\x1b\x44\x41TA_FLOW_QUERY_NOT_ALLOWED\x10\x06\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'core.error_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_WALLETNOTFOUND']._serialized_start=367 + _globals['_WALLETNOTFOUND']._serialized_end=475 + _globals['_WALLETPARTIALSTATE']._serialized_start=478 + _globals['_WALLETPARTIALSTATE']._serialized_end=673 + _globals['_CARDERROR']._serialized_start=676 + _globals['_CARDERROR']._serialized_end=1355 + _globals['_USERREJECTION']._serialized_start=1357 + _globals['_USERREJECTION']._serialized_end=1433 + _globals['_DATAFLOW']._serialized_start=1436 + _globals['_DATAFLOW']._serialized_end=1661 + _globals['_COMMONERROR']._serialized_start=28 + _globals['_COMMONERROR']._serialized_end=365 +# @@protoc_insertion_point(module_scope) diff --git a/hwilib/devices/cypherock_sdk/app_btc/utils/__init__.py b/hwilib/devices/cypherock_sdk/app_btc/utils/__init__.py new file mode 100644 index 000000000..9d42f55f0 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_btc/utils/__init__.py @@ -0,0 +1,13 @@ +from .operation_helper import OperationHelper, decode_result, encode_query +from .assert_utils import assert_derivation_path +from ...core.utils.common_error import assert_or_throw_invalid_result, parse_common_error + + +__all__ = [ + "OperationHelper", + "decode_result", + "encode_query", + "assert_or_throw_invalid_result", + "parse_common_error", + "assert_derivation_path", +] diff --git a/hwilib/devices/cypherock_sdk/app_btc/utils/assert_utils.py b/hwilib/devices/cypherock_sdk/app_btc/utils/assert_utils.py new file mode 100644 index 000000000..0a8182279 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_btc/utils/assert_utils.py @@ -0,0 +1,16 @@ +from hwilib.common import Chain +from ...common_utils import assert_condition +from typing import List +from hwilib.key import H_, get_addrtype_from_bip44_purpose, get_bip44_chain + +def assert_derivation_path(path: List[int]) -> None: + """ + Assert that derivation path is valid. + + Args: + path: Derivation path to validate + """ + assert_condition(path, "derivation_path should be defined") + assert_condition(len(path) >= 3, "derivation_path should be of at least depth 3") + assert_condition(get_addrtype_from_bip44_purpose(path[0]) is not None, "derivation_path should be of a supported address type") + assert_condition(path[1] == H_(get_bip44_chain(Chain.MAIN)), "derivation_path should be on the mainnet network") diff --git a/hwilib/devices/cypherock_sdk/app_btc/utils/operation_helper.py b/hwilib/devices/cypherock_sdk/app_btc/utils/operation_helper.py new file mode 100644 index 000000000..3b6a336fa --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_btc/utils/operation_helper.py @@ -0,0 +1,186 @@ +from typing import List, Dict, Any, TypeVar, Generic, Optional +from ...core.types import ISDK +from ...errors.app_error import DeviceAppError, DeviceAppErrorType +from ...common_utils.create_status_listener import OnStatus +from ..proto.generated.btc.core_pb2 import Query, Result +from ..proto.generated.common_pb2 import ChunkPayload +from ...core.utils.common_error import assert_or_throw_invalid_result, parse_common_error + +Q = TypeVar("Q") +R = TypeVar("R") + + +def decode_result(data: bytes) -> Result: + """ + Decode result from device response. + + Args: + data: Raw bytes from device + + Returns: + Decoded result + + Raises: + DeviceAppError: If decoding fails + """ + try: + result = Result() + result.ParseFromString(data) + return result + except Exception: + raise DeviceAppError(DeviceAppErrorType.INVALID_MSG_FROM_DEVICE) + + +def encode_query(query: Dict[str, Any]) -> bytes: + """ + Encode query to send to device. + + Args: + query: Query dictionary + + Returns: + Encoded query bytes + """ + query_obj = Query(**query) + return query_obj.SerializeToString() + + +class OperationHelper(Generic[Q, R]): + """ + Helper class for device operations with query/result pattern. + """ + + CHUNK_SIZE = 2048 + + def __init__( + self, + sdk: ISDK, + query_key: str, + result_key: str, + on_status: Optional[OnStatus] = None, + ): + """ + Initialize operation helper. + + Args: + sdk: SDK instance + query_key: Key for query type + result_key: Key for result type + on_status: Optional status callback + """ + self.sdk = sdk + self.query_key = query_key + self.result_key = result_key + self.on_status = on_status + + def send_query(self, query: Dict[str, Any]) -> None: + """ + Send query to device. + + Args: + query: Query data + """ + op_key = self.query_key + query_data = {op_key: query} + encoded_query = encode_query(query_data) + self.sdk.send_query(encoded_query) + + def wait_for_result(self) -> Any: + """ + Wait for and decode result from device. + + Returns: + Decoded result data + + Raises: + DeviceAppError: If result is invalid or contains errors + """ + result_data = self.sdk.wait_for_result({"on_status": self.on_status}) + result = decode_result(result_data) + + result_key = self.result_key + + if "." in result_key: + parts = result_key.split(".") + result_value = result + for part in parts: + result_value = getattr(result_value, part, None) + if result_value is None: + break + else: + result_value = getattr(result, result_key, None) + + # Check for errors in the specific operation response first + if result_value and hasattr(result_value, "common_error"): + parse_common_error(result_value.common_error) + + # Check for errors in the top-level result + parse_common_error(getattr(result, "common_error", None)) + + assert_or_throw_invalid_result(result_value) + + return result_value + + @staticmethod + def split_into_chunks(data: bytes) -> List[bytes]: + """ + Split data into chunks for transmission. + + Args: + data: Data to split + + Returns: + List of data chunks + """ + chunks = [] + total_chunks = ( + len(data) + OperationHelper.CHUNK_SIZE - 1 + ) // OperationHelper.CHUNK_SIZE + + for i in range(total_chunks): + start = i * OperationHelper.CHUNK_SIZE + end = min(start + OperationHelper.CHUNK_SIZE, len(data)) + chunk = data[start:end] + chunks.append(chunk) + + return chunks + + def send_in_chunks( + self, data: bytes, query_key: str, result_key: str + ) -> None: + """ + Send data in chunks to device. + + Args: + data: Data to send + query_key: Query key for chunk sending + result_key: Result key for chunk acknowledgment + """ + chunks = self.split_into_chunks(data) + remaining_size = len(data) + + for i, chunk in enumerate(chunks): + remaining_size -= len(chunk) + + chunk_payload = ChunkPayload( + chunk=chunk, + chunk_index=i, + total_chunks=len(chunks), + remaining_size=remaining_size, + ) + + self.send_query( + { + query_key: { + "chunk_payload": chunk_payload, + }, + } + ) + + result = self.wait_for_result() + result_data = getattr(result, result_key, None) + assert_or_throw_invalid_result(result_data) + + chunk_ack = getattr(result_data, "chunk_ack", None) + assert_or_throw_invalid_result(chunk_ack) + assert_or_throw_invalid_result(chunk_ack.chunk_index == i) diff --git a/hwilib/devices/cypherock_sdk/app_manager/README.md b/hwilib/devices/cypherock_sdk/app_manager/README.md new file mode 100644 index 000000000..6c353a3fe --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_manager/README.md @@ -0,0 +1,4 @@ +# app-manager + +This package provides the app manager SDK for Cypherock X1. + diff --git a/hwilib/devices/cypherock_sdk/app_manager/__init__.py b/hwilib/devices/cypherock_sdk/app_manager/__init__.py new file mode 100644 index 000000000..b138b1e1e --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_manager/__init__.py @@ -0,0 +1,3 @@ +from .app import ManagerApp + +__all__ = ["ManagerApp"] diff --git a/hwilib/devices/cypherock_sdk/app_manager/app.py b/hwilib/devices/cypherock_sdk/app_manager/app.py new file mode 100644 index 000000000..8a934ef01 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_manager/app.py @@ -0,0 +1,41 @@ +from ..interfaces import IDeviceConnection +from ..core import sdk as core_sdk +from ..core.types import ISDK + +from . import operations + + +class ManagerApp: + APPLET_ID = 1 + + def __init__(self, sdk: ISDK): + self._sdk = sdk + + @classmethod + def create(cls, connection: IDeviceConnection) -> "ManagerApp": + sdk = core_sdk.SDK.create(connection, cls.APPLET_ID) + return cls(sdk) + + def get_device_info(self): + return self._sdk.run_operation( + lambda: operations.get_device_info(self._sdk) + ) + + def get_wallets(self): + return self._sdk.run_operation(lambda: operations.get_wallets(self._sdk)) + + def get_logs(self, on_event: operations.GetLogsEventHandler = None): + return self._sdk.run_operation( + lambda: operations.get_logs(self._sdk, on_event) + ) + + def select_wallet(self): + return self._sdk.run_operation( + lambda: operations.select_wallet(self._sdk) + ) + + def destroy(self): + return self._sdk.destroy() + + def abort(self): + return self._sdk.send_abort() diff --git a/hwilib/devices/cypherock_sdk/app_manager/operations/__init__.py b/hwilib/devices/cypherock_sdk/app_manager/operations/__init__.py new file mode 100644 index 000000000..025f1889f --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_manager/operations/__init__.py @@ -0,0 +1,14 @@ +from .getDeviceInfo import get_device_info +from .getWallets import get_wallets +from .getLogs import get_logs, GetLogsError, GetLogsErrorType, GetLogsEventHandler +from .selectWallet import select_wallet + +__all__ = [ + "get_device_info", + "get_wallets", + "get_logs", + "GetLogsError", + "GetLogsErrorType", + "GetLogsEventHandler", + "select_wallet", +] diff --git a/hwilib/devices/cypherock_sdk/app_manager/operations/getDeviceInfo/__init__.py b/hwilib/devices/cypherock_sdk/app_manager/operations/getDeviceInfo/__init__.py new file mode 100644 index 000000000..f73c8910e --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_manager/operations/getDeviceInfo/__init__.py @@ -0,0 +1,17 @@ +from ....core.types import ISDK +from ...proto.generated.manager.get_device_info_pb2 import GetDeviceInfoResultResponse +from ...utils import assert_or_throw_invalid_result, OperationHelper + +import logging +logger = logging.getLogger(__name__) + + +def get_device_info(sdk: ISDK) -> GetDeviceInfoResultResponse: + logger.info("Started") + helper = OperationHelper(sdk, "get_device_info", "get_device_info") + helper.send_query({"initiate": {}}) + result = helper.wait_for_result() + logger.info("GetDeviceInfoResponse", {"result": result}) + assert_or_throw_invalid_result(result.result) + logger.info("Completed") + return result.result diff --git a/hwilib/devices/cypherock_sdk/app_manager/operations/getLogs/__init__.py b/hwilib/devices/cypherock_sdk/app_manager/operations/getLogs/__init__.py new file mode 100644 index 000000000..b07dc2ad8 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_manager/operations/getLogs/__init__.py @@ -0,0 +1,80 @@ +from typing import Optional +from ....core.types import ISDK +from ....common_utils import create_status_listener +from ...proto.generated.manager.get_logs_pb2 import GetLogsStatus, GetLogsErrorResponse +from ...utils import assert_or_throw_invalid_result, OperationHelper +from .types import GetLogsError, GetLogsErrorType, GetLogsEventHandler + +import logging +logger = logging.getLogger(__name__) + +__all__ = ["get_logs", "GetLogsError", "GetLogsErrorType", "GetLogsEventHandler"] + + +def parse_get_logs_error(error: Optional[GetLogsErrorResponse]) -> None: + if error is None: + return + + error_types_map = { + "logsDisabled": GetLogsErrorType.LOGS_DISABLED, + } + + for key, error_type in error_types_map.items(): + if hasattr(error, key) and getattr(error, key): + raise GetLogsError(error_type) + + +def fetch_logs_data(helper: OperationHelper, on_status) -> str: + result = helper.wait_for_result(on_status) + + parse_get_logs_error(result.error) + assert_or_throw_invalid_result(result.logs) + + return result.logs + + +def get_logs( + sdk: ISDK, + on_event: Optional[GetLogsEventHandler] = None, +) -> str: + logger.info("Started") + helper = OperationHelper(sdk, "get_logs", "get_logs") + + status_listener = create_status_listener( + { + "enums": GetLogsStatus, + "onEvent": on_event, + "logger": logger, + } + ) + on_status = status_listener["onStatus"] + force_status_update = status_listener["forceStatusUpdate"] + + # ASCII decoder for log data + def decode_ascii(data: bytes) -> str: + return data.decode("ascii", errors="replace") + + all_logs: list[str] = [] + is_confirmed = False + has_more = False + + helper.send_query({"initiate": {}}) + + while True: + result = fetch_logs_data(helper, on_status) + + if not is_confirmed: + force_status_update(GetLogsStatus.GET_LOGS_STATUS_USER_CONFIRMED) + + is_confirmed = True + has_more = result.has_more + + all_logs.append(decode_ascii(result.data)) + + if has_more: + helper.send_query({"fetch_next": {}}) + else: + break + + logger.info("Completed") + return "".join(all_logs) diff --git a/hwilib/devices/cypherock_sdk/app_manager/operations/getLogs/error.py b/hwilib/devices/cypherock_sdk/app_manager/operations/getLogs/error.py new file mode 100644 index 000000000..ce361bfb7 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_manager/operations/getLogs/error.py @@ -0,0 +1,19 @@ +from enum import Enum +from ....errors import DeviceError + + +class GetLogsErrorType(Enum): + LOGS_DISABLED = "MGA_GL_0000" + + +get_logs_error_type_details = { + GetLogsErrorType.LOGS_DISABLED: { + "message": "Logs are disabled on the device", + }, +} + + +class GetLogsError(DeviceError): + def __init__(self, error_code: GetLogsErrorType): + error_details = get_logs_error_type_details[error_code] + super().__init__(error_code.value, error_details["message"], GetLogsError) diff --git a/hwilib/devices/cypherock_sdk/app_manager/operations/getLogs/types.py b/hwilib/devices/cypherock_sdk/app_manager/operations/getLogs/types.py new file mode 100644 index 000000000..54fa95574 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_manager/operations/getLogs/types.py @@ -0,0 +1,8 @@ +from typing import Callable +from ...proto.generated.manager.get_logs_pb2 import GetLogsStatus +from .error import GetLogsError, GetLogsErrorType + +GetLogsEventHandler = Callable[[GetLogsStatus], None] + +# Re-export error types +__all__ = ["GetLogsError", "GetLogsErrorType", "GetLogsEventHandler"] diff --git a/hwilib/devices/cypherock_sdk/app_manager/operations/getWallets/__init__.py b/hwilib/devices/cypherock_sdk/app_manager/operations/getWallets/__init__.py new file mode 100644 index 000000000..68c7837bc --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_manager/operations/getWallets/__init__.py @@ -0,0 +1,20 @@ +from ....core.types import ISDK +from ...proto.generated.manager.get_wallets_pb2 import GetWalletsResultResponse +from ...utils import assert_or_throw_invalid_result, OperationHelper + +import logging +logger = logging.getLogger(__name__) + + +def get_wallets(sdk: ISDK) -> GetWalletsResultResponse: + logger.info("Started") + + helper = OperationHelper(sdk, "get_wallets", "get_wallets") + + helper.send_query({"initiate": {}}) + result = helper.wait_for_result() + logger.info("GetWalletsResponse", {"result": result}) + assert_or_throw_invalid_result(result.result) + + logger.info("Completed") + return result.result diff --git a/hwilib/devices/cypherock_sdk/app_manager/operations/selectWallet/__init__.py b/hwilib/devices/cypherock_sdk/app_manager/operations/selectWallet/__init__.py new file mode 100644 index 000000000..3e0008b6c --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_manager/operations/selectWallet/__init__.py @@ -0,0 +1,21 @@ +from ....core.types import ISDK +from ...proto.generated.manager.wallet_selector_pb2 import ( + SelectWalletResultResponse, +) +from ...utils import assert_or_throw_invalid_result, OperationHelper + +import logging +logger = logging.getLogger(__name__) + +def select_wallet(sdk: ISDK) -> SelectWalletResultResponse: + logger.info("Started") + + helper = OperationHelper(sdk, "select_wallet", "select_wallet") + + helper.send_query({"initiate": {}}) + result = helper.wait_for_result() + logger.info("SelectWalletResponse", {"result": result}) + assert_or_throw_invalid_result(result.result) + + logger.info("Completed") + return result.result diff --git a/hwilib/devices/cypherock_sdk/app_manager/operations/types.py b/hwilib/devices/cypherock_sdk/app_manager/operations/types.py new file mode 100644 index 000000000..e2aa24f82 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_manager/operations/types.py @@ -0,0 +1,7 @@ +from .getLogs.types import GetLogsEventHandler, GetLogsError, GetLogsErrorType + +__all__ = [ + "GetLogsEventHandler", + "GetLogsError", + "GetLogsErrorType", +] diff --git a/hwilib/devices/cypherock_sdk/app_manager/proto/generated/common_pb2.py b/hwilib/devices/cypherock_sdk/app_manager/proto/generated/common_pb2.py new file mode 100644 index 000000000..e19454d5c --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_manager/proto/generated/common_pb2.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: core/common.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x11\x63ore/common.proto\x12\x06\x63ommon\"6\n\x07Version\x12\r\n\x05major\x18\x01 \x01(\r\x12\r\n\x05minor\x18\x02 \x01(\r\x12\r\n\x05patch\x18\x03 \x01(\r\"`\n\x0c\x43hunkPayload\x12\r\n\x05\x63hunk\x18\x01 \x01(\x0c\x12\x16\n\x0eremaining_size\x18\x02 \x01(\r\x12\x13\n\x0b\x63hunk_index\x18\x03 \x01(\r\x12\x14\n\x0ctotal_chunks\x18\x04 \x01(\r\"\x1f\n\x08\x43hunkAck\x12\x13\n\x0b\x63hunk_index\x18\x01 \x01(\r*\x83\x01\n\x14SeedGenerationStatus\x12\x1f\n\x1bSEED_GENERATION_STATUS_INIT\x10\x00\x12%\n!SEED_GENERATION_STATUS_PASSPHRASE\x10\x01\x12#\n\x1fSEED_GENERATION_STATUS_PIN_CARD\x10\x02\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'core.common_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_SEEDGENERATIONSTATUS']._serialized_start=217 + _globals['_SEEDGENERATIONSTATUS']._serialized_end=348 + _globals['_VERSION']._serialized_start=29 + _globals['_VERSION']._serialized_end=83 + _globals['_CHUNKPAYLOAD']._serialized_start=85 + _globals['_CHUNKPAYLOAD']._serialized_end=181 + _globals['_CHUNKACK']._serialized_start=183 + _globals['_CHUNKACK']._serialized_end=214 +# @@protoc_insertion_point(module_scope) diff --git a/hwilib/devices/cypherock_sdk/app_manager/proto/generated/error_pb2.py b/hwilib/devices/cypherock_sdk/app_manager/proto/generated/error_pb2.py new file mode 100644 index 000000000..174fa840a --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_manager/proto/generated/error_pb2.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: core/error.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10\x63ore/error.proto\x12\x05\x65rror\"\xd1\x02\n\x0b\x43ommonError\x12\x17\n\runknown_error\x18\x01 \x01(\rH\x00\x12\'\n\x0c\x63orrupt_data\x18\x02 \x01(\x0e\x32\x0f.error.DataFlowH\x00\x12\x1f\n\x15\x64\x65vice_setup_required\x18\x04 \x01(\rH\x00\x12\x31\n\x10wallet_not_found\x18\x0b \x01(\x0e\x32\x15.error.WalletNotFoundH\x00\x12\x39\n\x14wallet_partial_state\x18\x0c \x01(\x0e\x32\x19.error.WalletPartialStateH\x00\x12&\n\ncard_error\x18\x15 \x01(\x0e\x32\x10.error.CardErrorH\x00\x12.\n\x0euser_rejection\x18\x16 \x01(\x0e\x32\x14.error.UserRejectionH\x00\x42\x07\n\x05\x65rrorJ\x04\x08\x03\x10\x04J\x04\x08\x05\x10\x0bJ\x04\x08\r\x10\x15*l\n\x0eWalletNotFound\x12\x1c\n\x18WALLET_NOT_FOUND_UNKNOWN\x10\x00\x12\x1e\n\x1aWALLET_NOT_FOUND_ON_DEVICE\x10\x01\x12\x1c\n\x18WALLET_NOT_FOUND_ON_CARD\x10\x02*\xc3\x01\n\x12WalletPartialState\x12 \n\x1cWALLET_PARTIAL_STATE_UNKNOWN\x10\x00\x12\x1f\n\x1bWALLET_PARTIAL_STATE_LOCKED\x10\x01\x12\x1f\n\x1bWALLET_PARTIAL_STATE_DELETE\x10\x02\x12#\n\x1fWALLET_PARTIAL_STATE_UNVERIFIED\x10\x03\x12$\n WALLET_PARTIAL_STATE_OUT_OF_SYNC\x10\x04*\xa7\x05\n\tCardError\x12\x16\n\x12\x43\x41RD_ERROR_UNKNOWN\x10\x00\x12\x19\n\x15\x43\x41RD_ERROR_NOT_PAIRED\x10\x01\x12%\n!CARD_ERROR_SW_INCOMPATIBLE_APPLET\x10\x03\x12(\n$CARD_ERROR_SW_NULL_POINTER_EXCEPTION\x10\x04\x12\'\n#CARD_ERROR_SW_TRANSACTION_EXCEPTION\x10\x05\x12\x1e\n\x1a\x43\x41RD_ERROR_SW_FILE_INVALID\x10\x06\x12\x33\n/CARD_ERROR_SW_SECURITY_CONDITIONS_NOT_SATISFIED\x10\x07\x12*\n&CARD_ERROR_SW_CONDITIONS_NOT_SATISFIED\x10\x08\x12\x1c\n\x18\x43\x41RD_ERROR_SW_WRONG_DATA\x10\t\x12 \n\x1c\x43\x41RD_ERROR_SW_FILE_NOT_FOUND\x10\n\x12\"\n\x1e\x43\x41RD_ERROR_SW_RECORD_NOT_FOUND\x10\x0b\x12\x1b\n\x17\x43\x41RD_ERROR_SW_FILE_FULL\x10\x0c\x12#\n\x1f\x43\x41RD_ERROR_SW_CORRECT_LENGTH_00\x10\r\x12\x1d\n\x19\x43\x41RD_ERROR_SW_INVALID_INS\x10\x0e\x12\x1c\n\x18\x43\x41RD_ERROR_SW_NOT_PAIRED\x10\x0f\x12\"\n\x1e\x43\x41RD_ERROR_SW_CRYPTO_EXCEPTION\x10\x10\x12#\n\x1f\x43\x41RD_ERROR_POW_SW_WALLET_LOCKED\x10\x11\x12\x1d\n\x19\x43\x41RD_ERROR_SW_INS_BLOCKED\x10\x12\x12!\n\x1d\x43\x41RD_ERROR_SW_OUT_OF_BOUNDARY\x10\x13*L\n\rUserRejection\x12\x1a\n\x16USER_REJECTION_UNKNOWN\x10\x00\x12\x1f\n\x1bUSER_REJECTION_CONFIRMATION\x10\x01*\xe1\x01\n\x08\x44\x61taFlow\x12\x1d\n\x19\x44\x41TA_FLOW_DECODING_FAILED\x10\x00\x12\x1b\n\x17\x44\x41TA_FLOW_INVALID_QUERY\x10\x01\x12\x1b\n\x17\x44\x41TA_FLOW_FIELD_MISSING\x10\x02\x12\x1d\n\x19\x44\x41TA_FLOW_INVALID_REQUEST\x10\x03\x12 \n\x1c\x44\x41TA_FLOW_INACTIVITY_TIMEOUT\x10\x04\x12\x1a\n\x16\x44\x41TA_FLOW_INVALID_DATA\x10\x05\x12\x1f\n\x1b\x44\x41TA_FLOW_QUERY_NOT_ALLOWED\x10\x06\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'core.error_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_WALLETNOTFOUND']._serialized_start=367 + _globals['_WALLETNOTFOUND']._serialized_end=475 + _globals['_WALLETPARTIALSTATE']._serialized_start=478 + _globals['_WALLETPARTIALSTATE']._serialized_end=673 + _globals['_CARDERROR']._serialized_start=676 + _globals['_CARDERROR']._serialized_end=1355 + _globals['_USERREJECTION']._serialized_start=1357 + _globals['_USERREJECTION']._serialized_end=1433 + _globals['_DATAFLOW']._serialized_start=1436 + _globals['_DATAFLOW']._serialized_end=1661 + _globals['_COMMONERROR']._serialized_start=28 + _globals['_COMMONERROR']._serialized_end=365 +# @@protoc_insertion_point(module_scope) diff --git a/hwilib/devices/cypherock_sdk/app_manager/proto/generated/manager/common_pb2.py b/hwilib/devices/cypherock_sdk/app_manager/proto/generated/manager/common_pb2.py new file mode 100644 index 000000000..04e169cde --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_manager/proto/generated/manager/common_pb2.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: manager/common.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14manager/common.proto\x12\x07manager\"^\n\nWalletItem\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0e\n\x06hasPin\x18\x03 \x01(\x08\x12\x15\n\rhasPassphrase\x18\x04 \x01(\x08\x12\x0f\n\x07isValid\x18\x05 \x01(\x08\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'manager.common_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_WALLETITEM']._serialized_start=33 + _globals['_WALLETITEM']._serialized_end=127 +# @@protoc_insertion_point(module_scope) diff --git a/hwilib/devices/cypherock_sdk/app_manager/proto/generated/manager/core_pb2.py b/hwilib/devices/cypherock_sdk/app_manager/proto/generated/manager/core_pb2.py new file mode 100644 index 000000000..2c8a62a09 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_manager/proto/generated/manager/core_pb2.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: manager/core.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from . import get_device_info_pb2 as manager_dot_get__device__info__pb2 +from . import get_wallets_pb2 as manager_dot_get__wallets__pb2 +from . import get_logs_pb2 as manager_dot_get__logs__pb2 +from . import wallet_selector_pb2 as manager_dot_wallet__selector__pb2 +from .. import error_pb2 as core_dot_error__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x12manager/core.proto\x12\x07manager\x1a\x1dmanager/get_device_info.proto\x1a\x19manager/get_wallets.proto\x1a\x16manager/get_logs.proto\x1a\x1dmanager/wallet_selector.proto\x1a\x10\x63ore/error.proto\"\xe3\x01\n\x05Query\x12\x38\n\x0fget_device_info\x18\x01 \x01(\x0b\x32\x1d.manager.GetDeviceInfoRequestH\x00\x12\x31\n\x0bget_wallets\x18\x02 \x01(\x0b\x32\x1a.manager.GetWalletsRequestH\x00\x12+\n\x08get_logs\x18\x05 \x01(\x0b\x32\x17.manager.GetLogsRequestH\x00\x12\x35\n\rselect_wallet\x18\t \x01(\x0b\x32\x1c.manager.SelectWalletRequestH\x00\x42\t\n\x07request\"\x95\x02\n\x06Result\x12\x39\n\x0fget_device_info\x18\x01 \x01(\x0b\x32\x1e.manager.GetDeviceInfoResponseH\x00\x12\x32\n\x0bget_wallets\x18\x02 \x01(\x0b\x32\x1b.manager.GetWalletsResponseH\x00\x12,\n\x08get_logs\x18\x05 \x01(\x0b\x32\x18.manager.GetLogsResponseH\x00\x12*\n\x0c\x63ommon_error\x18\x08 \x01(\x0b\x32\x12.error.CommonErrorH\x00\x12\x36\n\rselect_wallet\x18\n \x01(\x0b\x32\x1d.manager.SelectWalletResponseH\x00\x42\n\n\x08responseb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'manager.core_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_QUERY']._serialized_start=163 + _globals['_QUERY']._serialized_end=390 + _globals['_RESULT']._serialized_start=393 + _globals['_RESULT']._serialized_end=670 +# @@protoc_insertion_point(module_scope) diff --git a/hwilib/devices/cypherock_sdk/app_manager/proto/generated/manager/get_device_info_pb2.py b/hwilib/devices/cypherock_sdk/app_manager/proto/generated/manager/get_device_info_pb2.py new file mode 100644 index 000000000..23beaeaa1 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_manager/proto/generated/manager/get_device_info_pb2.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: manager/get_device_info.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from .. import common_pb2 as core_dot_common__pb2 +from .. import error_pb2 as core_dot_error__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1dmanager/get_device_info.proto\x12\x07manager\x1a\x11\x63ore/common.proto\x1a\x10\x63ore/error.proto\"C\n\x13SupportedAppletItem\x12\n\n\x02id\x18\x01 \x01(\r\x12 \n\x07version\x18\x02 \x01(\x0b\x32\x0f.common.Version\"\x1d\n\x1bGetDeviceInfoIntiateRequest\"\xf2\x01\n\x1bGetDeviceInfoResultResponse\x12\x15\n\rdevice_serial\x18\x01 \x01(\x0c\x12)\n\x10\x66irmware_version\x18\x02 \x01(\x0b\x32\x0f.common.Version\x12\x18\n\x10is_authenticated\x18\x03 \x01(\x08\x12\x31\n\x0b\x61pplet_list\x18\x04 \x03(\x0b\x32\x1c.manager.SupportedAppletItem\x12\x12\n\nis_initial\x18\x05 \x01(\x08\x12\x30\n\x0fonboarding_step\x18\x06 \x01(\x0e\x32\x17.manager.OnboardingStep\"[\n\x14GetDeviceInfoRequest\x12\x38\n\x08initiate\x18\x01 \x01(\x0b\x32$.manager.GetDeviceInfoIntiateRequestH\x00\x42\t\n\x07request\"\x87\x01\n\x15GetDeviceInfoResponse\x12\x36\n\x06result\x18\x01 \x01(\x0b\x32$.manager.GetDeviceInfoResultResponseH\x00\x12*\n\x0c\x63ommon_error\x18\x02 \x01(\x0b\x32\x12.error.CommonErrorH\x00\x42\n\n\x08response*\xe4\x01\n\x0eOnboardingStep\x12!\n\x1dONBOARDING_STEP_VIRGIN_DEVICE\x10\x00\x12\x1f\n\x1bONBOARDING_STEP_DEVICE_AUTH\x10\x01\x12%\n!ONBOARDING_STEP_JOYSTICK_TRAINING\x10\x02\x12 \n\x1cONBOARDING_STEP_CARD_CHECKUP\x10\x03\x12\'\n#ONBOARDING_STEP_CARD_AUTHENTICATION\x10\x04\x12\x1c\n\x18ONBOARDING_STEP_COMPLETE\x10\x05\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'manager.get_device_info_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_ONBOARDINGSTEP']._serialized_start=656 + _globals['_ONBOARDINGSTEP']._serialized_end=884 + _globals['_SUPPORTEDAPPLETITEM']._serialized_start=79 + _globals['_SUPPORTEDAPPLETITEM']._serialized_end=146 + _globals['_GETDEVICEINFOINTIATEREQUEST']._serialized_start=148 + _globals['_GETDEVICEINFOINTIATEREQUEST']._serialized_end=177 + _globals['_GETDEVICEINFORESULTRESPONSE']._serialized_start=180 + _globals['_GETDEVICEINFORESULTRESPONSE']._serialized_end=422 + _globals['_GETDEVICEINFOREQUEST']._serialized_start=424 + _globals['_GETDEVICEINFOREQUEST']._serialized_end=515 + _globals['_GETDEVICEINFORESPONSE']._serialized_start=518 + _globals['_GETDEVICEINFORESPONSE']._serialized_end=653 +# @@protoc_insertion_point(module_scope) diff --git a/hwilib/devices/cypherock_sdk/app_manager/proto/generated/manager/get_logs_pb2.py b/hwilib/devices/cypherock_sdk/app_manager/proto/generated/manager/get_logs_pb2.py new file mode 100644 index 000000000..3df006a2b --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_manager/proto/generated/manager/get_logs_pb2.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: manager/get_logs.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from .. import error_pb2 as core_dot_error__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16manager/get_logs.proto\x12\x07manager\x1a\x10\x63ore/error.proto\"\x18\n\x16GetLogsInitiateRequest\"\x19\n\x17GetLogsFetchNextRequest\"5\n\x13GetLogsDataResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\x10\n\x08has_more\x18\x02 \x01(\x08\"-\n\x14GetLogsErrorResponse\x12\x15\n\rlogs_disabled\x18\x01 \x01(\x08\"\x88\x01\n\x0eGetLogsRequest\x12\x33\n\x08initiate\x18\x01 \x01(\x0b\x32\x1f.manager.GetLogsInitiateRequestH\x00\x12\x36\n\nfetch_next\x18\x02 \x01(\x0b\x32 .manager.GetLogsFetchNextRequestH\x00\x42\t\n\x07request\"\xa7\x01\n\x0fGetLogsResponse\x12,\n\x04logs\x18\x01 \x01(\x0b\x32\x1c.manager.GetLogsDataResponseH\x00\x12*\n\x0c\x63ommon_error\x18\x02 \x01(\x0b\x32\x12.error.CommonErrorH\x00\x12.\n\x05\x65rror\x18\x03 \x01(\x0b\x32\x1d.manager.GetLogsErrorResponseH\x00\x42\n\n\x08response*M\n\rGetLogsStatus\x12\x18\n\x14GET_LOGS_STATUS_INIT\x10\x00\x12\"\n\x1eGET_LOGS_STATUS_USER_CONFIRMED\x10\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'manager.get_logs_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_GETLOGSSTATUS']._serialized_start=517 + _globals['_GETLOGSSTATUS']._serialized_end=594 + _globals['_GETLOGSINITIATEREQUEST']._serialized_start=53 + _globals['_GETLOGSINITIATEREQUEST']._serialized_end=77 + _globals['_GETLOGSFETCHNEXTREQUEST']._serialized_start=79 + _globals['_GETLOGSFETCHNEXTREQUEST']._serialized_end=104 + _globals['_GETLOGSDATARESPONSE']._serialized_start=106 + _globals['_GETLOGSDATARESPONSE']._serialized_end=159 + _globals['_GETLOGSERRORRESPONSE']._serialized_start=161 + _globals['_GETLOGSERRORRESPONSE']._serialized_end=206 + _globals['_GETLOGSREQUEST']._serialized_start=209 + _globals['_GETLOGSREQUEST']._serialized_end=345 + _globals['_GETLOGSRESPONSE']._serialized_start=348 + _globals['_GETLOGSRESPONSE']._serialized_end=515 +# @@protoc_insertion_point(module_scope) diff --git a/hwilib/devices/cypherock_sdk/app_manager/proto/generated/manager/get_wallets_pb2.py b/hwilib/devices/cypherock_sdk/app_manager/proto/generated/manager/get_wallets_pb2.py new file mode 100644 index 000000000..5ca7cee47 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_manager/proto/generated/manager/get_wallets_pb2.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: manager/get_wallets.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from .. import error_pb2 as core_dot_error__pb2 +from . import common_pb2 as manager_dot_common__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19manager/get_wallets.proto\x12\x07manager\x1a\x10\x63ore/error.proto\x1a\x14manager/common.proto\"\x1a\n\x18GetWalletsIntiateRequest\"D\n\x18GetWalletsResultResponse\x12(\n\x0bwallet_list\x18\x01 \x03(\x0b\x32\x13.manager.WalletItem\"U\n\x11GetWalletsRequest\x12\x35\n\x08initiate\x18\x01 \x01(\x0b\x32!.manager.GetWalletsIntiateRequestH\x00\x42\t\n\x07request\"\x81\x01\n\x12GetWalletsResponse\x12\x33\n\x06result\x18\x01 \x01(\x0b\x32!.manager.GetWalletsResultResponseH\x00\x12*\n\x0c\x63ommon_error\x18\x02 \x01(\x0b\x32\x12.error.CommonErrorH\x00\x42\n\n\x08responseb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'manager.get_wallets_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_GETWALLETSINTIATEREQUEST']._serialized_start=78 + _globals['_GETWALLETSINTIATEREQUEST']._serialized_end=104 + _globals['_GETWALLETSRESULTRESPONSE']._serialized_start=106 + _globals['_GETWALLETSRESULTRESPONSE']._serialized_end=174 + _globals['_GETWALLETSREQUEST']._serialized_start=176 + _globals['_GETWALLETSREQUEST']._serialized_end=261 + _globals['_GETWALLETSRESPONSE']._serialized_start=264 + _globals['_GETWALLETSRESPONSE']._serialized_end=393 +# @@protoc_insertion_point(module_scope) diff --git a/hwilib/devices/cypherock_sdk/app_manager/proto/generated/manager/wallet_selector_pb2.py b/hwilib/devices/cypherock_sdk/app_manager/proto/generated/manager/wallet_selector_pb2.py new file mode 100644 index 000000000..2a96f8f0a --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_manager/proto/generated/manager/wallet_selector_pb2.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: manager/wallet_selector.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from .. import error_pb2 as core_dot_error__pb2 +from . import common_pb2 as manager_dot_common__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1dmanager/wallet_selector.proto\x12\x07manager\x1a\x10\x63ore/error.proto\x1a\x14manager/common.proto\"\x1c\n\x1aSelectWalletIntiateRequest\"A\n\x1aSelectWalletResultResponse\x12#\n\x06wallet\x18\x01 \x01(\x0b\x32\x13.manager.WalletItem\"Y\n\x13SelectWalletRequest\x12\x37\n\x08initiate\x18\x01 \x01(\x0b\x32#.manager.SelectWalletIntiateRequestH\x00\x42\t\n\x07request\"\x85\x01\n\x14SelectWalletResponse\x12\x35\n\x06result\x18\x01 \x01(\x0b\x32#.manager.SelectWalletResultResponseH\x00\x12*\n\x0c\x63ommon_error\x18\x02 \x01(\x0b\x32\x12.error.CommonErrorH\x00\x42\n\n\x08responseb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'manager.wallet_selector_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_SELECTWALLETINTIATEREQUEST']._serialized_start=82 + _globals['_SELECTWALLETINTIATEREQUEST']._serialized_end=110 + _globals['_SELECTWALLETRESULTRESPONSE']._serialized_start=112 + _globals['_SELECTWALLETRESULTRESPONSE']._serialized_end=177 + _globals['_SELECTWALLETREQUEST']._serialized_start=179 + _globals['_SELECTWALLETREQUEST']._serialized_end=268 + _globals['_SELECTWALLETRESPONSE']._serialized_start=271 + _globals['_SELECTWALLETRESPONSE']._serialized_end=404 +# @@protoc_insertion_point(module_scope) diff --git a/hwilib/devices/cypherock_sdk/app_manager/proto/types.py b/hwilib/devices/cypherock_sdk/app_manager/proto/types.py new file mode 100644 index 000000000..5f551deb5 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_manager/proto/types.py @@ -0,0 +1,9 @@ +# UpdateFirmwareStatus enum - used for status tracking during firmware update +from enum import Enum + +class UpdateFirmwareStatus(Enum): + UPDATE_FIRMWARE_STATUS_INIT = 0 + UPDATE_FIRMWARE_STATUS_USER_CONFIRMED = 1 + UNRECOGNIZED = -1 + +__all__ = ["UpdateFirmwareStatus"] diff --git a/hwilib/devices/cypherock_sdk/app_manager/utils/__init__.py b/hwilib/devices/cypherock_sdk/app_manager/utils/__init__.py new file mode 100644 index 000000000..3a088f827 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_manager/utils/__init__.py @@ -0,0 +1,10 @@ +from .operations_helper import OperationHelper, decode_result, encode_query +from ...core.utils.common_error import assert_or_throw_invalid_result, parse_common_error + +__all__ = [ + "OperationHelper", + "decode_result", + "encode_query", + "assert_or_throw_invalid_result", + "parse_common_error" +] diff --git a/hwilib/devices/cypherock_sdk/app_manager/utils/operations_helper.py b/hwilib/devices/cypherock_sdk/app_manager/utils/operations_helper.py new file mode 100644 index 000000000..62e2059b1 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/app_manager/utils/operations_helper.py @@ -0,0 +1,103 @@ +from typing import TypeVar, Generic, Callable, Optional, Any, Dict +from ...core.types import ISDK +from ...core.encoders.proto.generated.core_pb2 import Status +from ...errors.app_error import DeviceAppError, DeviceAppErrorType +from ..proto.generated.manager.core_pb2 import Query, Result +from ...core.utils.common_error import assert_or_throw_invalid_result, parse_common_error + +Q = TypeVar("Q") +R = TypeVar("R") + + +def decode_result(data: bytes) -> Result: + """ + Decode result data from bytes using standard protobuf. + + Args: + data: The bytes to decode + + Returns: + Decoded Result object + + Raises: + DeviceAppError: If decoding fails + """ + try: + result = Result() + result.ParseFromString(data) + return result + except Exception as error: + raise DeviceAppError(DeviceAppErrorType.INVALID_MSG_FROM_DEVICE) from error + + +def encode_query(query_data: Dict[str, Any]) -> bytes: + """ + Encode query object to bytes. + + Args: + query_data: Dictionary with query field and value + + Returns: + Encoded query as bytes + """ + query = Query(**query_data) + return query.SerializeToString() + + +class OperationHelper(Generic[Q, R]): + """ + Helper class for managing device operations with typed query and result handling. + """ + + def __init__(self, sdk: ISDK, query_key: str, result_key: str): + """ + Initialize the operation helper. + + Args: + sdk: The SDK instance to use for communication + query_key: The key name for the query field + result_key: The key name for the result field + """ + self.sdk = sdk + self.query_key = query_key + self.result_key = result_key + + def send_query(self, query: Any) -> None: + """ + Send a query to the device. + + Args: + query: The query object to send + """ + query_data = {self.query_key: query} + encoded_query = encode_query(query_data) + return self.sdk.send_query(encoded_query) + + def wait_for_result( + self, on_status: Optional[Callable[[Status], None]] = None + ) -> Any: + """ + Wait for and process the result from the device. + + Args: + on_status: Optional callback for status updates + + Returns: + The result data for the specified result key + + Raises: + DeviceAppError: If the result is invalid or contains errors + """ + params = {"on_status": on_status} if on_status else None + result_data = self.sdk.wait_for_result(params=params) + result = decode_result(result_data) + if result.common_error: + parse_common_error(result.common_error) + + result_value = getattr(result, self.result_key, None) + assert_or_throw_invalid_result(result_value) + + if hasattr(result_value, "common_error") and result_value.common_error: + parse_common_error(result_value.common_error) + + return result_value diff --git a/hwilib/devices/cypherock_sdk/common_utils/__init__.py b/hwilib/devices/cypherock_sdk/common_utils/__init__.py new file mode 100644 index 000000000..01147d68e --- /dev/null +++ b/hwilib/devices/cypherock_sdk/common_utils/__init__.py @@ -0,0 +1,31 @@ +from .assert_utils import assert_condition +from .create_flow_status import create_flow_status +from .create_status_listener import create_status_listener, ForceStatusUpdate, OnStatus +from .crypto import ( + crc16, + is_hex, + format_hex, + hex_to_uint8array, + uint8array_to_hex, + pad_start, + int_to_uint_byte, + hex_to_ascii, + num_to_byte_array, +) + +__all__ = [ + "assert_condition", + "create_flow_status", + "create_status_listener", + "ForceStatusUpdate", + "OnStatus", + "crc16", + "is_hex", + "format_hex", + "hex_to_uint8array", + "uint8array_to_hex", + "pad_start", + "int_to_uint_byte", + "hex_to_ascii", + "num_to_byte_array", +] diff --git a/hwilib/devices/cypherock_sdk/common_utils/assert_utils.py b/hwilib/devices/cypherock_sdk/common_utils/assert_utils.py new file mode 100644 index 000000000..bba65388c --- /dev/null +++ b/hwilib/devices/cypherock_sdk/common_utils/assert_utils.py @@ -0,0 +1,11 @@ +from typing import TypeVar, Union, Any + +T = TypeVar("T") + + +def assert_condition(condition: Any, error: Union[str, Exception]) -> None: + if condition is None or condition is False: + if isinstance(error, str): + raise AssertionError(error) + else: + raise error diff --git a/hwilib/devices/cypherock_sdk/common_utils/create_flow_status.py b/hwilib/devices/cypherock_sdk/common_utils/create_flow_status.py new file mode 100644 index 000000000..990ecd44e --- /dev/null +++ b/hwilib/devices/cypherock_sdk/common_utils/create_flow_status.py @@ -0,0 +1,11 @@ +def create_flow_status(operation_status: int, core_status: int) -> int: + CORE_STATUS_MASK = 0xFF + CORE_STATUS_SHIFT = 8 + APP_STATUS_MASK = 0xFF + APP_STATUS_SHIFT = 0 + + flow_status = 0 + flow_status |= (core_status & CORE_STATUS_MASK) << CORE_STATUS_SHIFT + flow_status |= (operation_status & APP_STATUS_MASK) << APP_STATUS_SHIFT + + return flow_status diff --git a/hwilib/devices/cypherock_sdk/common_utils/create_status_listener.py b/hwilib/devices/cypherock_sdk/common_utils/create_status_listener.py new file mode 100644 index 000000000..85503b897 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/common_utils/create_status_listener.py @@ -0,0 +1,151 @@ +from typing import Dict, Any, Optional, Callable, List, TypedDict, Type +from enum import Enum +from .crypto import num_to_byte_array + +ForceStatusUpdate = Callable[[int], None] +OnStatus = Callable[[Dict[str, int]], None] +EventCallback = Callable[[int], None] + + +class CreateStatusListenerParams(TypedDict): + enums: Type[Enum] + operationEnums: Optional[Type[Enum]] + seedGenerationEnums: Optional[Type[Enum]] + onEvent: Optional[EventCallback] + logger: Optional[Any] + + +def get_numbers_from_enums(enums: Type[Enum]) -> List[int]: + try: + return [ + member.value + for member in enums + if isinstance(member.value, int) and member.value >= 0 + ] + except Exception: + return enums.values() + +def get_names_from_enums(enums: Type[Enum]) -> List[str]: + try: + return [member.name for member in enums] + except Exception: + return enums.keys() + +def create_dict_from_enums(enums: Type[Enum]) -> Dict[str, int]: + values = get_numbers_from_enums(enums) + keys = get_names_from_enums(enums) + return dict(zip(keys, values)) + +def get_numbers_from_dict(enums: Dict[str, int]) -> List[int]: + return list(enums.values()) + +def get_names_from_dict(enums: Dict[str, int]) -> List[str]: + return list(enums.keys()) + +def create_status_listener(params: CreateStatusListenerParams) -> Dict[str, Any]: + enums: Type[Enum] = params["enums"] + enum_dict: Dict[str, int] = create_dict_from_enums(enums) + on_event: Optional[EventCallback] = params.get("onEvent") + logger: Optional[Any] = params.get("logger") + _operation_enums: Optional[Type[Enum]] = params.get("operationEnums") + seed_generation_enums: Optional[Type[Enum]] = params.get("seedGenerationEnums") + + operation_enums: Type[Enum] = ( + _operation_enums if _operation_enums is not None else enums + ) + already_sent: Dict[int, bool] = {} + + event_list = get_numbers_from_dict(enum_dict) + seed_generation_event_list = ( + get_numbers_from_enums(seed_generation_enums) if seed_generation_enums else [] + ) + operation_event_names = get_names_from_dict(enum_dict) + + operation_seed_generation_event_name: Optional[str] = next( + (e for e in operation_event_names if "SEED_GENERATED" in e), None + ) + + def on_status(status: Dict[str, int]) -> None: + flow_status = getattr(status, "flow_status", 0) + byte_array = num_to_byte_array(flow_status) + operation_status = byte_array[-1] if byte_array else 0 + core_status = byte_array[-2] if len(byte_array) > 1 else 0 + + for event_index in event_list: + operation_seed_gen_value = 0 + if operation_seed_generation_event_name: + try: + operation_seed_gen_value = getattr( + operation_enums, operation_seed_generation_event_name + ).value + except AttributeError: + pass + + seed_gen_boundary = len(seed_generation_event_list) - 1 + + diff_event_op_seed = event_index - operation_seed_gen_value + + is_before_seed_generation = ( + not operation_seed_generation_event_name + or event_index < operation_seed_gen_value + ) + + is_seed_generation = ( + operation_seed_generation_event_name + and 0 <= diff_event_op_seed < seed_gen_boundary + ) + + is_after_seed_generation = ( + operation_seed_generation_event_name + and diff_event_op_seed >= seed_gen_boundary + ) + + is_completed = is_before_seed_generation and operation_status >= event_index + + if is_seed_generation: + is_completed = core_status > diff_event_op_seed + elif is_after_seed_generation: + is_completed = operation_status > (diff_event_op_seed + 1) + + if is_completed and not already_sent.get(event_index, False): + already_sent[event_index] = True + + if logger: + event_name = next( + ( + key + for key, value in enum_dict.items() + if value == event_index + ), + str(event_index), + ) + logger.info( + "Event", {"event": event_name, "eventIndex": event_index} + ) + + if on_event: + on_event(event_index) + + def force_status_update(flow_status: int) -> None: + for event_index in event_list: + if flow_status >= event_index and not already_sent.get(event_index, False): + already_sent[event_index] = True + + if logger: + # Find the enum member name corresponding to the event_index + event_name = next( + ( + key + for key, value in enum_dict.items() + if value == event_index + ), + str(event_index), + ) + logger.info( + "Event", {"event": event_name, "eventIndex": event_index} + ) + + if on_event: + on_event(event_index) + + return {"onStatus": on_status, "forceStatusUpdate": force_status_update} diff --git a/hwilib/devices/cypherock_sdk/common_utils/crypto.py b/hwilib/devices/cypherock_sdk/common_utils/crypto.py new file mode 100644 index 000000000..733e9682b --- /dev/null +++ b/hwilib/devices/cypherock_sdk/common_utils/crypto.py @@ -0,0 +1,239 @@ +from typing import List, Union +import re +from .assert_utils import assert_condition + + +def update_crc16(crc_param: int, byte: int) -> int: + """ + Update CRC16 with a single byte. + + Args: + crc_param: Current CRC value + byte: Byte to update with + + Returns: + int: Updated CRC value + """ + assert_condition(crc_param is not None, "Invalid crcParam") + assert_condition(byte is not None, "Invalid byte") + + input_val = byte | 0x100 + crc = crc_param + + while not (input_val & 0x10000): + crc <<= 1 + input_val <<= 1 + if input_val & 0x100: + crc += 1 + if crc & 0x10000: + crc ^= 0x1021 + + return crc & 0xFFFF + + +def crc16(data_buff: bytes) -> int: + """ + Calculate CRC16 for a byte array. + + Args: + data_buff: Byte array to calculate CRC for + + Returns: + int: CRC16 value + """ + assert_condition(data_buff is not None, "Data buffer cannot be empty") + + crc = 0 + for i in data_buff: + crc = update_crc16(crc, i) + + crc = update_crc16(crc, 0) + crc = update_crc16(crc, 0) + + return crc & 0xFFFF + + +def is_hex(maybe_hex: str) -> bool: + """ + Check if a string is a valid hexadecimal. + + Args: + maybe_hex: String to check + + Returns: + bool: True if the string is valid hex, False otherwise + """ + assert_condition(maybe_hex is not None, "Data cannot be empty") + hex_str = maybe_hex + if hex_str.startswith("0x"): + hex_str = hex_str[2:] + return bool(re.match(r"^[a-fA-F0-9]*$", hex_str)) + + +def format_hex(maybe_hex: str) -> str: + """ + Format a hexadecimal string. + + Args: + maybe_hex: Hexadecimal string to format + + Returns: + str: Formatted hexadecimal string + """ + assert_condition(maybe_hex is not None, "Invalid hex") + + hex_str = maybe_hex + if hex_str.startswith("0x"): + hex_str = hex_str[2:] + + assert_condition(is_hex(hex_str), f"Invalid hex string: {maybe_hex}") + + if len(hex_str) % 2 != 0: + hex_str = f"0{hex_str}" + + return hex_str + + +def hex_to_uint8array(data: str) -> bytes: + """ + Convert a hexadecimal string to a byte array. + + Args: + data: Hexadecimal string to convert + + Returns: + bytes: Converted byte array + """ + hex_str = format_hex(data) + + if len(hex_str) <= 0: + return bytes() + + # Split the hex string into pairs of characters + hex_pairs = [hex_str[i: i + 2] for i in range(0, len(hex_str), 2)] + + # Convert each pair to an integer and then to a byte + return bytes(int(pair, 16) for pair in hex_pairs) + + +def uint8array_to_hex(data: bytes) -> str: + """ + Convert a byte array to a hexadecimal string. + + Args: + data: Byte array to convert + + Returns: + str: Hexadecimal string + """ + assert_condition(data is not None, "Invalid data") + + return "".join(f"{i:02x}" for i in data) + + +def pad_start(string: str, target_length: int, pad_string: str) -> str: + """ + Pad the start of a string to a target length. + + Args: + string: String to pad + target_length: Target length of the padded string + pad_string: String to use for padding + + Returns: + str: Padded string + """ + assert_condition(string is not None, "Invalid string") + assert_condition(target_length is not None, "Invalid targetLength") + assert_condition(pad_string is not None, "Invalid padString") + + if len(string) >= target_length: + return string + + if len(pad_string) <= 0: + raise ValueError("padString should not be empty") + + padding_needed = target_length - len(string) + + # Calculate how many times to repeat the pad_string + repeat_count = (padding_needed + len(pad_string) - 1) // len(pad_string) + padding = pad_string * repeat_count + + return padding[:padding_needed] + string + + +def int_to_uint_byte(num: Union[str, int], radix: int) -> str: + """ + Convert an integer to a hexadecimal string with a specific bit width. + + Args: + num: Number to convert + radix: a bit of width (must be a multiple of 8) + + Returns: + str: Hexadecimal string + """ + assert_condition(num is not None, "Invalid number") + assert_condition(radix is not None, "Invalid radix") + + if isinstance(num, str) and num.startswith("0x"): + num_copy = int(num, 16) + else: + num_copy = int(num) + if radix % 8 != 0: + raise ValueError(f"Invalid radix: {radix}") + + if num_copy < 0: + max_number = int("f" * (radix // 4), 16) + num_copy = max_number - abs(num_copy) + 1 + + val = format(num_copy, "x") + no_of_zeroes = radix // 4 - len(val) + + if no_of_zeroes < 0: + raise ValueError(f"Invalid serialization of data: {num} with radix {radix}") + + return "0" * no_of_zeroes + val + + +def hex_to_ascii(hex_str: str) -> str: + """ + Convert a hexadecimal string to ASCII. + + Args: + hex_str: Hexadecimal string to convert + + Returns: + str: ASCII string + """ + assert_condition(hex_str is not None, "Invalid string") + + hex_formatted = format_hex(hex_str) + result = "" + + for i in range(0, len(hex_formatted), 2): + char_code = int(hex_formatted[i: i + 2], 16) + result += chr(char_code) + + return result + + +def num_to_byte_array(num: int) -> List[int]: + """ + Convert a number to a byte array. + + Args: + num: Number to convert + + Returns: + List[int]: Byte array + """ + n = num + byte_array = [] + + while n > 0: + byte = n & 0xFF + byte_array.append(byte) + n = (n - byte) // 256 + + return list(reversed(byte_array)) diff --git a/hwilib/devices/cypherock_sdk/core/README.md b/hwilib/devices/cypherock_sdk/core/README.md new file mode 100644 index 000000000..b0da7a822 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/README.md @@ -0,0 +1,3 @@ +# core + +This package provides the core SDK functionality for Cypherock X1. diff --git a/hwilib/devices/cypherock_sdk/core/__init__.py b/hwilib/devices/cypherock_sdk/core/__init__.py new file mode 100644 index 000000000..4adf95ade --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/__init__.py @@ -0,0 +1,5 @@ +from .sdk import SDK + +__all__ = [ + "SDK", +] diff --git a/hwilib/devices/cypherock_sdk/core/config/__init__.py b/hwilib/devices/cypherock_sdk/core/config/__init__.py new file mode 100644 index 000000000..686d860af --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/config/__init__.py @@ -0,0 +1,23 @@ +from . import command +from . import constants +from . import radix +from types import SimpleNamespace + +# Version configurations +v1 = SimpleNamespace( + commands=command.v1, + constants=constants.v1, + radix=radix.v1, +) +v2 = SimpleNamespace( + commands=command.v1, + constants=constants.v2, + radix=radix.v2, +) +v3 = SimpleNamespace( + commands=command.v3, + constants=constants.v3, + radix=radix.v3, +) + +__all__ = ["v1", "v2", "v3", "command", "constants", "radix"] diff --git a/hwilib/devices/cypherock_sdk/core/config/command.py b/hwilib/devices/cypherock_sdk/core/config/command.py new file mode 100644 index 000000000..2d73153a3 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/config/command.py @@ -0,0 +1,31 @@ +from types import SimpleNamespace + + +def dict_to_namespace(d): + if isinstance(d, dict): + return SimpleNamespace(**{k: dict_to_namespace(v) for k, v in d.items()}) + return d + + +v1 = dict_to_namespace( + { + "ACK_PACKET": 1, + "NACK_PACKET": 7, + "USB_CONNECTION_STATE_PACKET": 8, + } +) + +v3 = dict_to_namespace( + { + "PACKET_TYPE": { + "STATUS_REQ": 1, + "CMD": 2, + "CMD_OUTPUT_REQ": 3, + "STATUS": 4, + "CMD_ACK": 5, + "CMD_OUTPUT": 6, + "ERROR": 7, + "ABORT": 8, + }, + } +) diff --git a/hwilib/devices/cypherock_sdk/core/config/constants.py b/hwilib/devices/cypherock_sdk/core/config/constants.py new file mode 100644 index 000000000..9f5477b23 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/config/constants.py @@ -0,0 +1,58 @@ +from types import SimpleNamespace + + +def dict_to_namespace(d): + if isinstance(d, dict): + return SimpleNamespace(**{k: dict_to_namespace(v) for k, v in d.items()}) + return d + + +# Device hardware constants +DEVICE_CONSTANTS = { + "VENDOR_ID": 0x0483, + "PRODUCT_ID": 0x5741, + "USAGE_PAGE": 0xFF00, + "USAGE": 0x0001, + "INTERFACE_NUMBER": 0, + "ENDPOINT_IN": 0x81, + "ENDPOINT_OUT": 0x01, + "PACKET_SIZE": 64, + "TIMEOUT": 1000, +} + +DEVICE_STATES = {"BOOTLOADER": 0, "FIRMWARE": 1, "INITIAL": 2} + +# Version-specific protocol constants +v1 = dict_to_namespace( + { + "START_OF_FRAME": "AA", + "STUFFING_BYTE": 0xAA, + "ACK_BYTE": "06", + "CHUNK_SIZE": 32 * 2, + "ACK_TIME": 2000, + "RECHECK_TIME": 50, + } +) +v2 = dict_to_namespace( + { + "START_OF_FRAME": "5A5A", + "STUFFING_BYTE": 0x5A, + "ACK_BYTE": "06", + "CHUNK_SIZE": 32 * 2, + "ACK_TIME": 2000, + "RECHECK_TIME": 50, + } +) +v3 = dict_to_namespace( + { + "START_OF_FRAME": "5555", + "STUFFING_BYTE": 0x5A, + "ACK_BYTE": "06", + "CHUNK_SIZE": 48 * 2, + "ACK_TIME": 2000, + "IDLE_TIMEOUT": 4000, + "CMD_RESPONSE_TIME": 2000, + "RECHECK_TIME": 2, + "IDLE_RECHECK_TIME": 200, + } +) diff --git a/hwilib/devices/cypherock_sdk/core/config/radix.py b/hwilib/devices/cypherock_sdk/core/config/radix.py new file mode 100644 index 000000000..ad9a90132 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/config/radix.py @@ -0,0 +1,86 @@ +from types import SimpleNamespace + + +def dict_to_namespace(d): + if isinstance(d, dict): + return SimpleNamespace(**{k: dict_to_namespace(v) for k, v in d.items()}) + return d + + +# Base radix constants +RADIX_HEX = 16 +RADIX_DECIMAL = 10 +RADIX_OCTAL = 8 +RADIX_BINARY = 2 + +# Version-specific radix configurations +v1 = dict_to_namespace( + { + "current_packet_number": 16, + "total_packet": 16, + "data_size": 8, + "command_type": 8, + "wallet_index": 8, + "coin_type": 8, + "future_use": 8, + "input_output_count": 8, + "address_index": 32, + "account_index": 8, + "crc": 16, + "output_length": 8, + "add_coins": { + "wallet": 128, + "no_of_coins": 8, + "coin_type": 32, + }, + "receive_address": { + "coin_type": 32, + "account_index": 32, + }, + } +) +v2 = dict_to_namespace( + { + "current_packet_number": 16, + "total_packet": 16, + "data_size": 8, + "command_type": 8 * 4, + "wallet_index": 8, + "coin_type": 8, + "future_use": 8, + "input_output_count": 8, + "address_index": 32, + "account_index": 8, + "crc": 16, + "output_length": 8, + "add_coins": { + "wallet": 128, + "no_of_coins": 8, + "coin_type": 32, + }, + "receive_address": { + "coin_type": 32, + "account_index": 32, + }, + } +) +v3 = dict_to_namespace( + { + "current_packet_number": 16, + "total_packet": 16, + "sequence_number": 16, + "packet_type": 8, + "command_type": 32, + "payload_length": 8, + "timestamp_length": 32, + "data_size": 16, + "crc": 16, + "status": { + "device_state": 8, + "abort_disabled": 8, + "current_cmd_seq": 16, + "cmd_state": 8, + "flow_status": 16, + }, + } +) diff --git a/hwilib/devices/cypherock_sdk/core/encoders/__init__.py b/hwilib/devices/cypherock_sdk/core/encoders/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/hwilib/devices/cypherock_sdk/core/encoders/packet/packet.py b/hwilib/devices/cypherock_sdk/core/encoders/packet/packet.py new file mode 100644 index 000000000..8eb606070 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/encoders/packet/packet.py @@ -0,0 +1,356 @@ +import time +from typing import TypedDict, List, Dict +from enum import Enum +from ...config import v3 +from ....common_utils import ( + assert_condition, + is_hex, + uint8array_to_hex, + hex_to_uint8array, + int_to_uint_byte, + crc16, +) +from ...utils.packet_version import PacketVersion, PacketVersionMap +from ....errors import DeviceCompatibilityError, DeviceCompatibilityErrorType + + +class DecodedPacketData(TypedDict): + start_of_frame: str + current_packet_number: int + total_packet_number: int + payload_data: str + crc: str + sequence_number: int + packet_type: int + error_list: List[str] + timestamp: int + + +class ErrorPacketRejectReason(Enum): + NO_ERROR = 0 + CHECKSUM_ERROR = 1 + BUSY_PREVIOUS_CMD = 2 + OUT_OF_ORDER_CHUNK = 3 + INVALID_CHUNK_COUNT = 4 + INVALID_SEQUENCE_NO = 5 + INVALID_PAYLOAD_LENGTH = 6 + APP_BUFFER_BLOCKED = 7 + NO_MORE_CHUNKS = 8 + INVALID_PACKET_TYPE = 9 + INVALID_CHUNK_NO = 10 + INCOMPLETE_PACKET = 11 + + +RejectReasonToMsgMap: Dict[ErrorPacketRejectReason, str] = { + ErrorPacketRejectReason.NO_ERROR: "No error", + ErrorPacketRejectReason.CHECKSUM_ERROR: "Checksum error", + ErrorPacketRejectReason.BUSY_PREVIOUS_CMD: "Device is busy on previous command", + ErrorPacketRejectReason.OUT_OF_ORDER_CHUNK: "Chunk out of order", + ErrorPacketRejectReason.INVALID_CHUNK_COUNT: "Invalid chunk count", + ErrorPacketRejectReason.INVALID_SEQUENCE_NO: "Invalid sequence number", + ErrorPacketRejectReason.INVALID_PAYLOAD_LENGTH: "Invalid payload length", + ErrorPacketRejectReason.APP_BUFFER_BLOCKED: "Application buffer blocked", + ErrorPacketRejectReason.NO_MORE_CHUNKS: "No more chunks", + ErrorPacketRejectReason.INVALID_PACKET_TYPE: "Invalid packet type", + ErrorPacketRejectReason.INVALID_CHUNK_NO: "Invalid chunk number", + ErrorPacketRejectReason.INCOMPLETE_PACKET: "Incomplete packet", +} + + +def encode_payload_data( + raw_data: str, + protobuf_data: str, + version: PacketVersion, +) -> str: + assert_condition(raw_data, "Invalid rawData") + assert_condition(protobuf_data, "Invalid protobufData") + assert_condition(version, "Invalid version") + assert_condition(is_hex(raw_data), "Invalid hex in rawData") + assert_condition(is_hex(protobuf_data), "Invalid hex in protobufData") + + if version != PacketVersionMap.v3: + raise DeviceCompatibilityError( + DeviceCompatibilityErrorType.INVALID_SDK_OPERATION, + ) + + if len(raw_data) == 0 and len(protobuf_data) == 0: + return "" + + usable_config = v3 + + serialized_raw_data_length = int_to_uint_byte( + len(raw_data) // 2, + usable_config.radix.data_size, + ) + serialized_protobuf_data_length = int_to_uint_byte( + len(protobuf_data) // 2, + usable_config.radix.data_size, + ) + + return ( + serialized_protobuf_data_length + + serialized_raw_data_length + + protobuf_data + + raw_data + ) + + +def encode_packet( + raw_data: str = "", + proto_data: str = "", + version: PacketVersion = PacketVersionMap.v3, + sequence_number: int = 0, + packet_type: int = 0, +) -> List[bytes]: + assert_condition(raw_data or proto_data, "Invalid data") + assert_condition(version, "Invalid version") + assert_condition(sequence_number is not None, "Invalid sequenceNumber") + assert_condition(packet_type is not None, "Invalid packetType") + + if raw_data: + assert_condition(is_hex(raw_data), "Invalid hex in raw data") + if proto_data: + assert_condition(is_hex(proto_data), "Invalid hex in proto data") + + assert_condition(packet_type > 0, "Packet type cannot be negative") + + if version != PacketVersionMap.v3: + raise DeviceCompatibilityError( + DeviceCompatibilityErrorType.INVALID_SDK_OPERATION, + ) + + usable_config = v3 + + serialized_sequence_number = int_to_uint_byte( + sequence_number, + usable_config.radix.sequence_number, + ) + serialized_packet_type = int_to_uint_byte( + packet_type, + usable_config.radix.packet_type, + ) + + chunk_size = usable_config.constants.CHUNK_SIZE + start_of_frame = usable_config.constants.START_OF_FRAME + + serialized_data = encode_payload_data( + raw_data, + proto_data, + version, + ) + + rounds = (len(serialized_data) + chunk_size - 1) // chunk_size + has_no_data = len(serialized_data) == 0 + if has_no_data: + rounds = 1 + + packet_list: List[bytes] = [] + + for i in range(1, rounds + 1): + current_packet_number = int_to_uint_byte( + i, + usable_config.radix.current_packet_number, + ) + total_packet_number = int_to_uint_byte( + rounds, + usable_config.radix.total_packet, + ) + data_chunk = serialized_data[ + (i - 1) * chunk_size: (i - 1) * chunk_size + chunk_size + ] + payload = data_chunk + payload_length = int_to_uint_byte( + len(data_chunk) // 2, + usable_config.radix.payload_length, + ) + # Match TypeScript behavior: Date.now().toString().slice(0, timestampLength / 4) + timestamp_ms = int(time.time() * 1000) # JavaScript Date.now() equivalent + timestamp_str = str(timestamp_ms)[: usable_config.radix.timestamp_length // 4] + serialized_timestamp = int_to_uint_byte( + int(timestamp_str), + usable_config.radix.timestamp_length, + ) + + comm_data = ( + current_packet_number + + total_packet_number + + serialized_sequence_number + + serialized_packet_type + + serialized_timestamp + + payload_length + + payload + ) + crc = int_to_uint_byte( + crc16(hex_to_uint8array(comm_data)), + usable_config.radix.crc, + ) + packet = start_of_frame + crc + comm_data + packet_list.append(hex_to_uint8array(packet)) + + return packet_list + + +def decode_packet( + param: bytes, + version: PacketVersion, +) -> List[DecodedPacketData]: + if version != PacketVersionMap.v3: + raise DeviceCompatibilityError( + DeviceCompatibilityErrorType.INVALID_SDK_OPERATION, + ) + + usable_config = v3 + start_of_frame = usable_config.constants.START_OF_FRAME + + data = uint8array_to_hex(param).lower() + packet_list: List[DecodedPacketData] = [] + offset = data.find(start_of_frame) + + while len(data) > 0: + offset = data.find(start_of_frame) + if offset == -1: + return packet_list + + # Add bounds checking for all field reads + if offset + len(start_of_frame) > len(data): + break + start_of_frame = data[offset: offset + len(start_of_frame)] + offset += len(start_of_frame) + + if offset + usable_config.radix.crc // 4 > len(data): + break + crc = data[offset: offset + usable_config.radix.crc // 4] + offset += usable_config.radix.crc // 4 + + if offset + usable_config.radix.current_packet_number // 4 > len(data): + break + current_packet_number = int( + data[offset: offset + usable_config.radix.current_packet_number // 4], + 16, + ) + offset += usable_config.radix.current_packet_number // 4 + + if offset + usable_config.radix.total_packet // 4 > len(data): + break + total_packet_number = int( + data[offset: offset + usable_config.radix.total_packet // 4], + 16, + ) + offset += usable_config.radix.total_packet // 4 + + if offset + usable_config.radix.sequence_number // 4 > len(data): + break + sequence_number = int( + data[offset: offset + usable_config.radix.sequence_number // 4], + 16, + ) + offset += usable_config.radix.sequence_number // 4 + + if offset + usable_config.radix.packet_type // 4 > len(data): + break + packet_type = int( + data[offset: offset + usable_config.radix.packet_type // 4], + 16, + ) + offset += usable_config.radix.packet_type // 4 + + if offset + usable_config.radix.timestamp_length // 4 > len(data): + break + timestamp = int( + data[offset: offset + usable_config.radix.timestamp_length // 4], + 16, + ) + offset += usable_config.radix.timestamp_length // 4 + + if offset + usable_config.radix.payload_length // 4 > len(data): + break + payload_length = int( + data[offset: offset + usable_config.radix.payload_length // 4], + 16, + ) + offset += usable_config.radix.payload_length // 4 + + payload_data = "" + if payload_length != 0: + available_length = len(data) - offset + read_length = min(payload_length * 2, available_length) + if read_length > 0: + payload_data = data[offset: offset + read_length] + offset += read_length + data = data[offset:] + + comm_data = ( + int_to_uint_byte( + current_packet_number, usable_config.radix.current_packet_number + ) + + int_to_uint_byte(total_packet_number, usable_config.radix.total_packet) + + int_to_uint_byte(sequence_number, usable_config.radix.sequence_number) + + int_to_uint_byte(packet_type, usable_config.radix.packet_type) + + int_to_uint_byte(timestamp, usable_config.radix.timestamp_length) + + int_to_uint_byte(payload_length, usable_config.radix.payload_length) + + payload_data + ) + actual_crc = int_to_uint_byte( + crc16(hex_to_uint8array(comm_data)), + usable_config.radix.crc, + ) + + error_list = [] + if start_of_frame.upper() != start_of_frame.upper(): + error_list.append("Invalid Start of frame") + if current_packet_number > total_packet_number: + error_list.append( + "current_packet_number is greater than total_packet_number" + ) + if actual_crc.upper() != crc.upper(): + error_list.append("invalid crc") + + packet_list.append( + DecodedPacketData( + start_of_frame=start_of_frame, + current_packet_number=current_packet_number, + total_packet_number=total_packet_number, + crc=crc, + payload_data=payload_data, + error_list=error_list, + sequence_number=sequence_number, + packet_type=packet_type, + timestamp=timestamp, + ) + ) + return packet_list + + +def decode_payload_data(payload: str, version: PacketVersion) -> Dict[str, str]: + assert_condition(payload, "Invalid payload") + assert_condition(version, "Invalid version") + assert_condition(is_hex(payload), "Invalid hex in payload") + + if version != PacketVersionMap.v3: + raise DeviceCompatibilityError( + DeviceCompatibilityErrorType.INVALID_SDK_OPERATION, + ) + + usable_config = v3 + payload_offset = 0 + + data_size_half = usable_config.radix.data_size // 4 + + protobuf_data_size = int( + payload[payload_offset: payload_offset + data_size_half], 16 + ) + payload_offset += data_size_half + + raw_data_size = int(payload[payload_offset: payload_offset + data_size_half], 16) + payload_offset += data_size_half + + protobuf_data = payload[payload_offset: payload_offset + protobuf_data_size * 2] + payload_offset += protobuf_data_size * 2 + + raw_data = payload[payload_offset: payload_offset + raw_data_size * 2] + payload_offset += raw_data_size * 2 + + return { + "protobuf_data": protobuf_data, + "raw_data": raw_data, + } diff --git a/hwilib/devices/cypherock_sdk/core/encoders/proto/generated/common_pb2.py b/hwilib/devices/cypherock_sdk/core/encoders/proto/generated/common_pb2.py new file mode 100644 index 000000000..e19454d5c --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/encoders/proto/generated/common_pb2.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: core/common.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x11\x63ore/common.proto\x12\x06\x63ommon\"6\n\x07Version\x12\r\n\x05major\x18\x01 \x01(\r\x12\r\n\x05minor\x18\x02 \x01(\r\x12\r\n\x05patch\x18\x03 \x01(\r\"`\n\x0c\x43hunkPayload\x12\r\n\x05\x63hunk\x18\x01 \x01(\x0c\x12\x16\n\x0eremaining_size\x18\x02 \x01(\r\x12\x13\n\x0b\x63hunk_index\x18\x03 \x01(\r\x12\x14\n\x0ctotal_chunks\x18\x04 \x01(\r\"\x1f\n\x08\x43hunkAck\x12\x13\n\x0b\x63hunk_index\x18\x01 \x01(\r*\x83\x01\n\x14SeedGenerationStatus\x12\x1f\n\x1bSEED_GENERATION_STATUS_INIT\x10\x00\x12%\n!SEED_GENERATION_STATUS_PASSPHRASE\x10\x01\x12#\n\x1fSEED_GENERATION_STATUS_PIN_CARD\x10\x02\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'core.common_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_SEEDGENERATIONSTATUS']._serialized_start=217 + _globals['_SEEDGENERATIONSTATUS']._serialized_end=348 + _globals['_VERSION']._serialized_start=29 + _globals['_VERSION']._serialized_end=83 + _globals['_CHUNKPAYLOAD']._serialized_start=85 + _globals['_CHUNKPAYLOAD']._serialized_end=181 + _globals['_CHUNKACK']._serialized_start=183 + _globals['_CHUNKACK']._serialized_end=214 +# @@protoc_insertion_point(module_scope) diff --git a/hwilib/devices/cypherock_sdk/core/encoders/proto/generated/core_pb2.py b/hwilib/devices/cypherock_sdk/core/encoders/proto/generated/core_pb2.py new file mode 100644 index 000000000..66a00735c --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/encoders/proto/generated/core_pb2.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: core/core.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from . import version_pb2 as core_dot_version__pb2 +from . import session_pb2 as core_dot_session__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0f\x63ore/core.proto\x12\x04\x63ore\x1a\x12\x63ore/version.proto\x1a\x12\x63ore/session.proto\"<\n\x08\x45rrorCmd\x12\x11\n\tapplet_id\x18\x01 \x01(\r\x12\x1d\n\x04type\x18\x02 \x01(\x0e\x32\x0f.core.ErrorType\"\xd5\x01\n\x06Status\x12\x30\n\x11\x64\x65vice_waiting_on\x18\x01 \x01(\x0e\x32\x15.core.DeviceWaitingOn\x12\x30\n\x11\x64\x65vice_idle_state\x18\x02 \x01(\x0e\x32\x15.core.DeviceIdleState\x12\x16\n\x0e\x61\x62ort_disabled\x18\x03 \x01(\x08\x12\x17\n\x0f\x63urrent_cmd_seq\x18\x04 \x01(\r\x12!\n\tcmd_state\x18\x05 \x01(\x0e\x32\x0e.core.CmdState\x12\x13\n\x0b\x66low_status\x18\x06 \x01(\r\"\x1c\n\x07\x43ommand\x12\x11\n\tapplet_id\x18\x01 \x01(\r\"\xd8\x01\n\x03Msg\x12\x1c\n\x03\x63md\x18\x01 \x01(\x0b\x32\r.core.CommandH\x00\x12\x1f\n\x05\x65rror\x18\x02 \x01(\x0b\x32\x0e.core.ErrorCmdH\x00\x12*\n\x0b\x61pp_version\x18\x03 \x01(\x0b\x32\x13.core.AppVersionCmdH\x00\x12.\n\rsession_start\x18\x04 \x01(\x0b\x32\x15.core.SessionStartCmdH\x00\x12.\n\rsession_close\x18\x05 \x01(\x0b\x32\x15.core.SessionCloseCmdH\x00\x42\x06\n\x04type*\x90\x01\n\x0f\x44\x65viceWaitingOn\x12\x1a\n\x16\x44\x45VICE_WAITING_ON_NULL\x10\x00\x12\x1a\n\x16\x44\x45VICE_WAITING_ON_IDLE\x10\x01\x12\"\n\x1e\x44\x45VICE_WAITING_ON_BUSY_IP_CARD\x10\x02\x12!\n\x1d\x44\x45VICE_WAITING_ON_BUSY_IP_KEY\x10\x03*\x82\x01\n\x0f\x44\x65viceIdleState\x12\x1a\n\x16\x44\x45VICE_IDLE_STATE_NULL\x10\x00\x12\x1a\n\x16\x44\x45VICE_IDLE_STATE_IDLE\x10\x01\x12\x19\n\x15\x44\x45VICE_IDLE_STATE_USB\x10\x02\x12\x1c\n\x18\x44\x45VICE_IDLE_STATE_DEVICE\x10\x03*\xad\x01\n\x08\x43mdState\x12\x12\n\x0e\x43MD_STATE_NONE\x10\x00\x12\x17\n\x13\x43MD_STATE_RECEIVING\x10\x01\x12\x16\n\x12\x43MD_STATE_RECEIVED\x10\x02\x12\x17\n\x13\x43MD_STATE_EXECUTING\x10\x03\x12\x12\n\x0e\x43MD_STATE_DONE\x10\x04\x12\x14\n\x10\x43MD_STATE_FAILED\x10\x05\x12\x19\n\x15\x43MD_STATE_INVALID_CMD\x10\x06*\x85\x01\n\tErrorType\x12\x0c\n\x08NO_ERROR\x10\x00\x12\x0f\n\x0bUNKNOWN_APP\x10\x01\x12\x0f\n\x0bINVALID_MSG\x10\x02\x12\x12\n\x0e\x41PP_NOT_ACTIVE\x10\x03\x12\x18\n\x14\x41PP_TIMEOUT_OCCURRED\x10\x04\x12\x1a\n\x16\x44\x45VICE_SESSION_INVALID\x10\x05\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'core.core_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_DEVICEWAITINGON']._serialized_start=593 + _globals['_DEVICEWAITINGON']._serialized_end=737 + _globals['_DEVICEIDLESTATE']._serialized_start=740 + _globals['_DEVICEIDLESTATE']._serialized_end=870 + _globals['_CMDSTATE']._serialized_start=873 + _globals['_CMDSTATE']._serialized_end=1046 + _globals['_ERRORTYPE']._serialized_start=1049 + _globals['_ERRORTYPE']._serialized_end=1182 + _globals['_ERRORCMD']._serialized_start=65 + _globals['_ERRORCMD']._serialized_end=125 + _globals['_STATUS']._serialized_start=128 + _globals['_STATUS']._serialized_end=341 + _globals['_COMMAND']._serialized_start=343 + _globals['_COMMAND']._serialized_end=371 + _globals['_MSG']._serialized_start=374 + _globals['_MSG']._serialized_end=590 +# @@protoc_insertion_point(module_scope) diff --git a/hwilib/devices/cypherock_sdk/core/encoders/proto/generated/error_pb2.py b/hwilib/devices/cypherock_sdk/core/encoders/proto/generated/error_pb2.py new file mode 100644 index 000000000..174fa840a --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/encoders/proto/generated/error_pb2.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: core/error.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10\x63ore/error.proto\x12\x05\x65rror\"\xd1\x02\n\x0b\x43ommonError\x12\x17\n\runknown_error\x18\x01 \x01(\rH\x00\x12\'\n\x0c\x63orrupt_data\x18\x02 \x01(\x0e\x32\x0f.error.DataFlowH\x00\x12\x1f\n\x15\x64\x65vice_setup_required\x18\x04 \x01(\rH\x00\x12\x31\n\x10wallet_not_found\x18\x0b \x01(\x0e\x32\x15.error.WalletNotFoundH\x00\x12\x39\n\x14wallet_partial_state\x18\x0c \x01(\x0e\x32\x19.error.WalletPartialStateH\x00\x12&\n\ncard_error\x18\x15 \x01(\x0e\x32\x10.error.CardErrorH\x00\x12.\n\x0euser_rejection\x18\x16 \x01(\x0e\x32\x14.error.UserRejectionH\x00\x42\x07\n\x05\x65rrorJ\x04\x08\x03\x10\x04J\x04\x08\x05\x10\x0bJ\x04\x08\r\x10\x15*l\n\x0eWalletNotFound\x12\x1c\n\x18WALLET_NOT_FOUND_UNKNOWN\x10\x00\x12\x1e\n\x1aWALLET_NOT_FOUND_ON_DEVICE\x10\x01\x12\x1c\n\x18WALLET_NOT_FOUND_ON_CARD\x10\x02*\xc3\x01\n\x12WalletPartialState\x12 \n\x1cWALLET_PARTIAL_STATE_UNKNOWN\x10\x00\x12\x1f\n\x1bWALLET_PARTIAL_STATE_LOCKED\x10\x01\x12\x1f\n\x1bWALLET_PARTIAL_STATE_DELETE\x10\x02\x12#\n\x1fWALLET_PARTIAL_STATE_UNVERIFIED\x10\x03\x12$\n WALLET_PARTIAL_STATE_OUT_OF_SYNC\x10\x04*\xa7\x05\n\tCardError\x12\x16\n\x12\x43\x41RD_ERROR_UNKNOWN\x10\x00\x12\x19\n\x15\x43\x41RD_ERROR_NOT_PAIRED\x10\x01\x12%\n!CARD_ERROR_SW_INCOMPATIBLE_APPLET\x10\x03\x12(\n$CARD_ERROR_SW_NULL_POINTER_EXCEPTION\x10\x04\x12\'\n#CARD_ERROR_SW_TRANSACTION_EXCEPTION\x10\x05\x12\x1e\n\x1a\x43\x41RD_ERROR_SW_FILE_INVALID\x10\x06\x12\x33\n/CARD_ERROR_SW_SECURITY_CONDITIONS_NOT_SATISFIED\x10\x07\x12*\n&CARD_ERROR_SW_CONDITIONS_NOT_SATISFIED\x10\x08\x12\x1c\n\x18\x43\x41RD_ERROR_SW_WRONG_DATA\x10\t\x12 \n\x1c\x43\x41RD_ERROR_SW_FILE_NOT_FOUND\x10\n\x12\"\n\x1e\x43\x41RD_ERROR_SW_RECORD_NOT_FOUND\x10\x0b\x12\x1b\n\x17\x43\x41RD_ERROR_SW_FILE_FULL\x10\x0c\x12#\n\x1f\x43\x41RD_ERROR_SW_CORRECT_LENGTH_00\x10\r\x12\x1d\n\x19\x43\x41RD_ERROR_SW_INVALID_INS\x10\x0e\x12\x1c\n\x18\x43\x41RD_ERROR_SW_NOT_PAIRED\x10\x0f\x12\"\n\x1e\x43\x41RD_ERROR_SW_CRYPTO_EXCEPTION\x10\x10\x12#\n\x1f\x43\x41RD_ERROR_POW_SW_WALLET_LOCKED\x10\x11\x12\x1d\n\x19\x43\x41RD_ERROR_SW_INS_BLOCKED\x10\x12\x12!\n\x1d\x43\x41RD_ERROR_SW_OUT_OF_BOUNDARY\x10\x13*L\n\rUserRejection\x12\x1a\n\x16USER_REJECTION_UNKNOWN\x10\x00\x12\x1f\n\x1bUSER_REJECTION_CONFIRMATION\x10\x01*\xe1\x01\n\x08\x44\x61taFlow\x12\x1d\n\x19\x44\x41TA_FLOW_DECODING_FAILED\x10\x00\x12\x1b\n\x17\x44\x41TA_FLOW_INVALID_QUERY\x10\x01\x12\x1b\n\x17\x44\x41TA_FLOW_FIELD_MISSING\x10\x02\x12\x1d\n\x19\x44\x41TA_FLOW_INVALID_REQUEST\x10\x03\x12 \n\x1c\x44\x41TA_FLOW_INACTIVITY_TIMEOUT\x10\x04\x12\x1a\n\x16\x44\x41TA_FLOW_INVALID_DATA\x10\x05\x12\x1f\n\x1b\x44\x41TA_FLOW_QUERY_NOT_ALLOWED\x10\x06\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'core.error_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_WALLETNOTFOUND']._serialized_start=367 + _globals['_WALLETNOTFOUND']._serialized_end=475 + _globals['_WALLETPARTIALSTATE']._serialized_start=478 + _globals['_WALLETPARTIALSTATE']._serialized_end=673 + _globals['_CARDERROR']._serialized_start=676 + _globals['_CARDERROR']._serialized_end=1355 + _globals['_USERREJECTION']._serialized_start=1357 + _globals['_USERREJECTION']._serialized_end=1433 + _globals['_DATAFLOW']._serialized_start=1436 + _globals['_DATAFLOW']._serialized_end=1661 + _globals['_COMMONERROR']._serialized_start=28 + _globals['_COMMONERROR']._serialized_end=365 +# @@protoc_insertion_point(module_scope) diff --git a/hwilib/devices/cypherock_sdk/core/encoders/proto/generated/session_pb2.py b/hwilib/devices/cypherock_sdk/core/encoders/proto/generated/session_pb2.py new file mode 100644 index 000000000..2749207e6 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/encoders/proto/generated/session_pb2.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: core/session.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from . import error_pb2 as core_dot_error__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x12\x63ore/session.proto\x12\x04\x63ore\x1a\x10\x63ore/error.proto\"\x1d\n\x1bSessionStartInitiateRequest\"\x9f\x01\n\"SessionStartInitiateResultResponse\x12\x1c\n\x14\x64\x65vice_random_public\x18\x01 \x01(\x0c\x12\x11\n\tdevice_id\x18\x02 \x01(\x0c\x12\x11\n\tsignature\x18\x03 \x01(\x0c\x12\x10\n\x08postfix1\x18\x04 \x01(\x0c\x12\x10\n\x08postfix2\x18\x05 \x01(\x0c\x12\x11\n\tkey_index\x18\x06 \x01(\r\"t\n\x18SessionStartBeginRequest\x12\x1d\n\x15session_random_public\x18\x01 \x01(\x0c\x12\x13\n\x0bsession_age\x18\x02 \x01(\r\x12\x11\n\tsignature\x18\x03 \x01(\x0c\x12\x11\n\tdevice_id\x18\x04 \x01(\x0c\"\x19\n\x17SessionStartAckResponse\"\x88\x01\n\x13SessionStartRequest\x12\x35\n\x08initiate\x18\x01 \x01(\x0b\x32!.core.SessionStartInitiateRequestH\x00\x12/\n\x05start\x18\x02 \x01(\x0b\x32\x1e.core.SessionStartBeginRequestH\x00\x42\t\n\x07request\"\xd6\x01\n\x14SessionStartResponse\x12I\n\x15\x63onfirmation_initiate\x18\x01 \x01(\x0b\x32(.core.SessionStartInitiateResultResponseH\x00\x12;\n\x12\x63onfirmation_start\x18\x02 \x01(\x0b\x32\x1d.core.SessionStartAckResponseH\x00\x12*\n\x0c\x63ommon_error\x18\x05 \x01(\x0b\x32\x12.error.CommonErrorH\x00\x42\n\n\x08response\"v\n\x0fSessionStartCmd\x12,\n\x07request\x18\x01 \x01(\x0b\x32\x19.core.SessionStartRequestH\x00\x12.\n\x08response\x18\x02 \x01(\x0b\x32\x1a.core.SessionStartResponseH\x00\x42\x05\n\x03\x63md\"\x1a\n\x18SessionCloseClearRequest\"\x1b\n\x19SessionCloseClearResponse\"Q\n\x13SessionCloseRequest\x12/\n\x05\x63lear\x18\x01 \x01(\x0b\x32\x1e.core.SessionCloseClearRequestH\x00\x42\t\n\x07request\"\x80\x01\n\x14SessionCloseResponse\x12\x30\n\x05\x63lear\x18\x01 \x01(\x0b\x32\x1f.core.SessionCloseClearResponseH\x00\x12*\n\x0c\x63ommon_error\x18\x02 \x01(\x0b\x32\x12.error.CommonErrorH\x00\x42\n\n\x08response\"v\n\x0fSessionCloseCmd\x12,\n\x07request\x18\x01 \x01(\x0b\x32\x19.core.SessionCloseRequestH\x00\x12.\n\x08response\x18\x02 \x01(\x0b\x32\x1a.core.SessionCloseResponseH\x00\x42\x05\n\x03\x63mdb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'core.session_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_SESSIONSTARTINITIATEREQUEST']._serialized_start=46 + _globals['_SESSIONSTARTINITIATEREQUEST']._serialized_end=75 + _globals['_SESSIONSTARTINITIATERESULTRESPONSE']._serialized_start=78 + _globals['_SESSIONSTARTINITIATERESULTRESPONSE']._serialized_end=237 + _globals['_SESSIONSTARTBEGINREQUEST']._serialized_start=239 + _globals['_SESSIONSTARTBEGINREQUEST']._serialized_end=355 + _globals['_SESSIONSTARTACKRESPONSE']._serialized_start=357 + _globals['_SESSIONSTARTACKRESPONSE']._serialized_end=382 + _globals['_SESSIONSTARTREQUEST']._serialized_start=385 + _globals['_SESSIONSTARTREQUEST']._serialized_end=521 + _globals['_SESSIONSTARTRESPONSE']._serialized_start=524 + _globals['_SESSIONSTARTRESPONSE']._serialized_end=738 + _globals['_SESSIONSTARTCMD']._serialized_start=740 + _globals['_SESSIONSTARTCMD']._serialized_end=858 + _globals['_SESSIONCLOSECLEARREQUEST']._serialized_start=860 + _globals['_SESSIONCLOSECLEARREQUEST']._serialized_end=886 + _globals['_SESSIONCLOSECLEARRESPONSE']._serialized_start=888 + _globals['_SESSIONCLOSECLEARRESPONSE']._serialized_end=915 + _globals['_SESSIONCLOSEREQUEST']._serialized_start=917 + _globals['_SESSIONCLOSEREQUEST']._serialized_end=998 + _globals['_SESSIONCLOSERESPONSE']._serialized_start=1001 + _globals['_SESSIONCLOSERESPONSE']._serialized_end=1129 + _globals['_SESSIONCLOSECMD']._serialized_start=1131 + _globals['_SESSIONCLOSECMD']._serialized_end=1249 +# @@protoc_insertion_point(module_scope) diff --git a/hwilib/devices/cypherock_sdk/core/encoders/proto/generated/version_pb2.py b/hwilib/devices/cypherock_sdk/core/encoders/proto/generated/version_pb2.py new file mode 100644 index 000000000..96407d12b --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/encoders/proto/generated/version_pb2.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: core/version.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from . import common_pb2 as core_dot_common__pb2 +from . import error_pb2 as core_dot_error__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x12\x63ore/version.proto\x12\x04\x63ore\x1a\x11\x63ore/common.proto\x1a\x10\x63ore/error.proto\">\n\x0e\x41ppVersionItem\x12\n\n\x02id\x18\x01 \x01(\r\x12 \n\x07version\x18\x02 \x01(\x0b\x32\x0f.common.Version\"\x1a\n\x18\x41ppVersionIntiateRequest\"F\n\x18\x41ppVersionResultResponse\x12*\n\x0c\x61pp_versions\x18\x01 \x03(\x0b\x32\x14.core.AppVersionItem\"R\n\x11\x41ppVersionRequest\x12\x32\n\x08initiate\x18\x01 \x01(\x0b\x32\x1e.core.AppVersionIntiateRequestH\x00\x42\t\n\x07request\"~\n\x12\x41ppVersionResponse\x12\x30\n\x06result\x18\x01 \x01(\x0b\x32\x1e.core.AppVersionResultResponseH\x00\x12*\n\x0c\x63ommon_error\x18\x02 \x01(\x0b\x32\x12.error.CommonErrorH\x00\x42\n\n\x08response\"p\n\rAppVersionCmd\x12*\n\x07request\x18\x01 \x01(\x0b\x32\x17.core.AppVersionRequestH\x00\x12,\n\x08response\x18\x02 \x01(\x0b\x32\x18.core.AppVersionResponseH\x00\x42\x05\n\x03\x63mdb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'core.version_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_APPVERSIONITEM']._serialized_start=65 + _globals['_APPVERSIONITEM']._serialized_end=127 + _globals['_APPVERSIONINTIATEREQUEST']._serialized_start=129 + _globals['_APPVERSIONINTIATEREQUEST']._serialized_end=155 + _globals['_APPVERSIONRESULTRESPONSE']._serialized_start=157 + _globals['_APPVERSIONRESULTRESPONSE']._serialized_end=227 + _globals['_APPVERSIONREQUEST']._serialized_start=229 + _globals['_APPVERSIONREQUEST']._serialized_end=311 + _globals['_APPVERSIONRESPONSE']._serialized_start=313 + _globals['_APPVERSIONRESPONSE']._serialized_end=439 + _globals['_APPVERSIONCMD']._serialized_start=441 + _globals['_APPVERSIONCMD']._serialized_end=553 +# @@protoc_insertion_point(module_scope) diff --git a/hwilib/devices/cypherock_sdk/core/encoders/types.py b/hwilib/devices/cypherock_sdk/core/encoders/types.py new file mode 100644 index 000000000..87b650bbd --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/encoders/types.py @@ -0,0 +1,14 @@ +# Export all types from proto/types +from .proto.generated.core_pb2 import ( + Status, + DeviceIdleState, + DeviceWaitingOn, + CmdState, +) + +__all__ = [ + "Status", + "DeviceIdleState", + "DeviceWaitingOn", + "CmdState", +] diff --git a/hwilib/devices/cypherock_sdk/core/operations/__init__.py b/hwilib/devices/cypherock_sdk/core/operations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/hwilib/devices/cypherock_sdk/core/operations/helpers/__init__.py b/hwilib/devices/cypherock_sdk/core/operations/helpers/__init__.py new file mode 100644 index 000000000..ca2538dd9 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/operations/helpers/__init__.py @@ -0,0 +1,15 @@ +from .can_retry import can_retry +from .get_command_output import get_command_output +from .get_status import get_status +from .send_command import send_command +from .wait_for_packet import wait_for_packet +from .write_command import write_command + +__all__ = [ + "can_retry", + "get_command_output", + "get_status", + "send_command", + "wait_for_packet", + "write_command", +] diff --git a/hwilib/devices/cypherock_sdk/core/operations/helpers/can_retry.py b/hwilib/devices/cypherock_sdk/core/operations/helpers/can_retry.py new file mode 100644 index 000000000..c401931a6 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/operations/helpers/can_retry.py @@ -0,0 +1,35 @@ +from ....errors import ( + DeviceAppError, + DeviceAppErrorType, + DeviceCommunicationError, + DeviceCommunicationErrorType, + DeviceConnectionErrorType, +) + + +def can_retry(error: Exception) -> bool: + dont_retry = False + + if isinstance(error, Exception) and hasattr(error, "code"): + if error.code in [e.value for e in DeviceConnectionErrorType]: + dont_retry = True + + if ( + isinstance(error, DeviceCommunicationError) + and hasattr(error, "code") + and error.code == DeviceCommunicationErrorType.WRITE_REJECTED + ): + dont_retry = True + + if ( + isinstance(error, DeviceAppError) + and hasattr(error, "code") + and error.code + in [ + DeviceAppErrorType.PROCESS_ABORTED, + DeviceAppErrorType.DEVICE_ABORT, + ] + ): + dont_retry = True + + return not dont_retry diff --git a/hwilib/devices/cypherock_sdk/core/operations/helpers/get_command_output.py b/hwilib/devices/cypherock_sdk/core/operations/helpers/get_command_output.py new file mode 100644 index 000000000..6408ba863 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/operations/helpers/get_command_output.py @@ -0,0 +1,98 @@ +from typing import Dict, Any, Optional, List +from ....errors import DeviceCompatibilityError, DeviceCompatibilityErrorType +from ....interfaces import IDeviceConnection +from ....common_utils import int_to_uint_byte, assert_condition +from ...utils.packet_version import PacketVersion, PacketVersionMap +from ...config import v3 as config_v3 +from ...encoders.packet.packet import decode_payload_data, encode_packet +from .write_command import write_command +from .can_retry import can_retry + + +def get_command_output( + connection: IDeviceConnection, + version: PacketVersion, + sequence_number: int, + max_tries: int = 5, + timeout: Optional[int] = None, +) -> Dict[str, Any]: + assert_condition(connection, "Invalid connection") + assert_condition(version, "Invalid version") + assert_condition(sequence_number, "Invalid sequenceNumber") + + if version != PacketVersionMap.v3: + raise DeviceCompatibilityError( + DeviceCompatibilityErrorType.INVALID_SDK_OPERATION + ) + + usable_config = config_v3 + + first_error: Optional[Exception] = None + data_list: List[str] = [] + + total_packets = 1 + current_packet = 1 + is_status_response = False + + while current_packet <= total_packets: + tries = 1 + inner_max_tries = max_tries + first_error = None + is_success = False + + packets_list = encode_packet( + raw_data=int_to_uint_byte(current_packet, 16), + version=version, + sequence_number=sequence_number, + packet_type=usable_config.commands.PACKET_TYPE.CMD_OUTPUT_REQ, + ) + + if len(packets_list) > 1: + raise Exception("Get Command Output exceeded 1 packet limit") + + packet = packets_list[0] + + while tries <= inner_max_tries and not is_success: + try: + received_packet = write_command( + connection=connection, + packet=packet, + version=version, + sequence_number=sequence_number, + ack_packet_types=[ + usable_config.commands.PACKET_TYPE.CMD_OUTPUT, + usable_config.commands.PACKET_TYPE.STATUS, + ], + timeout=timeout, + ) + + data_list.insert( + received_packet["current_packet_number"] - 1, + received_packet["payload_data"], + ) + total_packets = received_packet["total_packet_number"] + current_packet = received_packet["current_packet_number"] + 1 + is_success = True + is_status_response = ( + received_packet["packet_type"] + == usable_config.commands.PACKET_TYPE.STATUS + ) + + except Exception as e: + if not can_retry(e): + tries = inner_max_tries + + if not first_error: + first_error = e + + tries += 1 + + if not is_success and first_error: + raise first_error + + final_data = "".join(data_list) + + result = decode_payload_data(final_data, version) + result["is_status"] = is_status_response + + return result diff --git a/hwilib/devices/cypherock_sdk/core/operations/helpers/get_status.py b/hwilib/devices/cypherock_sdk/core/operations/helpers/get_status.py new file mode 100644 index 000000000..7ec080cec --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/operations/helpers/get_status.py @@ -0,0 +1,76 @@ +from typing import Dict, Any, Optional +from ....errors import DeviceCompatibilityError, DeviceCompatibilityErrorType +from ....interfaces import IDeviceConnection +from ....common_utils import assert_condition +from ...config import v3 as config_v3 +from ...utils.packet_version import PacketVersion, PacketVersionMap +from ...encoders.packet.packet import decode_payload_data, encode_packet +from .write_command import write_command +from .can_retry import can_retry + + +def get_status( + connection: IDeviceConnection, + version: PacketVersion, + max_tries: int = 5, + timeout: Optional[int] = None, +) -> Dict[str, Any]: + assert_condition(connection, "Invalid connection") + assert_condition(version, "Invalid version") + + if version != PacketVersionMap.v3: + raise DeviceCompatibilityError( + DeviceCompatibilityErrorType.INVALID_SDK_OPERATION + ) + + usable_config = config_v3 + + packets_list = encode_packet( + raw_data="", + version=version, + sequence_number=-1, + packet_type=usable_config.commands.PACKET_TYPE.STATUS_REQ, + ) + + if len(packets_list) == 0: + raise Exception("Could not create packets") + + if len(packets_list) > 1: + raise Exception("Status command has multiple packets") + + first_error: Optional[Exception] = None + + tries = 1 + inner_max_tries = max_tries + first_error = None + is_success = False + final_data = "" + + packet = packets_list[0] + + while tries <= inner_max_tries and not is_success: + try: + received_packet = write_command( + connection=connection, + packet=packet, + version=version, + sequence_number=-1, + ack_packet_types=[usable_config.commands.PACKET_TYPE.STATUS], + timeout=timeout, + ) + final_data = received_packet["payload_data"] + is_success = True + + except Exception as e: + if not can_retry(e): + tries = inner_max_tries + + if not first_error: + first_error = e + + tries += 1 + + if not is_success and first_error: + raise first_error + + return decode_payload_data(final_data, version) diff --git a/hwilib/devices/cypherock_sdk/core/operations/helpers/send_command.py b/hwilib/devices/cypherock_sdk/core/operations/helpers/send_command.py new file mode 100644 index 000000000..b8f7eb17a --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/operations/helpers/send_command.py @@ -0,0 +1,71 @@ +from typing import Optional +from ....errors import DeviceCompatibilityError, DeviceCompatibilityErrorType +from ....interfaces import IDeviceConnection +from ....common_utils import assert_condition +from ...config import v3 as config_v3 +from ...utils.packet_version import PacketVersion, PacketVersionMap +from ...encoders.packet.packet import encode_packet +from .write_command import write_command +from .can_retry import can_retry + + +def send_command( + connection: IDeviceConnection, + version: PacketVersion, + sequence_number: int, + raw_data: Optional[str] = None, + proto_data: Optional[str] = None, + max_tries: int = 5, + timeout: Optional[int] = None, +) -> None: + assert_condition(connection, "Invalid connection") + assert_condition(raw_data or proto_data, "Raw data or proto data is required") + assert_condition(version, "Invalid version") + assert_condition(sequence_number, "Invalid sequenceNumber") + + if version != PacketVersionMap.v3: + raise DeviceCompatibilityError( + DeviceCompatibilityErrorType.INVALID_SDK_OPERATION + ) + + usable_config = config_v3 + + packets_list = encode_packet( + raw_data=raw_data or "", + proto_data=proto_data or "", + version=version, + sequence_number=sequence_number, + packet_type=usable_config.commands.PACKET_TYPE.CMD, + ) + + first_error: Optional[Exception] = None + + for packet in packets_list: + tries = 1 + inner_max_tries = max_tries if max_tries is not None else 5 + first_error = None + is_success = False + + while tries <= inner_max_tries and not is_success: + try: + write_command( + connection=connection, + packet=packet, + version=version, + sequence_number=sequence_number, + ack_packet_types=[usable_config.commands.PACKET_TYPE.CMD_ACK], + timeout=timeout, + ) + is_success = True + + except Exception as e: + if not can_retry(e): + tries = inner_max_tries + + if not first_error: + first_error = e + + tries += 1 + + if not is_success and first_error: + raise first_error diff --git a/hwilib/devices/cypherock_sdk/core/operations/helpers/wait_for_packet.py b/hwilib/devices/cypherock_sdk/core/operations/helpers/wait_for_packet.py new file mode 100644 index 000000000..be3b77f76 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/operations/helpers/wait_for_packet.py @@ -0,0 +1,233 @@ +import threading +import time +from typing import List, Optional +from ....errors import ( + DeviceAppError, + DeviceAppErrorType, + DeviceConnectionError, + DeviceConnectionErrorType, + DeviceCommunicationError, + DeviceCommunicationErrorType, + DeviceCompatibilityError, + DeviceCompatibilityErrorType, +) +from ....interfaces import IDeviceConnection +from ....common_utils import assert_condition +from ...config import v3 as config_v3 +from ...utils.packet_version import PacketVersion, PacketVersionMap +from ...encoders.packet.packet import ( + DecodedPacketData, + decode_packet, + decode_payload_data, + ErrorPacketRejectReason, + RejectReasonToMsgMap, +) +import logging + +logger = logging.getLogger(__name__) + + +class CancellableTask: + def __init__(self): + self._result = None + self._error = None + self._completed = threading.Event() + self._cancelled = False + self._lock = threading.Lock() + + def cancel(self): + with self._lock: + self._cancelled = True + self._completed.set() + + def is_cancelled(self) -> bool: + with self._lock: + return self._cancelled + + def set_result(self, result: DecodedPacketData): + with self._lock: + if not self._cancelled: + self._result = result + self._completed.set() + + def set_error(self, error: Exception): + with self._lock: + if not self._cancelled: + self._error = error + self._completed.set() + + def result(self, timeout: Optional[float] = None) -> DecodedPacketData: + """Wait for result synchronously. Returns result or raises error.""" + if self._completed.wait(timeout=timeout): + with self._lock: + if self._cancelled: + raise DeviceConnectionError(DeviceConnectionErrorType.CONNECTION_CLOSED) + if self._error: + raise self._error + return self._result + else: + raise DeviceCommunicationError(DeviceCommunicationErrorType.READ_TIMEOUT) + + +def wait_for_packet( + connection: IDeviceConnection, + sequence_number: int, + packet_types: List[int], + version: PacketVersion, + ack_timeout: Optional[int] = None, +) -> CancellableTask: + assert_condition(connection, "Invalid connection") + assert_condition(version, "Invalid version") + assert_condition(packet_types, "Invalid packetTypes") + assert_condition(sequence_number, "Invalid sequenceNumber") + assert_condition( + len(packet_types) > 0, "packetTypes should contain atleast 1 element" + ) + + if version != PacketVersionMap.v3: + raise DeviceCompatibilityError( + DeviceCompatibilityErrorType.INVALID_SDK_OPERATION + ) + + usable_config = config_v3 + timeout_val = ( + ack_timeout if ack_timeout is not None else usable_config.constants.ACK_TIME + ) + + task = CancellableTask() + is_completed = threading.Event() + stop_event = threading.Event() + + def recheck_packet(): + """Polling thread that checks for incoming packets""" + start_time = time.time() + recheck_interval = usable_config.constants.RECHECK_TIME / 1000 + + while not stop_event.is_set() and not is_completed.is_set(): + try: + # Check timeout + elapsed = (time.time() - start_time) * 1000 + if elapsed >= timeout_val: + if not is_completed.is_set(): + is_completed.set() + if not connection.is_connected(): + task.set_error(DeviceConnectionError( + DeviceConnectionErrorType.CONNECTION_CLOSED + )) + else: + task.set_error(DeviceCommunicationError( + DeviceCommunicationErrorType.READ_TIMEOUT + )) + return + + # Check connection + if not connection.is_connected(): + if not is_completed.is_set(): + is_completed.set() + task.set_error(DeviceConnectionError( + DeviceConnectionErrorType.CONNECTION_CLOSED + )) + return + + # Try to receive packet + raw_packet = connection.receive() + if not raw_packet: + time.sleep(recheck_interval) + continue + + # Decode and process packet + packet_list = decode_packet(raw_packet, version) + + is_success = False + received_packet: Optional[DecodedPacketData] = None + error: Optional[Exception] = None + + for packet in packet_list: + if len(packet["error_list"]) == 0: + if ( + packet["packet_type"] + == usable_config.commands.PACKET_TYPE.ERROR + ): + error = DeviceCommunicationError( + DeviceCommunicationErrorType.WRITE_REJECTED + ) + + payload_data = decode_payload_data( + packet["payload_data"], version + ) + raw_data = payload_data["raw_data"] + + reject_status = int(f"0x{raw_data}", 16) + latest_seq_number = connection.get_sequence_number() + + if ( + reject_status + == ErrorPacketRejectReason.INVALID_SEQUENCE_NO + and latest_seq_number != sequence_number + ): + error = DeviceAppError( + DeviceAppErrorType.PROCESS_ABORTED + ) + break + + inner_reject_reason = RejectReasonToMsgMap.get( + ErrorPacketRejectReason(reject_status) + ) + + if inner_reject_reason: + reject_reason = inner_reject_reason + else: + reject_reason = f"Unknown reject reason: {raw_data}" + + error.message = f"The write packet operation was rejected by the device because: {reject_reason}" + + elif packet["packet_type"] in packet_types: + if ( + sequence_number == packet["sequence_number"] + or packet["packet_type"] + == usable_config.commands.PACKET_TYPE.STATUS + ): + is_success = True + received_packet = packet + + if error or is_success: + break + + # Handle result + if error or is_success: + if not is_completed.is_set(): + is_completed.set() + if error: + task.set_error(error) + elif received_packet: + task.set_result(received_packet) + return + else: + time.sleep(recheck_interval) + + except Exception as e: + if hasattr(e, "code") and e.code in [ + err.value for err in DeviceConnectionErrorType + ]: + if not is_completed.is_set(): + is_completed.set() + task.set_error(e) + return + + logger.error("Error while rechecking packet on `waitForPacket`") + logger.error(str(e)) + time.sleep(recheck_interval) + + # Start the polling thread + thread = threading.Thread(target=recheck_packet, daemon=True) + thread.start() + + # Override cancel to also stop the thread + original_cancel = task.cancel + + def cancel_with_stop(): + stop_event.set() + original_cancel() + task.cancel = cancel_with_stop + + return task diff --git a/hwilib/devices/cypherock_sdk/core/operations/helpers/write_command.py b/hwilib/devices/cypherock_sdk/core/operations/helpers/write_command.py new file mode 100644 index 000000000..b2cbc7c72 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/operations/helpers/write_command.py @@ -0,0 +1,81 @@ +from typing import List, Optional +from ....errors import ( + DeviceCommunicationError, + DeviceCommunicationErrorType, + DeviceCompatibilityError, + DeviceCompatibilityErrorType, + DeviceConnectionError, + DeviceConnectionErrorType, +) +from ....interfaces import IDeviceConnection +from ....common_utils import assert_condition +from ...utils.packet_version import PacketVersion, PacketVersionMap +from ...encoders.packet.packet import DecodedPacketData +from .wait_for_packet import wait_for_packet + + +def write_command( + connection: IDeviceConnection, + packet: bytes, + version: PacketVersion, + sequence_number: int, + ack_packet_types: List[int], + timeout: Optional[int] = None, +) -> DecodedPacketData: + assert_condition(connection, "Invalid connection") + assert_condition(packet, "Invalid packet") + assert_condition(version, "Invalid version") + assert_condition(ack_packet_types, "Invalid ackPacketTypes") + assert_condition(sequence_number, "Invalid sequenceNumber") + + assert_condition( + len(ack_packet_types) > 0, "ackPacketTypes should contain atleast 1 element" + ) + assert_condition(len(packet) > 0, "packet cannot be empty") + + if version != PacketVersionMap.v3: + raise DeviceCompatibilityError( + DeviceCompatibilityErrorType.INVALID_SDK_OPERATION + ) + + if not connection.is_connected(): + raise DeviceConnectionError(DeviceConnectionErrorType.CONNECTION_CLOSED) + + # Start waiting for acknowledgment packet (non-blocking, runs in background thread) + ack_promise = wait_for_packet( + connection=connection, + version=version, + packet_types=ack_packet_types, + sequence_number=sequence_number, + ack_timeout=timeout, + ) + + try: + # Send the packet synchronously (blocking call) + try: + connection.send(packet) + except Exception as send_error: + # If send fails, cancel the ack wait and raise error + ack_promise.cancel() + if not connection.is_connected(): + raise DeviceConnectionError( + DeviceConnectionErrorType.CONNECTION_CLOSED + ) + else: + raise DeviceCommunicationError( + DeviceCommunicationErrorType.WRITE_ERROR + ) from send_error + + # Wait for acknowledgment (blocking call, will return result or raise error) + try: + return ack_promise.result() + except Exception as ack_error: + # If ack wait was cancelled, check why + if ack_promise.is_cancelled(): + raise Exception("Operation cancelled") from ack_error + raise + + except Exception as error: + # Ensure we cancel the ack wait if something goes wrong + ack_promise.cancel() + raise error diff --git a/hwilib/devices/cypherock_sdk/core/operations/proto/__init__.py b/hwilib/devices/cypherock_sdk/core/operations/proto/__init__.py new file mode 100644 index 000000000..2fab19ede --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/operations/proto/__init__.py @@ -0,0 +1,15 @@ +from .get_status import get_status +from .get_result import get_result +from .send_query import send_query +from .wait_for_result import wait_for_result +from .send_abort import send_abort +from .wait_for_idle import wait_for_idle + +__all__ = [ + "get_status", + "get_result", + "send_query", + "wait_for_result", + "send_abort", + "wait_for_idle", +] diff --git a/hwilib/devices/cypherock_sdk/core/operations/proto/get_result.py b/hwilib/devices/cypherock_sdk/core/operations/proto/get_result.py new file mode 100644 index 000000000..510819eab --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/operations/proto/get_result.py @@ -0,0 +1,85 @@ +from typing import Optional, Union, Dict +from ....errors import DeviceAppError, DeviceAppErrorType +from ....interfaces import IDeviceConnection +from ....common_utils import assert_condition, hex_to_uint8array +from ...utils.packet_version import PacketVersion +from ..helpers import get_command_output +from ...encoders.proto.generated.core_pb2 import Status, Msg, ErrorType + + +def get_result( + connection: IDeviceConnection, + version: PacketVersion, + sequence_number: int, + applet_id: int, + max_tries: int = 5, + timeout: Optional[int] = None, + allow_core_data: Optional[bool] = None, +) -> Dict[str, Union[bool, Union[Status, bytes]]]: + assert_condition(applet_id, 'Invalid appletId') + + command_output = get_command_output( + connection=connection, + version=version, + max_tries=max_tries, + sequence_number=sequence_number, + timeout=timeout + ) + + is_status = command_output["is_status"] + protobuf_data = command_output["protobuf_data"] + raw_data = command_output["raw_data"] + + output: Union[bytes, Status] + + if is_status: + status = Status() + status.ParseFromString(hex_to_uint8array(protobuf_data)) + if status.current_cmd_seq != sequence_number: + raise DeviceAppError(DeviceAppErrorType.EXECUTING_OTHER_COMMAND) + output = status + else: + msg = Msg() + msg.ParseFromString(hex_to_uint8array(protobuf_data)) + + # Determine which oneof is set and route accordingly + active_field = None + try: + active_field = msg.WhichOneof("type") + except Exception: + active_field = None + if not active_field: + try: + applet = getattr(msg, "cmd", None) + if applet is not None and getattr(applet, "applet_id", 0): + active_field = "cmd" + except Exception: + active_field = None + + # Error handling only if error oneof is active and not NO_ERROR + if active_field == "error" and msg.error and msg.error.type != ErrorType.NO_ERROR: + error_map = { + ErrorType.NO_ERROR: DeviceAppErrorType.UNKNOWN_ERROR, + ErrorType.UNKNOWN_APP: DeviceAppErrorType.UNKNOWN_APP, + ErrorType.INVALID_MSG: DeviceAppErrorType.INVALID_MSG, + ErrorType.APP_NOT_ACTIVE: DeviceAppErrorType.APP_NOT_ACTIVE, + ErrorType.APP_TIMEOUT_OCCURRED: DeviceAppErrorType.APP_TIMEOUT, + ErrorType.DEVICE_SESSION_INVALID: DeviceAppErrorType.DEVICE_SESSION_INVALID, + } + raise DeviceAppError(error_map[msg.error.type]) + + # If command oneof is active, validate applet id and return raw_data; otherwise return protobuf_data + if active_field == "cmd": + if not msg.cmd: + raise DeviceAppError(DeviceAppErrorType.INVALID_MSG_FROM_DEVICE) + if msg.cmd.applet_id != applet_id: + raise DeviceAppError(DeviceAppErrorType.INVALID_APP_ID_FROM_DEVICE) + # If raw_data is empty, treat it as a protobuf-only response + if raw_data: + output = hex_to_uint8array(raw_data) + else: + output = hex_to_uint8array(protobuf_data) + else: + output = hex_to_uint8array(protobuf_data) + + return {"is_status": is_status, "result": output} diff --git a/hwilib/devices/cypherock_sdk/core/operations/proto/get_status.py b/hwilib/devices/cypherock_sdk/core/operations/proto/get_status.py new file mode 100644 index 000000000..26cdaff2f --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/operations/proto/get_status.py @@ -0,0 +1,38 @@ +from typing import Optional +from ....interfaces import IDeviceConnection +from ....common_utils import hex_to_uint8array +from ...utils.packet_version import PacketVersion +from ..helpers import get_status as get_status_helper +from ...encoders.proto.generated.core_pb2 import Status +import logging + +logger = logging.getLogger(__name__) + +def get_status( + connection: IDeviceConnection, + version: PacketVersion, + max_tries: int = 5, + timeout: Optional[int] = None, + dont_log: bool = False, +) -> Status: + result = get_status_helper( + connection=connection, + version=version, + max_tries=max_tries, + timeout=timeout, + ) + + protobuf_data = result["protobuf_data"] + # Parse using standard protobuf + status = Status() + status.ParseFromString(hex_to_uint8array(protobuf_data)) + + if not dont_log: + try: + # Standard protobuf doesn't have to_dict(), use str representation + meta = {'status': str(status)} + except Exception: + meta = {'status': str(status)} + logger.debug('Received status', meta) + + return status diff --git a/hwilib/devices/cypherock_sdk/core/operations/proto/send_abort.py b/hwilib/devices/cypherock_sdk/core/operations/proto/send_abort.py new file mode 100644 index 000000000..1c800869b --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/operations/proto/send_abort.py @@ -0,0 +1,93 @@ +from typing import Optional +from ....interfaces import IDeviceConnection +from ....errors import DeviceAppError, DeviceAppErrorType, DeviceCompatibilityError, DeviceCompatibilityErrorType +from ....common_utils import hex_to_uint8array +from ...utils.packet_version import PacketVersion, PacketVersionMap +from ...config import v3 as config +from ...encoders.packet.packet import decode_payload_data, encode_packet +from ...encoders.proto.generated.core_pb2 import Status +from ..helpers import can_retry, write_command +from .wait_for_idle import wait_for_idle +import logging + +logger = logging.getLogger(__name__) + +def send_abort( + connection: IDeviceConnection, + version: PacketVersion, + sequence_number: int, + max_tries: int = 2, + timeout: Optional[int] = None, +) -> Status: + if version != PacketVersionMap.v3: + raise DeviceCompatibilityError( + DeviceCompatibilityErrorType.INVALID_SDK_OPERATION + ) + + usable_config = config + + packets_list = encode_packet( + raw_data='', + version=version, + sequence_number=sequence_number, + packet_type=usable_config.commands.PACKET_TYPE.ABORT, + ) + + if len(packets_list) == 0: + raise Exception('Cound not create packets') + + if len(packets_list) > 1: + raise Exception('Abort command has multiple packets') + + logger.debug('Sending abort') + + first_error: Optional[Exception] = None + + tries = 1 + inner_max_tries = max_tries + first_error = None + is_success = False + status: Optional[Status] = None + + packet = packets_list[0] + while tries <= inner_max_tries and not is_success: + try: + received_packet = write_command( + connection=connection, + packet=packet, + version=version, + sequence_number=sequence_number, + ack_packet_types=[usable_config.commands.PACKET_TYPE.STATUS], + timeout=timeout, + ) + + payload_data_result = decode_payload_data( + received_packet['payload_data'], + version, + ) + protobuf_data = payload_data_result['protobuf_data'] + status = Status() + status.ParseFromString(hex_to_uint8array(protobuf_data)) + + if status.current_cmd_seq != sequence_number: + raise DeviceAppError(DeviceAppErrorType.EXECUTING_OTHER_COMMAND) + + is_success = True + except Exception as e: + # Don't retry if connection closed + if not can_retry(e): + tries = inner_max_tries + + if not first_error: + first_error = e + tries += 1 + + if not is_success and first_error: + raise first_error + + if not status: + raise Exception('Did not found status') + + wait_for_idle(connection=connection, version=version) + + return status diff --git a/hwilib/devices/cypherock_sdk/core/operations/proto/send_query.py b/hwilib/devices/cypherock_sdk/core/operations/proto/send_query.py new file mode 100644 index 000000000..8c3f749fe --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/operations/proto/send_query.py @@ -0,0 +1,40 @@ +from typing import Optional +from ....interfaces import IDeviceConnection +from ....common_utils import assert_condition, uint8array_to_hex +from ...utils.packet_version import PacketVersion +from ...encoders.proto.generated.core_pb2 import Msg, Command +from ..helpers import send_command as send_command_helper +import logging + +logger = logging.getLogger(__name__) + +def send_query( + connection: IDeviceConnection, + applet_id: int, + data: bytes, + version: PacketVersion, + sequence_number: int, + max_tries: int = 5, + timeout: Optional[int] = None, +) -> None: + assert_condition(applet_id, 'Invalid appletId') + assert_condition(data, 'Invalid data') + + assert_condition(applet_id >= 0, 'appletId cannot be negative') + assert_condition(len(data) > 0, 'data cannot be empty') + + raw_data = uint8array_to_hex(data) + logger.debug('Sending query', {'appletId': applet_id, 'rawData': raw_data}) + + msg = Msg(cmd=Command(applet_id=applet_id)) + msg_data = uint8array_to_hex(msg.SerializeToString()) + + return send_command_helper( + connection=connection, + proto_data=msg_data, + raw_data=raw_data, + version=version, + max_tries=max_tries, + sequence_number=sequence_number, + timeout=timeout, + ) diff --git a/hwilib/devices/cypherock_sdk/core/operations/proto/wait_for_idle.py b/hwilib/devices/cypherock_sdk/core/operations/proto/wait_for_idle.py new file mode 100644 index 000000000..314561cf8 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/operations/proto/wait_for_idle.py @@ -0,0 +1,141 @@ +from typing import Optional +import time +import threading +from ....interfaces import IDeviceConnection +from ....errors import ( + DeviceConnectionError, + DeviceConnectionErrorType, + DeviceAppError, + DeviceAppErrorType, +) +from ...encoders.proto.generated.core_pb2 import DeviceIdleState +from ...utils.packet_version import PacketVersion +from ...config import v3 as config +from .get_status import get_status +import logging + +logger = logging.getLogger(__name__) + + +def wait_for_idle( + connection: IDeviceConnection, + version: PacketVersion, + timeout: Optional[int] = None, +) -> None: + logger.debug("Waiting for device to be idle") + + usable_config = config + is_completed = threading.Event() + stop_event = threading.Event() + error_to_raise = [None] # Use list to allow modification from nested function + + timeout_val = ( + timeout + if timeout is not None + else usable_config.constants.IDLE_TIMEOUT + ) / 1000 # Convert to seconds + + recheck_interval = usable_config.constants.IDLE_RECHECK_TIME / 1000 # Convert to seconds + + def check_if_idle(): + """Check if device is idle and handle completion/errors""" + try: + if not connection.is_connected(): + is_completed.set() + error_to_raise[0] = DeviceConnectionError( + DeviceConnectionErrorType.CONNECTION_CLOSED + ) + return + + if is_completed.is_set(): + return + + # Get device status (synchronous call) + status = get_status( + connection=connection, + version=version, + dont_log=True, + ) + + # Check if device is in USB idle state + if status.device_idle_state != DeviceIdleState.DEVICE_IDLE_STATE_USB: + # Device is not idle, we're done waiting + is_completed.set() + return + + # Device is idle, continue waiting (will check again after interval) + + except Exception as error: + if hasattr(error, "code") and error.code in [ + e.value for e in DeviceConnectionErrorType + ]: + is_completed.set() + error_to_raise[0] = error + return + + logger.error("Error while rechecking if idle") + logger.error(error) + # Continue polling despite error + + def timeout_handler(): + """Timeout thread that raises error if timeout is reached""" + time.sleep(timeout_val) + + if not is_completed.is_set(): + is_completed.set() + stop_event.set() + + if not connection.is_connected(): + error_to_raise[0] = DeviceConnectionError( + DeviceConnectionErrorType.CONNECTION_CLOSED + ) + else: + error_to_raise[0] = DeviceAppError(DeviceAppErrorType.EXECUTING_OTHER_COMMAND) + + # Start timeout thread + timeout_thread = threading.Thread(target=timeout_handler, daemon=True) + timeout_thread.start() + + # Main polling loop + start_time = time.time() + + try: + while not is_completed.is_set() and not stop_event.is_set(): + # Check connection + if not connection.is_connected(): + is_completed.set() + error_to_raise[0] = DeviceConnectionError( + DeviceConnectionErrorType.CONNECTION_CLOSED + ) + break + + # Check if idle + check_if_idle() + + if is_completed.is_set(): + break + + # Sleep before next check + time.sleep(recheck_interval) + + # Also check elapsed time as backup (in case timeout thread has issues) + elapsed = time.time() - start_time + if elapsed >= timeout_val: + is_completed.set() + stop_event.set() + if not connection.is_connected(): + error_to_raise[0] = DeviceConnectionError( + DeviceConnectionErrorType.CONNECTION_CLOSED + ) + else: + error_to_raise[0] = DeviceAppError(DeviceAppErrorType.EXECUTING_OTHER_COMMAND) + break + + # If there's an error to raise, raise it + if error_to_raise[0]: + raise error_to_raise[0] + + except Exception as error: + is_completed.set() + stop_event.set() + raise error diff --git a/hwilib/devices/cypherock_sdk/core/operations/proto/wait_for_result.py b/hwilib/devices/cypherock_sdk/core/operations/proto/wait_for_result.py new file mode 100644 index 000000000..80fa98e3d --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/operations/proto/wait_for_result.py @@ -0,0 +1,96 @@ +import time +from typing import Optional, Callable, Dict, Any +from ....interfaces import IDeviceConnection +from ....errors import DeviceAppError, DeviceAppErrorType, DeviceCompatibilityError, DeviceCompatibilityErrorType +from ....common_utils import assert_condition, uint8array_to_hex +from ...utils.packet_version import PacketVersion, PacketVersionMap +from ...encoders.proto.generated.core_pb2 import CmdState, DeviceIdleState, Status +from .get_result import get_result +import logging + +logger = logging.getLogger(__name__) + +class IWaitForCommandOutputParams: + def __init__( + self, + connection: IDeviceConnection, + sequence_number: int, + applet_id: int, + on_status: Optional[Callable[[Status], None]] = None, + version: PacketVersion = None, + options: Optional[Dict[str, Any]] = None, + allow_core_data: Optional[bool] = None, + ): + self.connection = connection + self.sequence_number = sequence_number + self.applet_id = applet_id + self.on_status = on_status + self.version = version + self.options = options + self.allow_core_data = allow_core_data + + +def wait_for_result( + connection: IDeviceConnection, + sequence_number: int, + applet_id: int, + on_status: Optional[Callable[[Status], None]] = None, + options: Optional[Dict[str, Any]] = None, + version: PacketVersion = None, + allow_core_data: Optional[bool] = None, +) -> bytes: + assert_condition(connection, 'Invalid connection') + assert_condition(sequence_number, 'Invalid sequenceNumber') + assert_condition(applet_id, 'Invalid appletId') + assert_condition(version, 'Invalid version') + + assert_condition(applet_id >= 0, 'appletId cannot be negative') + + if version != PacketVersionMap.v3: + raise DeviceCompatibilityError( + DeviceCompatibilityErrorType.INVALID_SDK_OPERATION + ) + + while True: + response = get_result( + connection=connection, + version=version, + applet_id=applet_id, + max_tries=options.get('maxTries', 5) if options else 5, + sequence_number=sequence_number, + timeout=options.get('timeout') if options else None, + allow_core_data=allow_core_data, + ) + + if not response['is_status']: + resp = response['result'] + + logger.debug('Received result', { + 'result': uint8array_to_hex(resp), + 'appletId': applet_id, + }) + + return resp + + status = response['result'] + + if ( + status.device_idle_state == DeviceIdleState.DEVICE_IDLE_STATE_DEVICE or + status.current_cmd_seq != sequence_number + ): + raise DeviceAppError(DeviceAppErrorType.EXECUTING_OTHER_COMMAND) + + if status.cmd_state in [ + CmdState.CMD_STATE_DONE, + CmdState.CMD_STATE_FAILED, + CmdState.CMD_STATE_INVALID_CMD, + ]: + raise Exception( + 'Command status is done or rejected, but no output is received' + ) + + if status.device_idle_state == DeviceIdleState.DEVICE_IDLE_STATE_USB: + if on_status: + on_status(status) + + time.sleep((options.get('interval', 200) if options else 200) / 1000) diff --git a/hwilib/devices/cypherock_sdk/core/sdk.py b/hwilib/devices/cypherock_sdk/core/sdk.py new file mode 100644 index 000000000..81568e29c --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/sdk.py @@ -0,0 +1,200 @@ +from typing import Optional, Dict, Any, Callable +from ..interfaces import IDeviceConnection +from .operations import proto as operations +from .utils.packet_version import PacketVersionMap +from .types import ISDK +from .encoders.proto.generated.core_pb2 import DeviceIdleState +from ..errors import DeviceAppError, DeviceAppErrorType + +import logging + +logger = logging.getLogger(__name__) + +class SDK: + def __init__( + self, + connection: IDeviceConnection, + applet_id: int, + ): + self.connection = connection + self.applet_id = applet_id + + @classmethod + def create( + cls, + connection: IDeviceConnection, + applet_id: int, + ) -> ISDK: + return cls( + connection, + applet_id, + ) + + def get_connection(self) -> IDeviceConnection: + return self.connection + + def get_sequence_number(self) -> int: + return self.connection.get_sequence_number() + + def get_new_sequence_number(self) -> int: + return self.connection.get_new_sequence_number() + + def before_operation(self) -> None: + return self.connection.before_operation() + + def after_operation(self) -> None: + return self.connection.after_operation() + + def configure_applet_id(self, applet_id: int) -> None: + self.applet_id = applet_id + + def destroy(self) -> None: + return self.connection.destroy() + + # ************** v3 Packet Version with protobuf **************** + def send_query( + self, + data: bytes, + options: Optional[Dict[str, Any]] = None, + ) -> None: + sequence_number = options.get("sequence_number") if options else None + if sequence_number is None: + sequence_number = self.get_new_sequence_number() + + max_tries = options.get("max_tries") if options else None + timeout = options.get("timeout") if options else None + + # Set defaults for None values + if max_tries is None: + max_tries = 5 + + return operations.send_query( + connection=self.connection, + data=data, + applet_id=self.applet_id, + sequence_number=sequence_number, + version=PacketVersionMap.v3, + max_tries=max_tries, + timeout=timeout, + ) + + def get_result(self, options: Optional[Dict[str, Any]] = None): + sequence_number = options.get("sequence_number") if options else None + if sequence_number is None: + sequence_number = self.get_sequence_number() + + max_tries = options.get("max_tries") if options else None + timeout = options.get("timeout") if options else None + + # Set defaults for None values + if max_tries is None: + max_tries = 5 + + return operations.get_result( + connection=self.connection, + applet_id=self.applet_id, + sequence_number=sequence_number, + version=PacketVersionMap.v3, + max_tries=max_tries, + timeout=timeout, + ) + + def wait_for_result(self, params: Optional[Dict[str, Any]] = None): + sequence_number = params.get("sequence_number") if params else None + if sequence_number is None: + sequence_number = self.get_sequence_number() + + on_status = params.get("on_status") if params else None + options = params.get("options") if params else None + + return operations.wait_for_result( + connection=self.connection, + version=PacketVersionMap.v3, + applet_id=self.applet_id, + sequence_number=sequence_number, + on_status=on_status, + options=options, + ) + + def get_status( + self, + max_tries: Optional[int] = None, + timeout: Optional[int] = None, + dont_log: Optional[bool] = None, + ): + # Set defaults for None values + if max_tries is None: + max_tries = 5 + if dont_log is None: + dont_log = False + + return operations.get_status( + connection=self.connection, + version=PacketVersionMap.v3, + max_tries=max_tries, + timeout=timeout, + dont_log=dont_log, + ) + + def send_abort(self, options: Optional[Dict[str, Any]] = None): + sequence_number = options.get("sequence_number") if options else None + if sequence_number is None: + sequence_number = self.get_new_sequence_number() + + max_tries = options.get("max_tries") if options else None + timeout = options.get("timeout") if options else None + + # Set defaults for None values + if max_tries is None: + max_tries = 5 + + return operations.send_abort( + connection=self.connection, + sequence_number=sequence_number, + version=PacketVersionMap.v3, + max_tries=max_tries, + timeout=timeout, + ) + + def make_device_ready(self) -> None: + self.ensure_if_usb_idle() + + status = self.get_status() + if status.device_idle_state in [ + DeviceIdleState.DEVICE_IDLE_STATE_USB, + DeviceIdleState.DEVICE_IDLE_STATE_DEVICE, + ]: + if status.abort_disabled: + raise DeviceAppError(DeviceAppErrorType.EXECUTING_OTHER_COMMAND) + + self.send_abort() + + def run_operation(self, operation: Callable[[], Any]) -> Any: + try: + self.connection.before_operation() + self.make_device_ready() + + result = operation() + + if self.connection.is_connected(): + self.connection.after_operation() + + return result + except Exception as error: + if self.connection.is_connected(): + self.connection.after_operation() + + raise error + + def ensure_if_usb_idle(self) -> None: + try: + operations.wait_for_idle( + connection=self.connection, + version=PacketVersionMap.v3, + ) + except Exception as error: + logger.warn("Error while checking for idle state") + logger.warn(error) + + +__all__ = ["SDK"] diff --git a/hwilib/devices/cypherock_sdk/core/types.py b/hwilib/devices/cypherock_sdk/core/types.py new file mode 100644 index 000000000..3b01494de --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/types.py @@ -0,0 +1,62 @@ +# flake8: noqa E704 +from typing import ( + Protocol, + Optional, + Callable, + Any, + Dict, +) +from ..interfaces import IDeviceConnection + + +class ISDK(Protocol): + def get_connection(self) -> IDeviceConnection: ... + + def get_sequence_number(self) -> int: ... + + def get_new_sequence_number(self) -> int: ... + + def before_operation(self) -> None: ... + + def after_operation(self) -> None: ... + + def configure_applet_id(self, applet_id: int) -> None: ... + + def destroy(self) -> None: ... + + def send_query( + self, + data: bytes, + options: Optional[Dict[str, Any]] = None, + ) -> None: ... + + def get_result( + self, + options: Optional[Dict[str, Any]] = None, + ) -> Dict[str, Any]: ... + + def wait_for_result( + self, + params: Optional[Dict[str, Any]] = None, + ) -> bytes: ... + + def get_status( + self, + max_tries: Optional[int] = None, + timeout: Optional[int] = None, + dont_log: Optional[bool] = None, + ) -> Any: # Status from proto types + ... + + def send_abort( + self, + options: Optional[Dict[str, Any]] = None, + ) -> Any: # Status from proto types + ... + + def run_operation(self, operation: Callable[[], Any]) -> Any: ... + + +__all__ = [ + "ISDK", +] diff --git a/hwilib/devices/cypherock_sdk/core/utils/common_error.py b/hwilib/devices/cypherock_sdk/core/utils/common_error.py new file mode 100644 index 000000000..fb33047b0 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/utils/common_error.py @@ -0,0 +1,47 @@ +from typing import TypeVar, Optional, Dict, Any +from ...errors import DeviceAppError, DeviceAppErrorType +from ..encoders.proto.generated.error_pb2 import CommonError +from ...common_utils import assert_condition + +T = TypeVar("T") + + +def assert_or_throw_invalid_result(condition: T) -> T: + assert_condition( + condition is not None, + DeviceAppError(DeviceAppErrorType.INVALID_MSG_FROM_DEVICE), + ) + return condition + + +def _is_truthy_error_value(value: Any) -> bool: + try: + if isinstance(value, bool): + return value is True + if isinstance(value, int): + return value != 0 + if isinstance(value, (bytes, str, list, tuple, dict)): + return len(value) > 0 + except Exception: + pass + return value is not None + + +def parse_common_error(error: Optional[CommonError]) -> None: + if error is None: + return + + error_types_map: Dict[str, DeviceAppErrorType] = { + "unknown_error": DeviceAppErrorType.UNKNOWN_ERROR, + "device_setup_required": DeviceAppErrorType.DEVICE_SETUP_REQUIRED, + "wallet_not_found": DeviceAppErrorType.WALLET_NOT_FOUND, + "wallet_partial_state": DeviceAppErrorType.WALLET_PARTIAL_STATE, + "card_error": DeviceAppErrorType.CARD_OPERATION_FAILED, + "user_rejection": DeviceAppErrorType.USER_REJECTION, + "corrupt_data": DeviceAppErrorType.CORRUPT_DATA, + } + + for key, field in getattr(error, "__dataclass_fields__", {}).items(): + value = getattr(error, key) + if key in error_types_map and _is_truthy_error_value(value): + raise DeviceAppError(error_types_map[key], value) diff --git a/hwilib/devices/cypherock_sdk/core/utils/packet_version.py b/hwilib/devices/cypherock_sdk/core/utils/packet_version.py new file mode 100644 index 000000000..974d75beb --- /dev/null +++ b/hwilib/devices/cypherock_sdk/core/utils/packet_version.py @@ -0,0 +1,17 @@ +from typing import Literal, List + +PacketVersion = Literal["v1", "v2", "v3"] + + +class PacketVersionMap: + v1: PacketVersion = "v1" + v2: PacketVersion = "v2" + v3: PacketVersion = "v3" + + +# Order is from older to newer +PacketVersionList: List[PacketVersion] = [ + PacketVersionMap.v1, + PacketVersionMap.v2, + PacketVersionMap.v3, +] diff --git a/hwilib/devices/cypherock_sdk/errors/__init__.py b/hwilib/devices/cypherock_sdk/errors/__init__.py new file mode 100644 index 000000000..46a570ba1 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/errors/__init__.py @@ -0,0 +1,20 @@ +from .app_error import DeviceAppError, DeviceAppErrorType +from .card_error import CardAppErrorType, cardErrorTypeDetails +from .device_error import DeviceError +from .communication_error import DeviceCommunicationError, DeviceCommunicationErrorType +from .connection_error import DeviceConnectionError, DeviceConnectionErrorType +from .compatibility_error import DeviceCompatibilityError, DeviceCompatibilityErrorType + +__all__ = [ + "DeviceAppError", + "DeviceAppErrorType", + "cardErrorTypeDetails", + "CardAppErrorType", + "DeviceError", + "DeviceCommunicationError", + "DeviceCommunicationErrorType", + "DeviceConnectionError", + "DeviceConnectionErrorType", + "DeviceCompatibilityError", + "DeviceCompatibilityErrorType", +] diff --git a/hwilib/devices/cypherock_sdk/errors/app_error.py b/hwilib/devices/cypherock_sdk/errors/app_error.py new file mode 100644 index 000000000..91b206e92 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/errors/app_error.py @@ -0,0 +1,130 @@ +from enum import Enum +from typing import Dict, Any, Optional, Union +from .card_error import cardErrorTypeDetails +from .device_error import DeviceError + + +class DeviceAppErrorType(Enum): + UNKNOWN_ERROR = "APP_0000" + EXECUTING_OTHER_COMMAND = "APP_0101" + PROCESS_ABORTED = "APP_0102" + DEVICE_ABORT = "APP_0103" + INVALID_MSG_FROM_DEVICE = "APP_0200" + INVALID_APP_ID_FROM_DEVICE = "APP_0201" + INVALID_MSG = "APP_0202" + UNKNOWN_APP = "APP_0203" + APP_NOT_ACTIVE = "APP_0204" + DEVICE_SETUP_REQUIRED = "APP_0205" + APP_TIMEOUT = "APP_0206" + DEVICE_SESSION_INVALID = "APP_0207" + WALLET_NOT_FOUND = "APP_0300" + WALLET_PARTIAL_STATE = "APP_0301" + CARD_OPERATION_FAILED = "APP_0400" + USER_REJECTION = "APP_0501" + CORRUPT_DATA = "APP_0600" + DEVICE_AUTH_FAILED = "APP_0700" + CARD_AUTH_FAILED = "APP_0701" + + +deviceAppErrorTypeDetails: Dict[DeviceAppErrorType, Dict[str, Any]] = { + DeviceAppErrorType.UNKNOWN_ERROR: { + "subError": {}, + "message": "Unknown application error", + }, + DeviceAppErrorType.EXECUTING_OTHER_COMMAND: { + "subError": {}, + "message": "The device is executing some other command", + }, + DeviceAppErrorType.PROCESS_ABORTED: { + "subError": {}, + "message": "The process was aborted", + }, + DeviceAppErrorType.DEVICE_ABORT: { + "subError": {}, + "message": "The request was timed out on the device", + }, + DeviceAppErrorType.INVALID_MSG_FROM_DEVICE: { + "subError": {}, + "message": "Invalid result received from device", + }, + DeviceAppErrorType.INVALID_APP_ID_FROM_DEVICE: { + "subError": {}, + "message": "Invalid appId received from device", + }, + DeviceAppErrorType.INVALID_MSG: { + "subError": {}, + "message": "Invalid result sent from app", + }, + DeviceAppErrorType.UNKNOWN_APP: { + "subError": {}, + "message": "The app does not exist on device", + }, + DeviceAppErrorType.APP_NOT_ACTIVE: { + "subError": {}, + "message": "The app is active on the device", + }, + DeviceAppErrorType.APP_TIMEOUT: { + "subError": {}, + "message": "Operation timed out on device", + }, + DeviceAppErrorType.DEVICE_SETUP_REQUIRED: { + "subError": {}, + "message": "Device setup is required", + }, + DeviceAppErrorType.WALLET_NOT_FOUND: { + "subError": {}, + "message": "Selected wallet is not present on the device", + }, + DeviceAppErrorType.WALLET_PARTIAL_STATE: { + "subError": {}, + "message": "Selected wallet is in partial state", + }, + DeviceAppErrorType.CARD_OPERATION_FAILED: { + "subError": cardErrorTypeDetails, + "message": "Card operation failed", + }, + DeviceAppErrorType.USER_REJECTION: { + "subError": {}, + "message": "User rejected the operation", + }, + DeviceAppErrorType.CORRUPT_DATA: { + "subError": {}, + "message": "Corrupt data error from device", + }, + DeviceAppErrorType.DEVICE_AUTH_FAILED: { + "subError": {}, + "message": "Device seems to be compromised. Contact Cypherock support", + }, + DeviceAppErrorType.CARD_AUTH_FAILED: { + "subError": {}, + "message": "Card seems to be compromised. Contact Cypherock support", + }, + DeviceAppErrorType.DEVICE_SESSION_INVALID: { + "subError": {}, + "message": "Could not establish session on device. Try again, or contact Cypherock support", + }, +} + + +class DeviceAppError(DeviceError): + def __init__( + self, + error_code: DeviceAppErrorType, + error_value: Optional[Union[int, str]] = None, + ): + message: str + error_code_key: str + + details = deviceAppErrorTypeDetails[error_code] + + if error_value is not None and error_value in details["subError"]: + sub_error = details["subError"][error_value] + message = sub_error.message + error_code_key = sub_error.error_code + else: + message = details["message"] + if error_value is not None: + message = f"{message} ({error_value})" + error_code_key = error_code.value + + super().__init__(error_code_key, message, DeviceAppError) diff --git a/hwilib/devices/cypherock_sdk/errors/card_error.py b/hwilib/devices/cypherock_sdk/errors/card_error.py new file mode 100644 index 000000000..4722b29b4 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/errors/card_error.py @@ -0,0 +1,92 @@ +from enum import Enum +from ..core.encoders.proto.generated.error_pb2 import CardError +from .sub_error import SubErrorToMap, SubErrorDetail + + +class CardAppErrorType(Enum): + UNKNOWN = "APP_0400_001" + NOT_PAIRED = "APP_0400_002" + SW_INCOMPATIBLE_APPLET = "APP_0400_003" + SW_NULL_POINTER_EXCEPTION = "APP_0400_004" + SW_TRANSACTION_EXCEPTION = "APP_0400_005" + SW_FILE_INVALID = "APP_0400_006" + SW_SECURITY_CONDITIONS_NOT_SATISFIED = "APP_0400_007" + SW_CONDITIONS_NOT_SATISFIED = "APP_0400_008" + SW_WRONG_DATA = "APP_0400_009" + SW_FILE_NOT_FOUND = "APP_0400_010" + SW_RECORD_NOT_FOUND = "APP_0400_011" + SW_FILE_FULL = "APP_0400_012" + SW_CORRECT_LENGTH_00 = "APP_0400_013" + SW_INVALID_INS = "APP_0400_014" + SW_NOT_PAIRED = "APP_0400_015" + SW_CRYPTO_EXCEPTION = "APP_0400_016" + POW_SW_WALLET_LOCKED = "APP_0400_017" + SW_INS_BLOCKED = "APP_0400_018" + SW_OUT_OF_BOUNDARY = "APP_0400_019" + UNRECOGNIZED = "APP_0400_000" + + +cardErrorTypeDetails = SubErrorToMap[int]() + +cardErrorTypeDetails[CardError.CARD_ERROR_UNKNOWN] = SubErrorDetail( + CardAppErrorType.UNKNOWN.value, "Unknown card error" +) +cardErrorTypeDetails[CardError.CARD_ERROR_NOT_PAIRED] = SubErrorDetail( + CardAppErrorType.NOT_PAIRED.value, "Card is not paired" +) +cardErrorTypeDetails[CardError.CARD_ERROR_SW_INCOMPATIBLE_APPLET] = SubErrorDetail( + CardAppErrorType.SW_INCOMPATIBLE_APPLET.value, "Incompatible applet version" +) +cardErrorTypeDetails[CardError.CARD_ERROR_SW_NULL_POINTER_EXCEPTION] = SubErrorDetail( + CardAppErrorType.SW_NULL_POINTER_EXCEPTION.value, "Null pointer exception" +) +cardErrorTypeDetails[CardError.CARD_ERROR_SW_TRANSACTION_EXCEPTION] = SubErrorDetail( + CardAppErrorType.SW_TRANSACTION_EXCEPTION.value, "Operation failed on card (Tx Exp)" +) +cardErrorTypeDetails[CardError.CARD_ERROR_SW_FILE_INVALID] = SubErrorDetail( + CardAppErrorType.SW_FILE_INVALID.value, "Tapped card family id mismatch" +) +cardErrorTypeDetails[CardError.CARD_ERROR_SW_SECURITY_CONDITIONS_NOT_SATISFIED] = ( + SubErrorDetail( + CardAppErrorType.SW_SECURITY_CONDITIONS_NOT_SATISFIED.value, + "Security conditions not satisfied, i.e. pairing session invalid", + ) +) +cardErrorTypeDetails[CardError.CARD_ERROR_SW_CONDITIONS_NOT_SATISFIED] = SubErrorDetail( + CardAppErrorType.SW_CONDITIONS_NOT_SATISFIED.value, "Wrong card sequence" +) +cardErrorTypeDetails[CardError.CARD_ERROR_SW_WRONG_DATA] = SubErrorDetail( + CardAppErrorType.SW_WRONG_DATA.value, "Invalid APDU length" +) +cardErrorTypeDetails[CardError.CARD_ERROR_SW_FILE_NOT_FOUND] = SubErrorDetail( + CardAppErrorType.SW_FILE_NOT_FOUND.value, "Corrupted card" +) +cardErrorTypeDetails[CardError.CARD_ERROR_SW_RECORD_NOT_FOUND] = SubErrorDetail( + CardAppErrorType.SW_RECORD_NOT_FOUND.value, "Wallet does not exist on device" +) +cardErrorTypeDetails[CardError.CARD_ERROR_SW_FILE_FULL] = SubErrorDetail( + CardAppErrorType.SW_FILE_FULL.value, "Card is full" +) +cardErrorTypeDetails[CardError.CARD_ERROR_SW_CORRECT_LENGTH_00] = SubErrorDetail( + CardAppErrorType.SW_CORRECT_LENGTH_00.value, "Incorrect pin entered" +) +cardErrorTypeDetails[CardError.CARD_ERROR_SW_INVALID_INS] = SubErrorDetail( + CardAppErrorType.SW_INVALID_INS.value, "Applet unknown error" +) +cardErrorTypeDetails[CardError.CARD_ERROR_SW_NOT_PAIRED] = SubErrorDetail( + CardAppErrorType.SW_NOT_PAIRED.value, "Card pairing to device missing" +) +cardErrorTypeDetails[CardError.CARD_ERROR_SW_CRYPTO_EXCEPTION] = SubErrorDetail( + CardAppErrorType.SW_CRYPTO_EXCEPTION.value, "Operation failed on card (Crypto Exp)" +) +cardErrorTypeDetails[CardError.CARD_ERROR_POW_SW_WALLET_LOCKED] = SubErrorDetail( + CardAppErrorType.POW_SW_WALLET_LOCKED.value, + "Locked wallet status word, POW meaning proof of word", +) +cardErrorTypeDetails[CardError.CARD_ERROR_SW_INS_BLOCKED] = SubErrorDetail( + CardAppErrorType.SW_INS_BLOCKED.value, "Card health critical, migration required" +) +cardErrorTypeDetails[CardError.CARD_ERROR_SW_OUT_OF_BOUNDARY] = SubErrorDetail( + CardAppErrorType.SW_OUT_OF_BOUNDARY.value, + "Operation failed on card (Out of boundary)", +) diff --git a/hwilib/devices/cypherock_sdk/errors/communication_error.py b/hwilib/devices/cypherock_sdk/errors/communication_error.py new file mode 100644 index 000000000..d5bd7eaf8 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/errors/communication_error.py @@ -0,0 +1,55 @@ +from enum import Enum +from typing import Dict +from .device_error import DeviceError + + +class DeviceCommunicationErrorType(Enum): + IN_BOOTLOADER = "COM_0000" + UNKNOWN_COMMUNICATION_ERROR = "COM_0100" + WRITE_ERROR = "COM_0101" + WRITE_TIMEOUT = "COM_0102" + READ_TIMEOUT = "COM_0103" + WRITE_REJECTED = "COM_0104" + + +class CodeToErrorMap: + def __init__(self): + self._map: Dict[DeviceCommunicationErrorType, Dict[str, str]] = { + DeviceCommunicationErrorType.IN_BOOTLOADER: { + "message": "Device is in bootloader mode" + }, + DeviceCommunicationErrorType.WRITE_REJECTED: { + "message": "The write packet operation was rejected by the device" + }, + DeviceCommunicationErrorType.WRITE_ERROR: { + "message": "Unable to write packet to the device" + }, + DeviceCommunicationErrorType.WRITE_TIMEOUT: { + "message": "Did not receive ACK of sent packet on time" + }, + DeviceCommunicationErrorType.READ_TIMEOUT: { + "message": "Did not receive the expected data from device on time" + }, + DeviceCommunicationErrorType.UNKNOWN_COMMUNICATION_ERROR: { + "message": "Unknown Error at communication module" + }, + } + + def __getitem__(self, key: DeviceCommunicationErrorType) -> Dict[str, str]: + return self._map.get(key) + + +deviceCommunicationErrorTypeDetails = CodeToErrorMap() + + +class DeviceCommunicationError(DeviceError): + """ + Device communication error class. + """ + + def __init__(self, error_code: DeviceCommunicationErrorType): + super().__init__( + error_code.value, + deviceCommunicationErrorTypeDetails[error_code]["message"], + DeviceCommunicationError, + ) diff --git a/hwilib/devices/cypherock_sdk/errors/compatibility_error.py b/hwilib/devices/cypherock_sdk/errors/compatibility_error.py new file mode 100644 index 000000000..4cbb26b93 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/errors/compatibility_error.py @@ -0,0 +1,35 @@ +from enum import Enum +from typing import Dict +from .device_error import DeviceError + + +class DeviceCompatibilityErrorType(Enum): + INVALID_SDK_OPERATION = "COM_0200" + DEVICE_NOT_SUPPORTED = "COM_0201" + + +class CodeToErrorMap: + def __init__(self): + self._map: Dict[DeviceCompatibilityErrorType, Dict[str, str]] = { + DeviceCompatibilityErrorType.INVALID_SDK_OPERATION: { + "message": "The device sdk does not support this function" + }, + DeviceCompatibilityErrorType.DEVICE_NOT_SUPPORTED: { + "message": "The connected device is not supported by this SDK" + }, + } + + def __getitem__(self, key: DeviceCompatibilityErrorType) -> Dict[str, str]: + return self._map.get(key) + + +deviceCompatibilityErrorTypeDetails = CodeToErrorMap() + + +class DeviceCompatibilityError(DeviceError): + def __init__(self, error_code: DeviceCompatibilityErrorType): + super().__init__( + error_code.value, + deviceCompatibilityErrorTypeDetails[error_code]["message"], + DeviceCompatibilityError, + ) diff --git a/hwilib/devices/cypherock_sdk/errors/connection_error.py b/hwilib/devices/cypherock_sdk/errors/connection_error.py new file mode 100644 index 000000000..b31c90de7 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/errors/connection_error.py @@ -0,0 +1,37 @@ +from enum import Enum +from typing import Dict +from .device_error import DeviceError + + +class DeviceConnectionErrorType(Enum): + NOT_CONNECTED = "CON_0100" + CONNECTION_CLOSED = "CON_0101" + FAILED_TO_CONNECT = "CON_0102" + + +class CodeToErrorMap: + def __init__(self): + self._map: Dict[DeviceConnectionErrorType, Dict[str, str]] = { + DeviceConnectionErrorType.NOT_CONNECTED: {"message": "No device connected"}, + DeviceConnectionErrorType.CONNECTION_CLOSED: { + "message": "Connection was closed while in process" + }, + DeviceConnectionErrorType.FAILED_TO_CONNECT: { + "message": "Failed to create device connection" + }, + } + + def __getitem__(self, key: DeviceConnectionErrorType) -> Dict[str, str]: + return self._map.get(key) + + +deviceConnectionErrorTypeDetails = CodeToErrorMap() + + +class DeviceConnectionError(DeviceError): + def __init__(self, error_code: DeviceConnectionErrorType): + super().__init__( + error_code.value, + deviceConnectionErrorTypeDetails[error_code]["message"], + DeviceConnectionError, + ) diff --git a/hwilib/devices/cypherock_sdk/errors/device_error.py b/hwilib/devices/cypherock_sdk/errors/device_error.py new file mode 100644 index 000000000..1d940b0c2 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/errors/device_error.py @@ -0,0 +1,29 @@ +from typing import Any, Dict, Type + + +class DeviceError(Exception): + def __init__(self, error_code: str, message: str, cls: Type): + super().__init__(message) + self.code = error_code + self.message = message + self.is_device_error = True + self.set_prototype(cls) + + def set_prototype(self, cls: Type) -> None: + # In Python, this is handled by proper inheritance, but we keep the method + # for API compatibility with the TypeScript version + pass + + def to_json(self) -> Dict[str, Any]: + """ + Convert the error to a JSON-serializable dictionary. + + Returns: + Dict with error details + """ + return { + "code": self.code, + "message": f"{self.code}: {self.message}", + "isDeviceError": self.is_device_error, + "stack": self.__traceback__, + } diff --git a/hwilib/devices/cypherock_sdk/errors/sub_error.py b/hwilib/devices/cypherock_sdk/errors/sub_error.py new file mode 100644 index 000000000..314709baf --- /dev/null +++ b/hwilib/devices/cypherock_sdk/errors/sub_error.py @@ -0,0 +1,32 @@ +from typing import Dict, TypeVar, Generic, Union + +T = TypeVar("T", int, str) + + +class SubErrorDetail: + def __init__(self, error_code: str, message: str): + self.error_code = error_code + self.message = message + + +class SubErrorToMap(Generic[T]): + def __init__(self): + self._map: Dict[T, SubErrorDetail] = {} + + def __getitem__(self, key: T) -> SubErrorDetail: + return self._map.get(key) + + def __setitem__(self, key: T, value: SubErrorDetail) -> None: + self._map[key] = value + + def get(self, key: T) -> Union[SubErrorDetail, None]: + return self._map.get(key) + + def items(self): + return self._map.items() + + def keys(self): + return self._map.keys() + + def values(self): + return self._map.values() diff --git a/hwilib/devices/cypherock_sdk/hw_hid/README.md b/hwilib/devices/cypherock_sdk/hw_hid/README.md new file mode 100644 index 000000000..89b236669 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/hw_hid/README.md @@ -0,0 +1,3 @@ +# hw_hid + +This package provides the hardware HID (Human Interface Device) connector for Cypherock X1 wallet. \ No newline at end of file diff --git a/hwilib/devices/cypherock_sdk/hw_hid/__init__.py b/hwilib/devices/cypherock_sdk/hw_hid/__init__.py new file mode 100644 index 000000000..6fcb80260 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/hw_hid/__init__.py @@ -0,0 +1,4 @@ +from .device_connection import DeviceConnection +from .helpers import get_available_devices + +__all__ = ["DeviceConnection", "get_available_devices"] diff --git a/hwilib/devices/cypherock_sdk/hw_hid/device_connection.py b/hwilib/devices/cypherock_sdk/hw_hid/device_connection.py new file mode 100644 index 000000000..7d0e00f20 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/hw_hid/device_connection.py @@ -0,0 +1,109 @@ +from typing import Any, List, Optional +import logging +import hid + +from ..interfaces import ( + IDevice, + PoolData, + IDeviceConnection, +) + +from .helpers import DataListener, get_available_devices + +from hwilib.errors import DeviceConnectionError + +logger = logging.getLogger(__name__) + + +class DeviceConnection(IDeviceConnection): + def __init__(self, device: IDevice, connection: Any): + self.device: IDevice = device + self.sequence_number = 0 + self.initialized = True + self.is_port_open = True + self.connection = connection + + listener_params = { + "connection": self.connection, + "device": self.device, + "on_close": self.on_close, + "on_error": self.on_error, + } + self.data_listener: DataListener = DataListener(listener_params) + + @staticmethod + def connect(device: IDevice) -> "DeviceConnection": + try: + connection = hid.device() + connection.open_path(device["path"].encode()) + except Exception as e: + raise DeviceConnectionError(f"Failed to connect to device: {e}") + + return DeviceConnection(device, connection) + + @staticmethod + def list(): + return get_available_devices() + + @staticmethod + def create(): + devices = get_available_devices() + + if not devices: + raise DeviceConnectionError("No devices found") + + device_to_connect = devices[0] + return DeviceConnection.connect(device_to_connect) + + @staticmethod + def get_available_connection(): + connection_info = get_available_devices() + return connection_info + + def is_initialized(self) -> bool: + return self.initialized + + def get_new_sequence_number(self) -> int: + self.sequence_number += 1 + return self.sequence_number + + def get_sequence_number(self) -> int: + return self.sequence_number + + def is_connected(self) -> bool: + return self.is_port_open + + def destroy(self) -> None: + if not self.is_port_open: + return + + self.data_listener.destroy() + try: + self.connection.close() + except Exception as error: + logger.warn("Error while closing device connection") + logger.warn(error) + + def before_operation(self) -> None: + self.data_listener.start_listening() + + def after_operation(self) -> None: + self.data_listener.stop_listening() + + def send(self, data: bytearray) -> None: + data_to_write = [0x00] + list(data) + [0x00] * (64 - len(data)) + self.connection.write(bytes(data_to_write)) + + def receive(self) -> Optional[bytearray]: + result = self.data_listener.receive() + return bytearray(result) if result is not None else None + + def peek(self) -> List[PoolData]: + return self.data_listener.peek() + + def on_close(self): + self.is_port_open = False + + def on_error(self, error: Exception): + logger.error("Error on device connection callback") + logger.error(error) diff --git a/hwilib/devices/cypherock_sdk/hw_hid/helpers/__init__.py b/hwilib/devices/cypherock_sdk/hw_hid/helpers/__init__.py new file mode 100644 index 000000000..565a414d8 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/hw_hid/helpers/__init__.py @@ -0,0 +1,4 @@ +from .connection import get_available_devices +from .data_listeners import DataListener + +__all__ = ["get_available_devices", "DataListener"] diff --git a/hwilib/devices/cypherock_sdk/hw_hid/helpers/connection.py b/hwilib/devices/cypherock_sdk/hw_hid/helpers/connection.py new file mode 100644 index 000000000..8a49d1513 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/hw_hid/helpers/connection.py @@ -0,0 +1,32 @@ +from typing import List, cast +from ...interfaces import IDevice + +import hid + +CYPHEROCK_VENDOR_ID = 0x3503 +CYPHEROCK_PRODUCT_ID = 0x0103 + +def get_available_devices() -> List[IDevice]: + result_device_list: List[IDevice] = [] + + devices = hid.enumerate(CYPHEROCK_VENDOR_ID, CYPHEROCK_PRODUCT_ID) + for d in devices: + if d.get('path') is not None and d.get('serial_number') is not None: + device_info = cast( + IDevice, + { + "path": d['path'].decode(), + "vendor_id": d['vendor_id'], + "product_id": d['product_id'], + "serial": d['serial_number'], + "type": 'cypherock', + "model": 'cypherock-x1', + "label": None, + "needs_pin_sent": False, + "needs_passphrase_sent": False, + "fingerprint": bytes.fromhex(d['serial_number']).hex(), # Using the serial number as the fingerprint for now + } + ) + result_device_list.append(device_info) + + return result_device_list diff --git a/hwilib/devices/cypherock_sdk/hw_hid/helpers/data_listeners.py b/hwilib/devices/cypherock_sdk/hw_hid/helpers/data_listeners.py new file mode 100644 index 000000000..712a852ef --- /dev/null +++ b/hwilib/devices/cypherock_sdk/hw_hid/helpers/data_listeners.py @@ -0,0 +1,145 @@ +import threading +import time +import uuid +import logging +from typing import Any, Dict, Optional + +from ...interfaces import IDevice, PoolData + +from .connection import get_available_devices + +logger = logging.getLogger(__name__) + + +class DataListener: + def __init__(self, params: Dict[str, Any]): + self.connection = params["connection"] + self.device: IDevice = params["device"] + self.on_close_callback = params.get("on_close") + self.on_error_callback = params.get("on_error") + self.on_some_device_disconnect_binded = self.on_some_device_disconnect + self.listening = False + self.pool: [PoolData] = [] + + self.read_thread: Optional[threading.Thread] = None + self._monitor_thread: Optional[threading.Thread] = None + self._stop_event = threading.Event() + + self.add_all_listeners() + + def destroy(self): + self.stop_listening() + self.remove_all_listeners() + + if self.on_close_callback: + self.on_close_callback() + + def is_listening(self): + return self.listening + + def receive(self): + if self.pool: + return self.pool.pop(0).get("data") + return None + + def peek(self): + return self.pool.copy() + + def stop_read_thread(self): + if self.read_thread and self.read_thread.is_alive(): + self.read_thread.join(timeout=1.0) + self.read_thread = None + + def start_read_thread(self): + if self.read_thread is None or not self.read_thread.is_alive(): + self.read_thread = threading.Thread( + target=self._run_read_loop, daemon=True + ) + self.read_thread.start() + + def start_listening(self): + self.listening = True + self.start_read_thread() + + def stop_listening(self): + self.stop_read_thread() + self.listening = False + + def add_all_listeners(self) -> None: + if not self._monitor_thread or not self._monitor_thread.is_alive(): + logger.debug("Starting device disconnect monitor thread.") + self._monitor_thread = threading.Thread( + target=self._run_device_monitor, daemon=True + ) + self._monitor_thread.start() + + def remove_all_listeners(self) -> None: + self._stop_event.set() + if self._monitor_thread and self._monitor_thread.is_alive(): + self._monitor_thread.join(timeout=1.0) + logger.debug("Device disconnect monitor thread stopped.") + + def _run_read_loop(self): + while self.listening: + try: + data = self._read_data() + if data: + self.on_data(data) + except Exception as error: + logger.error("Error while reading data from device") + logger.error(error) + time.sleep(0.1) # Small delay to avoid busy-waiting + + def _read_data(self): + try: + return self.connection.read(64) + except Exception as error: + # Only call error callback for actual errors, not timeouts/empty reads + error_str = str(error).lower() + if "timeout" not in error_str and "read error" not in error_str and self.on_error_callback: + self.on_error_callback(error) + return None + + def on_data(self, data): + if data and len(data) > 0: + self.pool.append({"id": str(uuid.uuid4()), "data": bytearray(data)}) + + def on_close(self): + self.stop_listening() + self.remove_all_listeners() + + if self.on_close_callback: + self.on_close_callback() + + def on_error(self, error: Exception): + if self.on_error_callback: + self.on_error_callback(error) + + def _run_device_monitor(self): + while not self._stop_event.is_set(): + try: + self._check_device_connection() + except Exception as e: + logger.error(f"Error in device monitor: {e}") + time.sleep(1) + + def _check_device_connection(self): + try: + self.on_some_device_disconnect() + except Exception as e: + logger.error(f"Error checking device connection: {e}") + + def on_some_device_disconnect(self): + connected_devices = get_available_devices() + + is_device_connected = any( + d["path"] == self.device["path"] + and d["serial"] == self.device["serial"] + and d["product_id"] == self.device["product_id"] + and d["vendor_id"] == self.device["vendor_id"] + for d in connected_devices + ) + + if not is_device_connected: + self.destroy() + self.on_close() diff --git a/hwilib/devices/cypherock_sdk/interfaces.py b/hwilib/devices/cypherock_sdk/interfaces.py new file mode 100644 index 000000000..b51532bc7 --- /dev/null +++ b/hwilib/devices/cypherock_sdk/interfaces.py @@ -0,0 +1,40 @@ +# flake8: noqa E704 +from typing import List, Literal, Optional, Protocol, TypedDict, runtime_checkable + +class IDevice(TypedDict): + path: str + vendor_id: int + product_id: int + serial: str + type: Literal['cypherock'] + model: Literal['cypherock-x1'] + label: Optional[str] + needs_pin_sent: bool + needs_passphrase_sent: bool + fingerprint: str + +class PoolData(TypedDict): + id: str + data: bytes + +@runtime_checkable +class IDeviceConnection(Protocol): + def get_connection_type(self) -> str: ... + + def is_connected(self) -> bool: ... + + def before_operation(self) -> None: ... + + def after_operation(self) -> None: ... + + def get_sequence_number(self) -> int: ... + + def get_new_sequence_number(self) -> int: ... + + def send(self, data: bytes) -> None: ... + + def receive(self) -> Optional[bytes]: ... + + def peek(self) -> List[PoolData]: ... + + def destroy(self) -> None: ... diff --git a/hwilib/udev/21-cypherock.rules b/hwilib/udev/21-cypherock.rules new file mode 100644 index 000000000..bf5546c48 --- /dev/null +++ b/hwilib/udev/21-cypherock.rules @@ -0,0 +1,8 @@ +# Put this file into /etc/udev/rules.d +# Cypherock X1 +SUBSYSTEM=="input", GROUP="input", MODE="0666" +SUBSYSTEM=="usb", ATTRS{idVendor}=="3503", ATTRS{idProduct}=="0103", MODE:="666", GROUP="plugdev" +KERNEL=="hidraw*", ATTRS{idVendor}=="3503", ATTRS{idProduct}=="0103", MODE="0666", GROUP="plugdev" +SUBSYSTEM=="tty", ATTRS{idVendor}=="3503", ATTRS{idProduct}=="0103", GROUP="dialout", MODE="0666" +SUBSYSTEM=="tty", ATTRS{idVendor}=="3503", ATTRS{idProduct}=="0102", GROUP="dialout", MODE="0666" +SUBSYSTEM=="tty", ATTRS{idVendor}=="3503", ATTRS{idProduct}=="0101", GROUP="dialout", MODE="0666" \ No newline at end of file diff --git a/hwilib/udev/README.md b/hwilib/udev/README.md index b3078d78f..71aa65380 100644 --- a/hwilib/udev/README.md +++ b/hwilib/udev/README.md @@ -8,6 +8,7 @@ These are necessary for the devices to be reachable on linux environments. - `51-hid-digitalbitbox.rules`, `52-hid-digitalbitbox.rules` (Digital Bitbox): https://shiftcrypto.ch/start_linux - `51-trezor.rules` (Trezor): https://github.com/trezor/trezor-common/blob/master/udev/51-trezor.rules - `51-usb-keepkey.rules` (Keepkey): https://github.com/keepkey/udev-rules/blob/master/51-usb-keepkey.rules + - `21-cypherock.rules` (Cypherock): https://github.com/Cypherock/cysync-scripts/blob/main/configure-usb.sh # Usage