commit 9d0bb295e6f55a2bff9f5b6770fa744c16af6e8a
parent 7dabbdd08234ae036177d056a513a2189e9f14e1
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:
8 files changed, 50 insertions(+), 6 deletions(-)
diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py
@@ -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:
@@ -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)
@@ -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)
diff --git a/electrum/keystore.py b/electrum/keystore.py
@@ -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)
@@ -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):
@@ -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...
diff --git a/electrum/plugin.py b/electrum/plugin.py
@@ -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):
@@ -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
@@ -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]
@@ -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),
@@ -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()
diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py
@@ -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()
@@ -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()
diff --git a/electrum/plugins/keepkey/clientbase.py b/electrum/plugins/keepkey/clientbase.py
@@ -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
diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py
@@ -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
@@ -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
@@ -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)
diff --git a/electrum/plugins/safe_t/clientbase.py b/electrum/plugins/safe_t/clientbase.py
@@ -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
diff --git a/electrum/plugins/trezor/clientbase.py b/electrum/plugins/trezor/clientbase.py
@@ -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