thww: distinguish devices based on "soft device id" (not just labels) - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit 9d0bb295e6f55a2bff9f5b6770fa744c16af6e8a DIR parent 7dabbdd08234ae036177d056a513a2189e9f14e1 HTML Author: SomberNight <somber.night@protonmail.com> Date: Wed, 8 Apr 2020 14:43:01 +0200 hww: distinguish devices based on "soft device id" (not just labels) fixes #5759 Diffstat: M electrum/base_wizard.py | 4 +++- M electrum/keystore.py | 5 +++++ M electrum/plugin.py | 14 +++++++++++--- M electrum/plugins/hw_wallet/plugin.… | 12 +++++++++++- M electrum/plugins/keepkey/clientbas… | 3 +++ M electrum/plugins/ledger/ledger.py | 12 +++++++++++- M electrum/plugins/safe_t/clientbase… | 3 +++ M electrum/plugins/trezor/clientbase… | 3 +++ 8 files changed, 50 insertions(+), 6 deletions(-) --- DIR diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py t@@ -438,6 +438,7 @@ class BaseWizard(Logger): if not client: raise Exception("failed to find client for device id") root_fingerprint = client.request_root_fingerprint_from_device() label = client.label() # use this as device_info.label might be outdated! + soft_device_id = client.get_soft_device_id() # use this as device_info.device_id might be outdated! except ScriptTypeNotSupported: raise # this is handled in derivation_dialog except BaseException as e: t@@ -451,6 +452,7 @@ class BaseWizard(Logger): 'root_fingerprint': root_fingerprint, 'xpub': xpub, 'label': label, + 'soft_device_id': soft_device_id, } k = hardware_keystore(d) self.on_keystore(k) t@@ -612,7 +614,7 @@ class BaseWizard(Logger): if os.path.exists(path): raise Exception('file already exists at path') if not self.pw_args: - return + return # FIXME pw_args = self.pw_args self.pw_args = None # clean-up so that it can get GC-ed storage = WalletStorage(path) DIR diff --git a/electrum/keystore.py b/electrum/keystore.py t@@ -724,6 +724,7 @@ class Hardware_KeyStore(Xpub, KeyStore): # device reconnects self.xpub = d.get('xpub') self.label = d.get('label') + self.soft_device_id = d.get('soft_device_id') # type: Optional[str] self.handler = None # type: Optional[HardwareHandlerBase] run_hook('init_keystore', self) t@@ -747,6 +748,7 @@ class Hardware_KeyStore(Xpub, KeyStore): 'derivation': self.get_derivation_prefix(), 'root_fingerprint': self.get_root_fingerprint(), 'label':self.label, + 'soft_device_id': self.soft_device_id, } def unpaired(self): t@@ -788,6 +790,9 @@ class Hardware_KeyStore(Xpub, KeyStore): if self.label != client.label(): self.label = client.label() self.is_requesting_to_be_rewritten_to_wallet_file = True + if self.soft_device_id != client.get_soft_device_id(): + self.soft_device_id = client.get_soft_device_id() + self.is_requesting_to_be_rewritten_to_wallet_file = True KeyStoreWithMPK = Union[KeyStore, MasterPublicKeyMixin] # intersection really... DIR diff --git a/electrum/plugin.py b/electrum/plugin.py t@@ -306,6 +306,7 @@ class DeviceInfo(NamedTuple): initialized: Optional[bool] = None exception: Optional[Exception] = None plugin_name: Optional[str] = None # manufacturer, e.g. "trezor" + soft_device_id: Optional[str] = None # if available, used to distinguish same-type hw devices class HardwarePluginToScan(NamedTuple): t@@ -548,7 +549,8 @@ class DeviceMgr(ThreadJob): infos.append(DeviceInfo(device=device, label=client.label(), initialized=client.is_initialized(), - plugin_name=plugin.name)) + plugin_name=plugin.name, + soft_device_id=client.get_soft_device_id())) return infos t@@ -575,6 +577,11 @@ class DeviceMgr(ThreadJob): devices = None if len(infos) == 1: return infos[0] + # select device by id + if keystore.soft_device_id: + for info in infos: + if info.soft_device_id == keystore.soft_device_id: + return info # select device by label automatically; # but only if not a placeholder label and only if there is no collision device_labels = [info.label for info in infos] t@@ -583,7 +590,7 @@ class DeviceMgr(ThreadJob): for info in infos: if info.label == keystore.label: return info - # ask user to select device + # ask user to select device manually msg = _("Please select which {} device to use:").format(plugin.device) descriptions = ["{label} ({init}, {transport})" .format(label=info.label or _("An unnamed {}").format(info.plugin_name), t@@ -594,8 +601,9 @@ class DeviceMgr(ThreadJob): if c is None: raise UserCancelled() info = infos[c] - # save new label + # save new label / soft_device_id keystore.set_label(info.label) + keystore.soft_device_id = info.soft_device_id wallet = handler.get_wallet() if wallet is not None: wallet.save_keystore() DIR diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py t@@ -167,7 +167,7 @@ class HW_PluginBase(BasePlugin): class HardwareClientBase: plugin: 'HW_PluginBase' - handler: Optional['HardwareHandlerBase'] + handler = None # type: Optional['HardwareHandlerBase'] def is_pairable(self) -> bool: raise NotImplementedError() t@@ -191,6 +191,16 @@ class HardwareClientBase: """ raise NotImplementedError() + def get_soft_device_id(self) -> Optional[str]: + """An id-like string that is used to distinguish devices programmatically. + This is a long term id for the device, that does not change between reconnects. + This method should not prompt the user, i.e. no user interaction, as it is used + during USB device enumeration (called for each unpaired device). + Stored in the wallet file. + """ + # This functionality is optional. If not implemented just return None: + return None + def has_usable_connection_with_device(self) -> bool: raise NotImplementedError() DIR diff --git a/electrum/plugins/keepkey/clientbase.py b/electrum/plugins/keepkey/clientbase.py t@@ -119,6 +119,9 @@ class KeepKeyClientBase(HardwareClientBase, GuiMixin, Logger): def label(self): return self.features.label + def get_soft_device_id(self): + return self.features.device_id + def is_initialized(self): return self.features.initialized DIR diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py t@@ -66,6 +66,7 @@ class Ledger_Client(HardwareClientBase): self.dongleObject = btchip(hidDevice) self.preflightDone = False self._is_hw1 = is_hw1 + self._soft_device_id = None def is_pairable(self): return True t@@ -82,6 +83,14 @@ class Ledger_Client(HardwareClientBase): def label(self): return "" + def get_soft_device_id(self): + if self._soft_device_id is None: + # modern ledger can provide xpub without user interaction + # (hw1 would prompt for PIN) + if not self.is_hw1(): + self._soft_device_id = self.request_root_fingerprint_from_device() + return self._soft_device_id + def is_hw1(self) -> bool: return self._is_hw1 t@@ -176,7 +185,8 @@ class Ledger_Client(HardwareClientBase): # Acquire the new client on the next run else: raise e - if self.has_detached_pin_support(self.dongleObject) and not self.is_pin_validated(self.dongleObject) and (self.handler is not None): + if self.has_detached_pin_support(self.dongleObject) and not self.is_pin_validated(self.dongleObject): + assert self.handler, "no handler for client" remaining_attempts = self.dongleObject.getVerifyPinRemainingAttempts() if remaining_attempts != 1: msg = "Enter your Ledger PIN - remaining attempts : " + str(remaining_attempts) DIR diff --git a/electrum/plugins/safe_t/clientbase.py b/electrum/plugins/safe_t/clientbase.py t@@ -121,6 +121,9 @@ class SafeTClientBase(HardwareClientBase, GuiMixin, Logger): def label(self): return self.features.label + def get_soft_device_id(self): + return self.features.device_id + def is_initialized(self): return self.features.initialized DIR diff --git a/electrum/plugins/trezor/clientbase.py b/electrum/plugins/trezor/clientbase.py t@@ -98,6 +98,9 @@ class TrezorClientBase(HardwareClientBase, Logger): def label(self): return self.features.label + def get_soft_device_id(self): + return self.features.device_id + def is_initialized(self): return self.features.initialized