hw DeviceMgr: mostly switch away from xpubs for device pairing
- the DeviceMgr no longer uses xpubs to keep track of paired hw devices - instead, introduce keystore.pairing_code(), based on soft_device_id - xpubs are now only used in a single place when the actual pairing happens - motivation is to allow pairing a single device with multiple generic output script descriptors, not just a single account-level xpub - as a side-effect, we now allow pairing a device with multiple open windows simultaneously (if keystores have the same root fingerprint -- was already the case before if keystores had the same xpub)
This commit is contained in:
@@ -622,7 +622,7 @@ class BaseWizard(Logger):
|
|||||||
password = k.get_password_for_storage_encryption()
|
password = k.get_password_for_storage_encryption()
|
||||||
except UserCancelled:
|
except UserCancelled:
|
||||||
devmgr = self.plugins.device_manager
|
devmgr = self.plugins.device_manager
|
||||||
devmgr.unpair_xpub(k.xpub)
|
devmgr.unpair_pairing_code(k.pairing_code())
|
||||||
raise ChooseHwDeviceAgain()
|
raise ChooseHwDeviceAgain()
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
self.logger.exception('')
|
self.logger.exception('')
|
||||||
|
|||||||
@@ -915,6 +915,12 @@ class Hardware_KeyStore(Xpub, KeyStore):
|
|||||||
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
|
self.is_requesting_to_be_rewritten_to_wallet_file = True
|
||||||
|
|
||||||
|
def pairing_code(self) -> Optional[str]:
|
||||||
|
"""Used by the DeviceMgr to keep track of paired hw devices."""
|
||||||
|
if not self.soft_device_id:
|
||||||
|
return None
|
||||||
|
return f"{self.plugin.name}/{self.soft_device_id}"
|
||||||
|
|
||||||
|
|
||||||
KeyStoreWithMPK = Union[KeyStore, MasterPublicKeyMixin] # intersection really...
|
KeyStoreWithMPK = Union[KeyStore, MasterPublicKeyMixin] # intersection really...
|
||||||
AddressIndexGeneric = Union[Sequence[int], str] # can be hex pubkey str
|
AddressIndexGeneric = Union[Sequence[int], str] # can be hex pubkey str
|
||||||
|
|||||||
@@ -401,8 +401,8 @@ class DeviceMgr(ThreadJob):
|
|||||||
|
|
||||||
def __init__(self, config: SimpleConfig):
|
def __init__(self, config: SimpleConfig):
|
||||||
ThreadJob.__init__(self)
|
ThreadJob.__init__(self)
|
||||||
# An xpub->id_ map. Item only present if we have active pairing. Needs self.lock.
|
# A pairing_code->id_ map. Item only present if we have active pairing. Needs self.lock.
|
||||||
self.xpub_ids = {} # type: Dict[str, str]
|
self.pairing_code_to_id = {} # type: Dict[str, str]
|
||||||
# A client->id_ map. Needs self.lock.
|
# A client->id_ map. Needs self.lock.
|
||||||
self.clients = {} # type: Dict[HardwareClientBase, str]
|
self.clients = {} # type: Dict[HardwareClientBase, str]
|
||||||
# What we recognise. (vendor_id, product_id) -> Plugin
|
# What we recognise. (vendor_id, product_id) -> Plugin
|
||||||
@@ -454,28 +454,28 @@ class DeviceMgr(ThreadJob):
|
|||||||
self.clients[client] = device.id_
|
self.clients[client] = device.id_
|
||||||
return client
|
return client
|
||||||
|
|
||||||
def xpub_id(self, xpub):
|
def id_by_pairing_code(self, pairing_code):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
return self.xpub_ids.get(xpub)
|
return self.pairing_code_to_id.get(pairing_code)
|
||||||
|
|
||||||
def xpub_by_id(self, id_):
|
def pairing_code_by_id(self, id_):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
for xpub, xpub_id in self.xpub_ids.items():
|
for pairing_code, id2 in self.pairing_code_to_id.items():
|
||||||
if xpub_id == id_:
|
if id2 == id_:
|
||||||
return xpub
|
return pairing_code
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def unpair_xpub(self, xpub):
|
def unpair_pairing_code(self, pairing_code):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
if xpub not in self.xpub_ids:
|
if pairing_code not in self.pairing_code_to_id:
|
||||||
return
|
return
|
||||||
_id = self.xpub_ids.pop(xpub)
|
_id = self.pairing_code_to_id.pop(pairing_code)
|
||||||
self._close_client(_id)
|
self._close_client(_id)
|
||||||
|
|
||||||
def unpair_id(self, id_):
|
def unpair_id(self, id_):
|
||||||
xpub = self.xpub_by_id(id_)
|
pairing_code = self.pairing_code_by_id(id_)
|
||||||
if xpub:
|
if pairing_code:
|
||||||
self.unpair_xpub(xpub)
|
self.unpair_pairing_code(pairing_code)
|
||||||
else:
|
else:
|
||||||
self._close_client(id_)
|
self._close_client(id_)
|
||||||
|
|
||||||
@@ -486,10 +486,6 @@ class DeviceMgr(ThreadJob):
|
|||||||
if client:
|
if client:
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
def pair_xpub(self, xpub, id_):
|
|
||||||
with self.lock:
|
|
||||||
self.xpub_ids[xpub] = id_
|
|
||||||
|
|
||||||
def _client_by_id(self, id_) -> Optional['HardwareClientBase']:
|
def _client_by_id(self, id_) -> Optional['HardwareClientBase']:
|
||||||
with self.lock:
|
with self.lock:
|
||||||
for client, client_id in self.clients.items():
|
for client, client_id in self.clients.items():
|
||||||
@@ -517,10 +513,8 @@ class DeviceMgr(ThreadJob):
|
|||||||
handler.update_status(False)
|
handler.update_status(False)
|
||||||
if devices is None:
|
if devices is None:
|
||||||
devices = self.scan_devices()
|
devices = self.scan_devices()
|
||||||
xpub = keystore.xpub
|
client = self.client_by_pairing_code(
|
||||||
derivation = keystore.get_derivation_prefix()
|
plugin=plugin, pairing_code=keystore.pairing_code(), handler=handler, devices=devices)
|
||||||
assert derivation is not None
|
|
||||||
client = self.client_by_xpub(plugin, xpub, handler, devices)
|
|
||||||
if client is None and force_pair:
|
if client is None and force_pair:
|
||||||
try:
|
try:
|
||||||
info = self.select_device(plugin, handler, keystore, devices,
|
info = self.select_device(plugin, handler, keystore, devices,
|
||||||
@@ -528,7 +522,7 @@ class DeviceMgr(ThreadJob):
|
|||||||
except CannotAutoSelectDevice:
|
except CannotAutoSelectDevice:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
client = self.force_pair_xpub(plugin, handler, info, xpub, derivation)
|
client = self.force_pair_keystore(plugin=plugin, handler=handler, info=info, keystore=keystore)
|
||||||
if client:
|
if client:
|
||||||
handler.update_status(True)
|
handler.update_status(True)
|
||||||
# note: if select_device was called, we might also update label etc here:
|
# note: if select_device was called, we might also update label etc here:
|
||||||
@@ -536,9 +530,11 @@ class DeviceMgr(ThreadJob):
|
|||||||
self.logger.info("end client for keystore")
|
self.logger.info("end client for keystore")
|
||||||
return client
|
return client
|
||||||
|
|
||||||
def client_by_xpub(self, plugin: 'HW_PluginBase', xpub, handler: 'HardwareHandlerBase',
|
def client_by_pairing_code(
|
||||||
devices: Sequence['Device']) -> Optional['HardwareClientBase']:
|
self, *, plugin: 'HW_PluginBase', pairing_code: str, handler: 'HardwareHandlerBase',
|
||||||
_id = self.xpub_id(xpub)
|
devices: Sequence['Device'],
|
||||||
|
) -> Optional['HardwareClientBase']:
|
||||||
|
_id = self.id_by_pairing_code(pairing_code)
|
||||||
client = self._client_by_id(_id)
|
client = self._client_by_id(_id)
|
||||||
if client:
|
if client:
|
||||||
if type(client.plugin) != type(plugin):
|
if type(client.plugin) != type(plugin):
|
||||||
@@ -552,10 +548,17 @@ class DeviceMgr(ThreadJob):
|
|||||||
if device.id_ == _id:
|
if device.id_ == _id:
|
||||||
return self.create_client(device, handler, plugin)
|
return self.create_client(device, handler, plugin)
|
||||||
|
|
||||||
def force_pair_xpub(self, plugin: 'HW_PluginBase', handler: 'HardwareHandlerBase',
|
def force_pair_keystore(
|
||||||
info: 'DeviceInfo', xpub, derivation) -> Optional['HardwareClientBase']:
|
self,
|
||||||
# The wallet has not been previously paired, so let the user
|
*,
|
||||||
# choose an unpaired device and compare its first address.
|
plugin: 'HW_PluginBase',
|
||||||
|
handler: 'HardwareHandlerBase',
|
||||||
|
info: 'DeviceInfo',
|
||||||
|
keystore: 'Hardware_KeyStore',
|
||||||
|
) -> 'HardwareClientBase':
|
||||||
|
xpub = keystore.xpub
|
||||||
|
derivation = keystore.get_derivation_prefix()
|
||||||
|
assert derivation is not None
|
||||||
xtype = bip32.xpub_type(xpub)
|
xtype = bip32.xpub_type(xpub)
|
||||||
client = self._client_by_id(info.device.id_)
|
client = self._client_by_id(info.device.id_)
|
||||||
if client and client.is_pairable() and type(client.plugin) == type(plugin):
|
if client and client.is_pairable() and type(client.plugin) == type(plugin):
|
||||||
@@ -568,7 +571,9 @@ class DeviceMgr(ThreadJob):
|
|||||||
# Bad / cancelled PIN / passphrase
|
# Bad / cancelled PIN / passphrase
|
||||||
client_xpub = None
|
client_xpub = None
|
||||||
if client_xpub == xpub:
|
if client_xpub == xpub:
|
||||||
self.pair_xpub(xpub, info.device.id_)
|
keystore.opportunistically_fill_in_missing_info_from_device(client)
|
||||||
|
with self.lock:
|
||||||
|
self.pairing_code_to_id[keystore.pairing_code()] = info.device.id_
|
||||||
return client
|
return client
|
||||||
|
|
||||||
# The user input has wrong PIN or passphrase, or cancelled input,
|
# The user input has wrong PIN or passphrase, or cancelled input,
|
||||||
@@ -590,7 +595,7 @@ class DeviceMgr(ThreadJob):
|
|||||||
raise HardwarePluginLibraryUnavailable(message)
|
raise HardwarePluginLibraryUnavailable(message)
|
||||||
if devices is None:
|
if devices is None:
|
||||||
devices = self.scan_devices()
|
devices = self.scan_devices()
|
||||||
devices = [dev for dev in devices if not self.xpub_by_id(dev.id_)]
|
devices = [dev for dev in devices if not self.pairing_code_by_id(dev.id_)]
|
||||||
infos = []
|
infos = []
|
||||||
for device in devices:
|
for device in devices:
|
||||||
if not plugin.can_recognize_device(device):
|
if not plugin.can_recognize_device(device):
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ class HW_PluginBase(BasePlugin):
|
|||||||
def close_wallet(self, wallet: 'Abstract_Wallet'):
|
def close_wallet(self, wallet: 'Abstract_Wallet'):
|
||||||
for keystore in wallet.get_keystores():
|
for keystore in wallet.get_keystores():
|
||||||
if isinstance(keystore, self.keystore_class):
|
if isinstance(keystore, self.keystore_class):
|
||||||
self.device_manager().unpair_xpub(keystore.xpub)
|
self.device_manager().unpair_pairing_code(keystore.pairing_code())
|
||||||
if keystore.thread:
|
if keystore.thread:
|
||||||
keystore.thread.stop()
|
keystore.thread.stop()
|
||||||
|
|
||||||
@@ -248,8 +248,8 @@ class HardwareClientBase:
|
|||||||
during USB device enumeration (called for each unpaired device).
|
during USB device enumeration (called for each unpaired device).
|
||||||
Stored in the wallet file.
|
Stored in the wallet file.
|
||||||
"""
|
"""
|
||||||
# This functionality is optional. If not implemented just return None:
|
root_fp = self.request_root_fingerprint_from_device()
|
||||||
return None
|
return root_fp
|
||||||
|
|
||||||
def has_usable_connection_with_device(self) -> bool:
|
def has_usable_connection_with_device(self) -> bool:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ class QtPluginBase(object):
|
|||||||
'''This dialog box should be usable even if the user has
|
'''This dialog box should be usable even if the user has
|
||||||
forgotten their PIN or it is in bootloader mode.'''
|
forgotten their PIN or it is in bootloader mode.'''
|
||||||
assert window.gui_thread != threading.current_thread(), 'must not be called from GUI thread'
|
assert window.gui_thread != threading.current_thread(), 'must not be called from GUI thread'
|
||||||
device_id = self.device_manager().xpub_id(keystore.xpub)
|
device_id = self.device_manager().id_by_pairing_code(keystore.pairing_code())
|
||||||
if not device_id:
|
if not device_id:
|
||||||
try:
|
try:
|
||||||
info = self.device_manager().select_device(self, keystore.handler, keystore)
|
info = self.device_manager().select_device(self, keystore.handler, keystore)
|
||||||
|
|||||||
Reference in New Issue
Block a user