diff --git a/hwilib/_cli.py b/hwilib/_cli.py index acd4b2578..e0afa7dd2 100644 --- a/hwilib/_cli.py +++ b/hwilib/_cli.py @@ -60,7 +60,7 @@ def displayaddress_handler(args: argparse.Namespace, client: HardwareWalletClien return displayaddress(client, desc=args.desc, path=args.path, addr_type=args.addr_type) def enumerate_handler(args: argparse.Namespace) -> List[Dict[str, Any]]: - return enumerate(password=args.password, expert=args.expert, chain=args.chain) + return enumerate(password=args.password, expert=args.expert, chain=args.chain, allow_emulators=args.allow_emulators) def getmasterxpub_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, str]: return getmasterxpub(client, addrtype=args.addr_type, account=args.account) @@ -145,6 +145,7 @@ def get_parser() -> HWIArgumentParser: parser.add_argument('--stdin', help='Enter commands and arguments via stdin', action='store_true') parser.add_argument('--interactive', '-i', help='Use some commands interactively. Currently required for all device configuration commands', action='store_true') parser.add_argument('--expert', help='Do advanced things and get more detailed information returned from some commands. Use at your own risk.', action='store_true') + parser.add_argument("--emulators", help="Enable enumeration and detection of device emulators", action="store_true", dest="allow_emulators") subparsers = parser.add_subparsers(description='Commands', dest='command') # work-around to make subparser required @@ -277,9 +278,9 @@ def process_commands(cli_args: List[str]) -> Any: # Auto detect if we are using fingerprint or type to identify device if args.fingerprint or (args.device_type and not args.device_path): - client = find_device(args.password, args.device_type, args.fingerprint, args.expert, args.chain) + client = find_device(args.password, args.device_type, args.fingerprint, args.expert, args.chain, args.allow_emulators) if not client: - return {'error': 'Could not find device with specified fingerprint', 'code': DEVICE_CONN_ERROR} + return {'error': 'Could not find device with specified fingerprint or type', 'code': DEVICE_CONN_ERROR} elif args.device_type and args.device_path: with handle_errors(result=result, code=DEVICE_CONN_ERROR): client = get_client(device_type, device_path, password, args.expert, args.chain) diff --git a/hwilib/commands.py b/hwilib/commands.py index 92f8d9f45..6d192aa5f 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -101,7 +101,7 @@ def get_client(device_type: str, device_path: str, password: Optional[str] = Non return client # Get a list of all available hardware wallets -def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN) -> List[Dict[str, Any]]: +def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN, allow_emulators: bool = False) -> List[Dict[str, Any]]: """ Enumerate all of the devices that HWI can potentially access. @@ -114,7 +114,7 @@ def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain for module in all_devs: try: imported_dev = importlib.import_module('.devices.' + module, __package__) - result.extend(imported_dev.enumerate(password, expert, chain)) + result.extend(imported_dev.enumerate(password, expert, chain, allow_emulators)) except ImportError as e: # Warn for ImportErrors, but largely ignore them to allow users not install # all device dependencies if only one or some devices are wanted. @@ -129,6 +129,7 @@ def find_device( fingerprint: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN, + allow_emulators: bool = False, ) -> Optional[HardwareWalletClient]: """ Find a device from the device type or fingerprint and get a client to access it. @@ -145,7 +146,7 @@ def find_device( :return: A client to interact with the found device """ - devices = enumerate(password) + devices = enumerate(password, expert, chain, allow_emulators) for d in devices: if device_type is not None and d['type'] != device_type and d['model'] != device_type: continue diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index 3e32517cb..173f9997d 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -173,7 +173,7 @@ def _xpubs_equal_ignoring_version(xpub1: bytes, xpub2: bytes) -> bool: return xpub1[4:] == xpub2[4:] -def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN) -> List[Dict[str, Any]]: +def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN, allow_emulators: bool = False) -> List[Dict[str, Any]]: """ Enumerate all BitBox02 devices. Bootloaders excluded. """ diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index c3c9d0269..e64f3619c 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -399,10 +399,11 @@ def can_sign_taproot(self) -> bool: return False -def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN) -> List[Dict[str, Any]]: +def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN, allow_emulators: bool = True) -> List[Dict[str, Any]]: results = [] devices = hid.enumerate(COINKITE_VID, CKCC_PID) - devices.append({'path': CC_SIMULATOR_SOCK.encode()}) + if allow_emulators: + devices.append({'path': CC_SIMULATOR_SOCK.encode()}) for d in devices: d_data: Dict[str, Any] = {} diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index a3693b7b9..2733d0a2e 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -679,17 +679,18 @@ def can_sign_taproot(self) -> bool: return False -def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN) -> List[Dict[str, Any]]: +def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN, allow_emulators: bool = False) -> List[Dict[str, Any]]: results = [] devices = hid.enumerate(DBB_VENDOR_ID, DBB_DEVICE_ID) # Try connecting to simulator - try: - dev = BitboxSimulator('127.0.0.1', 35345) - dev.send_recv(b'{"device" : "info"}') - devices.append({'path': b'udp:127.0.0.1:35345', 'interface_number': 0}) - dev.close() - except Exception: - pass + if allow_emulators: + try: + dev = BitboxSimulator('127.0.0.1', 35345) + dev.send_recv(b'{"device" : "info"}') + devices.append({'path': b'udp:127.0.0.1:35345', 'interface_number': 0}) + dev.close() + except Exception: + pass for d in devices: if ('interface_number' in d and d['interface_number'] == 0 or ('usage_page' in d and d['usage_page'] == 0xffff)): diff --git a/hwilib/devices/jade.py b/hwilib/devices/jade.py index fdfeed29c..6d471f6fc 100644 --- a/hwilib/devices/jade.py +++ b/hwilib/devices/jade.py @@ -508,7 +508,7 @@ def can_sign_taproot(self) -> bool: return False -def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN) -> List[Dict[str, Any]]: +def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN, allow_emulators: bool = False) -> List[Dict[str, Any]]: results = [] def _get_device_entry(device_model: str, device_path: str) -> Dict[str, Any]: @@ -537,16 +537,17 @@ def _get_device_entry(device_model: str, device_path: str) -> Dict[str, Any]: results.append(_get_device_entry('jade', devinfo.device)) # If we can connect to the simulator, add it too - try: - with JadeAPI.create_serial(SIMULATOR_PATH, timeout=1) as jade: - verinfo = jade.get_version_info() + if allow_emulators: + try: + with JadeAPI.create_serial(SIMULATOR_PATH, timeout=1) as jade: + verinfo = jade.get_version_info() - if verinfo is not None: - results.append(_get_device_entry('jade_simulator', SIMULATOR_PATH)) + if verinfo is not None: + results.append(_get_device_entry('jade_simulator', SIMULATOR_PATH)) - except Exception as e: - # If we get any sort of error do not add the simulator - logging.debug(f'Failed to connect to Jade simulator at {SIMULATOR_PATH}') - logging.debug(e) + except Exception as e: + # If we get any sort of error do not add the simulator + logging.debug(f'Failed to connect to Jade simulator at {SIMULATOR_PATH}') + logging.debug(e) return results diff --git a/hwilib/devices/keepkey.py b/hwilib/devices/keepkey.py index 894dca685..54b395dac 100644 --- a/hwilib/devices/keepkey.py +++ b/hwilib/devices/keepkey.py @@ -171,11 +171,12 @@ def can_sign_taproot(self) -> bool: return False -def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN) -> List[Dict[str, Any]]: +def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN, allow_emulators: bool = False) -> List[Dict[str, Any]]: results = [] devs = hid.HidTransport.enumerate(usb_ids=KEEPKEY_HID_IDS) devs.extend(webusb.WebUsbTransport.enumerate(usb_ids=KEEPKEY_WEBUSB_IDS)) - devs.extend(udp.UdpTransport.enumerate(KEEPKEY_SIMULATOR_PATH)) + if allow_emulators: + devs.extend(udp.UdpTransport.enumerate(KEEPKEY_SIMULATOR_PATH)) for dev in devs: d_data: Dict[str, Any] = {} diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 0db9cf382..f41ecb89c 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -546,11 +546,12 @@ def can_sign_taproot(self) -> bool: return isinstance(self.client, NewClient) -def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN) -> List[Dict[str, Any]]: +def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN, allow_emulators: bool = False) -> List[Dict[str, Any]]: results = [] devices = [] devices.extend(hid.enumerate(LEDGER_VENDOR_ID, 0)) - devices.append({'path': SIMULATOR_PATH.encode(), 'interface_number': 0, 'product_id': 0x1000}) + if allow_emulators: + devices.append({'path': SIMULATOR_PATH.encode(), 'interface_number': 0, 'product_id': 0x1000}) for d in devices: if ('interface_number' in d and d['interface_number'] == 0 diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 1848e8f42..e39b342ce 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -851,11 +851,12 @@ def can_sign_taproot(self) -> bool: return True -def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN) -> List[Dict[str, Any]]: +def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN, allow_emulators: bool = False) -> List[Dict[str, Any]]: results = [] devs = hid.HidTransport.enumerate() devs.extend(webusb.WebUsbTransport.enumerate()) - devs.extend(udp.UdpTransport.enumerate()) + if allow_emulators: + devs.extend(udp.UdpTransport.enumerate()) for dev in devs: d_data: Dict[str, Any] = {} diff --git a/test/test_device.py b/test/test_device.py index 1938c3e23..8dae52308 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -136,7 +136,7 @@ def __init__(self, bitcoind, emulator=None, interface='library', methodName='run self.rpc = bitcoind.rpc self.emulator = emulator - self.dev_args = ['-t', self.emulator.type, '-d', self.emulator.path, '--chain', 'test'] + self.dev_args = ['-t', self.emulator.type, '-d', self.emulator.path, '--chain', 'test', "--emulators"] if self.emulator.password is not None: self.dev_args.extend(['-p', self.emulator.password]) @@ -215,32 +215,42 @@ def test_enumerate(self): found = True self.assertTrue(found) + def test_no_emus(self): + res = self.do_command(self.get_password_args() + ["enumerate"]) + self.assertEqual(len(res), 0) + res = self.do_command(self.get_password_args() + ["-f", self.emulator.fingerprint, "--chain", "test", "getmasterxpub", "--addr-type", "legacy"]) + self.assertEqual(res['error'], 'Could not find device with specified fingerprint or type') + self.assertEqual(res['code'], -3) + res = self.do_command(self.get_password_args() + ["-t", self.detect_type, "--chain", "test", "getmasterxpub", "--addr-type", "legacy"]) + self.assertEqual(res['error'], 'Could not find device with specified fingerprint or type') + self.assertEqual(res['code'], -3) + def test_no_type(self): - gmxp_res = self.do_command(["--chain", "test", 'getmasterxpub', "--addr-type", "legacy"]) + gmxp_res = self.do_command(["--chain", "test", "--emulators", "getmasterxpub", "--addr-type", "legacy"]) self.assertIn('error', gmxp_res) self.assertEqual(gmxp_res['error'], 'You must specify a device type or fingerprint for all commands except enumerate') self.assertIn('code', gmxp_res) self.assertEqual(gmxp_res['code'], -1) def test_path_type(self): - gmxp_res = self.do_command(self.get_password_args() + ['-t', self.detect_type, '-d', self.emulator.path, "--chain", "test", 'getmasterxpub', "--addr-type", "legacy"]) + gmxp_res = self.do_command(self.get_password_args() + ["-t", self.detect_type, "-d", self.emulator.path, "--chain", "test", "--emulators", "getmasterxpub", "--addr-type", "legacy"]) self.assertEqual(gmxp_res['xpub'], self.emulator.master_xpub) def test_fingerprint_autodetect(self): - gmxp_res = self.do_command(self.get_password_args() + ['-f', self.emulator.fingerprint, "--chain", "test", 'getmasterxpub', "--addr-type", "legacy"]) + gmxp_res = self.do_command(self.get_password_args() + ["-f", self.emulator.fingerprint, "--chain", "test", "--emulators", "getmasterxpub", "--addr-type", "legacy"]) self.assertEqual(gmxp_res['xpub'], self.emulator.master_xpub) # Nonexistent fingerprint - gmxp_res = self.do_command(self.get_password_args() + ['-f', '0000ffff', "--chain", "test", 'getmasterxpub', "--addr-type", "legacy"]) - self.assertEqual(gmxp_res['error'], 'Could not find device with specified fingerprint') + gmxp_res = self.do_command(self.get_password_args() + ["-f", "0000ffff", "--chain", "test", "--emulators", "getmasterxpub", "--addr-type", "legacy"]) + self.assertEqual(gmxp_res['error'], 'Could not find device with specified fingerprint or type') self.assertEqual(gmxp_res['code'], -3) def test_type_only_autodetect(self): - gmxp_res = self.do_command(self.get_password_args() + ['-t', self.detect_type, "--chain", "test", 'getmasterxpub', "--addr-type", "legacy"]) + gmxp_res = self.do_command(self.get_password_args() + ["-t", self.detect_type, "--chain", "test", "--emulators", "getmasterxpub", "--addr-type", "legacy"]) self.assertEqual(gmxp_res['xpub'], self.emulator.master_xpub) # Unknown device type - gmxp_res = self.do_command(['-t', 'fakedev', '-d', 'fakepath', "--chain", "test", 'getmasterxpub', "--addr-type", "legacy"]) + gmxp_res = self.do_command(["-t", "fakedev", "-d", "fakepath", "--chain", "test", "--emulators", "getmasterxpub", "--addr-type", "legacy"]) self.assertEqual(gmxp_res['error'], 'Unknown device type specified') self.assertEqual(gmxp_res['code'], -4) diff --git a/test/test_keepkey.py b/test/test_keepkey.py index 9e74a0cb4..62dee943b 100755 --- a/test/test_keepkey.py +++ b/test/test_keepkey.py @@ -180,16 +180,16 @@ def test_getxpub(self): load_device_by_mnemonic(client=self.client, mnemonic=vec['mnemonic'], pin='', passphrase_protection=False, label='test', language='english') # Test getmasterxpub - gmxp_res = self.do_command(['-t', 'keepkey', '-d', 'udp:127.0.0.1:11044', 'getmasterxpub', "--addr-type", "legacy"]) + gmxp_res = self.do_command(["-t", "keepkey", "-d", "udp:127.0.0.1:11044", "--emulators", "getmasterxpub", "--addr-type", "legacy"]) self.assertEqual(gmxp_res['xpub'], vec['master_xpub']) # Test the path derivs for path_vec in vec['vectors']: - gxp_res = self.do_command(['-t', 'keepkey', '-d', 'udp:127.0.0.1:11044', 'getxpub', path_vec['path']]) + gxp_res = self.do_command(["-t", "keepkey", "-d", "udp:127.0.0.1:11044", "--emulators", "getxpub", path_vec["path"]]) self.assertEqual(gxp_res['xpub'], path_vec['xpub']) def test_expert_getxpub(self): - result = self.do_command(['-t', 'keepkey', '-d', 'udp:127.0.0.1:11044', '--expert', 'getxpub', 'm/44h/0h/0h/3']) + result = self.do_command(["-t", "keepkey", "-d", "udp:127.0.0.1:11044", "--expert", "--emulators", "getxpub", "m/44h/0h/0h/3"]) self.assertEqual(result['xpub'], 'xpub6FMafWAi3n3ET2rU5yQr16UhRD1Zx4dELmcEw3NaYeBaNnipcr2zjzYp1sNdwR3aTN37hxAqRWQ13AWUZr6L9jc617mU6EvgYXyBjXrEhgr') self.assertFalse(result['testnet']) self.assertFalse(result['private']) diff --git a/test/test_trezor.py b/test/test_trezor.py index 87fdb251c..dac465816 100755 --- a/test/test_trezor.py +++ b/test/test_trezor.py @@ -175,16 +175,16 @@ def test_getxpub(self): load_device_by_mnemonic(client=self.client, mnemonic=vec['mnemonic'], pin='', passphrase_protection=False, label='test', language='english') # Test getmasterxpub - gmxp_res = self.do_command(['-t', 'trezor', '-d', 'udp:127.0.0.1:21324', 'getmasterxpub', "--addr-type", "legacy"]) + gmxp_res = self.do_command(["-t", "trezor", "-d", "udp:127.0.0.1:21324", "--emulators", "getmasterxpub", "--addr-type", "legacy"]) self.assertEqual(gmxp_res['xpub'], vec['master_xpub']) # Test the path derivs for path_vec in vec['vectors']: - gxp_res = self.do_command(['-t', 'trezor', '-d', 'udp:127.0.0.1:21324', 'getxpub', path_vec['path']]) + gxp_res = self.do_command(["-t", "trezor", "-d", "udp:127.0.0.1:21324", "--emulators", "getxpub", path_vec["path"]]) self.assertEqual(gxp_res['xpub'], path_vec['xpub']) def test_expert_getxpub(self): - result = self.do_command(['-t', 'trezor', '-d', 'udp:127.0.0.1:21324', '--expert', 'getxpub', 'm/44h/0h/0h/3']) + result = self.do_command(["-t", "trezor", "-d", "udp:127.0.0.1:21324", "--expert", "--emulators", "getxpub", "m/44h/0h/0h/3"]) self.assertEqual(result['xpub'], 'xpub6FMafWAi3n3ET2rU5yQr16UhRD1Zx4dELmcEw3NaYeBaNnipcr2zjzYp1sNdwR3aTN37hxAqRWQ13AWUZr6L9jc617mU6EvgYXyBjXrEhgr') self.assertFalse(result['testnet']) self.assertFalse(result['private'])