Browse Source

ledger: re-add support for HW.1, and add a deprecation warning

patch-4
SomberNight 2 years ago
parent
commit
9b82eb6d06
No known key found for this signature in database GPG Key ID: B33B5F232C6271E9
  1. 1
      contrib/requirements/requirements-hw.txt
  2. 163
      electrum/plugins/ledger/ledger.py

1
contrib/requirements/requirements-hw.txt

@ -6,6 +6,7 @@ hidapi<0.11
trezor[hidapi]>=0.13.0,<0.14 trezor[hidapi]>=0.13.0,<0.14
safet>=0.1.5 safet>=0.1.5
keepkey>=6.3.1 keepkey>=6.3.1
btchip-python>=0.1.32
ledger-bitcoin>=0.1.1,<0.2.0 ledger-bitcoin>=0.1.1,<0.2.0
ckcc-protocol>=0.7.7 ckcc-protocol>=0.7.7
bitbox02>=6.0.0 bitbox02>=6.0.0

163
electrum/plugins/ledger/ledger.py

@ -35,12 +35,13 @@ try:
from ledgercomm.interfaces.hid_device import HID from ledgercomm.interfaces.hid_device import HID
# legacy imports # legacy imports
# note: we could replace "btchip" with "ledger_bitcoin.btchip" but the latter does not support HW.1
import hid import hid
from ledger_bitcoin.btchip.btchipComm import HIDDongleHIDAPI from btchip.btchipComm import HIDDongleHIDAPI
from ledger_bitcoin.btchip.btchip import btchip from btchip.btchip import btchip
from ledger_bitcoin.btchip.btchipUtils import compress_public_key from btchip.btchipUtils import compress_public_key
from ledger_bitcoin.btchip.bitcoinTransaction import bitcoinTransaction from btchip.bitcoinTransaction import bitcoinTransaction
from ledger_bitcoin.btchip.btchipException import BTChipException from btchip.btchipException import BTChipException
LEDGER_BITCOIN = True LEDGER_BITCOIN = True
except ImportError as e: except ImportError as e:
@ -298,21 +299,26 @@ def get_chain() -> 'Chain':
raise ValueError("Unsupported network") raise ValueError("Unsupported network")
# Metaclass, concretely instantiated in Ledger_Client_Legacy and Ledger_Client_New
class Ledger_Client(HardwareClientBase, ABC): class Ledger_Client(HardwareClientBase, ABC):
is_legacy: bool is_legacy: bool
def __new__(cls, hidDevice, *args, **kwargs): @staticmethod
transport = ledger_bitcoin.TransportClient('hid', hid=hidDevice) def construct_new(*args, device: Device, **kwargs) -> 'Ledger_Client':
"""The 'real' constructor, that automatically decides which subclass to use."""
if LedgerPlugin.is_hw1(device.product_key):
return Ledger_Client_Legacy_HW1(*args, **kwargs, device=device)
# for nano S or newer hw, decide which client impl to use based on software/firmware version:
hid_device = HID()
hid_device.path = device.path
hid_device.open()
transport = ledger_bitcoin.TransportClient('hid', hid=hid_device)
cl = ledger_bitcoin.createClient(transport, chain=get_chain()) cl = ledger_bitcoin.createClient(transport, chain=get_chain())
if isinstance(cl, ledger_bitcoin.client.NewClient): if isinstance(cl, ledger_bitcoin.client.NewClient):
return super().__new__(Ledger_Client_New) return Ledger_Client_New(hid_device, *args, **kwargs)
else: else:
return super().__new__(Ledger_Client_Legacy) return Ledger_Client_Legacy(hid_device, *args, **kwargs)
def __init__(self, hidDevice, *, product_key: Tuple[int, int], def __init__(self, *, plugin: HW_PluginBase):
plugin: HW_PluginBase):
HardwareClientBase.__init__(self, plugin=plugin) HardwareClientBase.__init__(self, plugin=plugin)
def get_master_fingerprint(self) -> bytes: def get_master_fingerprint(self) -> bytes:
@ -342,9 +348,9 @@ class Ledger_Client_Legacy(Ledger_Client):
"""Client based on the bitchip library, targeting versions 2.0.* and below.""" """Client based on the bitchip library, targeting versions 2.0.* and below."""
is_legacy = True is_legacy = True
def __init__(self, hidDevice, *, product_key: Tuple[int, int], def __init__(self, hidDevice: 'HID', *, product_key: Tuple[int, int],
plugin: HW_PluginBase): plugin: HW_PluginBase):
Ledger_Client.__init__(self, hidDevice, product_key=product_key, plugin=plugin) Ledger_Client.__init__(self, plugin=plugin)
# Hack, we close the old object and instantiate a new one # Hack, we close the old object and instantiate a new one
hidDevice.close() hidDevice.close()
@ -396,7 +402,7 @@ class Ledger_Client_Legacy(Ledger_Client):
return self._soft_device_id return self._soft_device_id
def is_hw1(self) -> bool: def is_hw1(self) -> bool:
return self._product_key[0] == 0x2581 return LedgerPlugin.is_hw1(self._product_key)
def device_model_name(self): def device_model_name(self):
return LedgerPlugin.device_name_from_product_key(self._product_key) return LedgerPlugin.device_name_from_product_key(self._product_key)
@ -655,6 +661,8 @@ class Ledger_Client_Legacy(Ledger_Client):
firstTransaction = True firstTransaction = True
inputIndex = 0 inputIndex = 0
rawTx = tx.serialize_to_network() rawTx = tx.serialize_to_network()
if self.is_hw1():
self.dongleObject.enableAlternate2fa(False)
if segwitTransaction: if segwitTransaction:
self.dongleObject.startUntrustedTransaction(True, inputIndex, chipInputs, redeemScripts[inputIndex], version=tx.version) self.dongleObject.startUntrustedTransaction(True, inputIndex, chipInputs, redeemScripts[inputIndex], version=tx.version)
# we don't set meaningful outputAddress, amount and fees # we don't set meaningful outputAddress, amount and fees
@ -785,14 +793,97 @@ class Ledger_Client_Legacy(Ledger_Client):
return bytes([27 + 4 + (signature[0] & 0x01)]) + r_padded + s_padded return bytes([27 + 4 + (signature[0] & 0x01)]) + r_padded + s_padded
class Ledger_Client_Legacy_HW1(Ledger_Client_Legacy):
"""Even "legacy-er" client for deprecated HW.1 support."""
MIN_SUPPORTED_HW1_FW_VERSION = "1.0.2"
def __init__(self, product_key: Tuple[int, int],
plugin: HW_PluginBase, device: 'Device'):
# note: Ledger_Client_Legacy.__init__ is *not* called
Ledger_Client.__init__(self, plugin=plugin)
self._product_key = product_key
assert self.is_hw1()
ledger = device.product_key[1] in (0x3b7c, 0x4b7c)
dev = hid.device()
dev.open_path(device.path)
dev.set_nonblocking(True)
hid_device = HIDDongleHIDAPI(dev, ledger, debug=False)
self.dongleObject = btchip(hid_device)
self._preflightDone = False
self.signing = False
self._soft_device_id = None
@runs_in_hwd_thread
def checkDevice(self):
super().checkDevice()
self._perform_hw1_preflight()
def _perform_hw1_preflight(self):
assert self.is_hw1()
if self._preflightDone:
return
try:
firmwareInfo = self.dongleObject.getFirmwareVersion()
firmware = firmwareInfo['version']
if versiontuple(firmware) < versiontuple(self.MIN_SUPPORTED_HW1_FW_VERSION):
self.close()
raise UserFacingException(
_("Unsupported device firmware (too old).") + f"\nInstalled: {firmware}. Needed: >={self.MIN_SUPPORTED_HW1_FW_VERSION}")
try:
self.dongleObject.getOperationMode()
except BTChipException as e:
if (e.sw == 0x6985):
self.close()
self.handler.get_setup()
# 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):
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)
else:
msg = "Enter your Ledger PIN - WARNING : LAST ATTEMPT. If the PIN is not correct, the dongle will be wiped."
confirmed, p, pin = self.password_dialog(msg)
if not confirmed:
raise UserFacingException('Aborted by user - please unplug the dongle and plug it again before retrying')
pin = pin.encode()
self.dongleObject.verifyPin(pin)
except BTChipException as e:
if (e.sw == 0x6faa):
raise UserFacingException("Dongle is temporarily locked - please unplug it and replug it again")
if ((e.sw & 0xFFF0) == 0x63c0):
raise UserFacingException("Invalid PIN - please unplug the dongle and plug it again before retrying")
if e.sw == 0x6f00 and e.message == 'Invalid channel':
# based on docs 0x6f00 might be a more general error, hence we also compare message to be sure
raise UserFacingException("Invalid channel.\n"
"Please make sure that 'Browser support' is disabled on your device.")
if e.sw == 0x6d00 or e.sw == 0x6700:
raise UserFacingException(_("Device not in Bitcoin mode")) from e
raise e
else:
deprecation_warning = (
"This Ledger device (HW.1) is being deprecated.\n\nIt is no longer supported by Ledger.\n"
"Future versions of Electrum will no longer be compatible with it.\n\n"
"You should move your coins and migrate to a modern hardware device.")
_logger.warning(deprecation_warning.replace("\n", " "))
if self.handler:
self.handler.show_message(deprecation_warning)
self._preflightDone = True
class Ledger_Client_New(Ledger_Client): class Ledger_Client_New(Ledger_Client):
"""Client based on the ledger_bitcoin library, targeting versions 2.1.* and above.""" """Client based on the ledger_bitcoin library, targeting versions 2.1.* and above."""
is_legacy = False is_legacy = False
def __init__(self, hidDevice, *, product_key: Tuple[int, int], def __init__(self, hidDevice: 'HID', *, product_key: Tuple[int, int],
plugin: HW_PluginBase): plugin: HW_PluginBase):
Ledger_Client.__init__(self, hidDevice, product_key=product_key, plugin=plugin) Ledger_Client.__init__(self, plugin=plugin)
transport = ledger_bitcoin.TransportClient('hid', hid=hidDevice) transport = ledger_bitcoin.TransportClient('hid', hid=hidDevice)
self.client = ledger_bitcoin.client.NewClient(transport, get_chain()) self.client = ledger_bitcoin.client.NewClient(transport, get_chain())
@ -1276,12 +1367,16 @@ class LedgerPlugin(HW_PluginBase):
else: else:
raise LibraryFoundButUnusable(library_version=version) raise LibraryFoundButUnusable(library_version=version)
@classmethod
def is_hw1(cls, product_key) -> bool:
return product_key[0] == 0x2581
@classmethod @classmethod
def _recognize_device(cls, product_key) -> Tuple[bool, Optional[str]]: def _recognize_device(cls, product_key) -> Tuple[bool, Optional[str]]:
"""Returns (can_recognize, model_name) tuple.""" """Returns (can_recognize, model_name) tuple."""
# legacy product_keys # legacy product_keys
if product_key in cls.DEVICE_IDS: if product_key in cls.DEVICE_IDS:
if product_key[0] == 0x2581: if cls.is_hw1(product_key):
return True, "Ledger HW.1" return True, "Ledger HW.1"
if product_key == (0x2c97, 0x0000): if product_key == (0x2c97, 0x0000):
return True, "Ledger Blue" return True, "Ledger Blue"
@ -1315,34 +1410,12 @@ class LedgerPlugin(HW_PluginBase):
return None return None
return device return device
@runs_in_hwd_thread
def get_btchip_device(self, device: Device) -> Optional['HID']:
# TODO: refactor
ledger = False
if device.product_key[0] == 0x2581 and device.product_key[1] == 0x3b7c:
ledger = True
if device.product_key[0] == 0x2581 and device.product_key[1] == 0x4b7c:
ledger = True
if device.product_key[0] == 0x2c97:
if device.interface_number == 0 or device.usage_page == 0xffa0:
ledger = True
else:
return None # non-compatible interface of a Nano S or Blue
btchip_device = HID()
btchip_device.path = device.path
btchip_device.open()
return btchip_device
@runs_in_hwd_thread @runs_in_hwd_thread
def create_client(self, device, handler) -> Optional[Ledger_Client]: def create_client(self, device, handler) -> Optional[Ledger_Client]:
hid_device = self.get_btchip_device(device) try:
if hid_device is not None: return Ledger_Client.construct_new(device=device, product_key=device.product_key, plugin=self)
try: except Exception as e:
return Ledger_Client(hid_device, product_key=device.product_key, plugin=self) self.logger.info(f"cannot connect at {device.path} {e}")
except Exception as e:
self.logger.info(f"cannot connect at {device.path} {e}")
return None return None
def setup_device(self, device_info, wizard, purpose): def setup_device(self, device_info, wizard, purpose):

Loading…
Cancel
Save