ledger: re-add support for HW.1, and add a deprecation warning
This commit is contained in:
@@ -6,6 +6,7 @@ hidapi<0.11
|
||||
trezor[hidapi]>=0.13.0,<0.14
|
||||
safet>=0.1.5
|
||||
keepkey>=6.3.1
|
||||
btchip-python>=0.1.32
|
||||
ledger-bitcoin>=0.1.1,<0.2.0
|
||||
ckcc-protocol>=0.7.7
|
||||
bitbox02>=6.0.0
|
||||
|
||||
@@ -35,12 +35,13 @@ try:
|
||||
from ledgercomm.interfaces.hid_device import HID
|
||||
|
||||
# legacy imports
|
||||
# note: we could replace "btchip" with "ledger_bitcoin.btchip" but the latter does not support HW.1
|
||||
import hid
|
||||
from ledger_bitcoin.btchip.btchipComm import HIDDongleHIDAPI
|
||||
from ledger_bitcoin.btchip.btchip import btchip
|
||||
from ledger_bitcoin.btchip.btchipUtils import compress_public_key
|
||||
from ledger_bitcoin.btchip.bitcoinTransaction import bitcoinTransaction
|
||||
from ledger_bitcoin.btchip.btchipException import BTChipException
|
||||
from btchip.btchipComm import HIDDongleHIDAPI
|
||||
from btchip.btchip import btchip
|
||||
from btchip.btchipUtils import compress_public_key
|
||||
from btchip.bitcoinTransaction import bitcoinTransaction
|
||||
from btchip.btchipException import BTChipException
|
||||
|
||||
LEDGER_BITCOIN = True
|
||||
except ImportError as e:
|
||||
@@ -298,21 +299,26 @@ def get_chain() -> 'Chain':
|
||||
raise ValueError("Unsupported network")
|
||||
|
||||
|
||||
# Metaclass, concretely instantiated in Ledger_Client_Legacy and Ledger_Client_New
|
||||
class Ledger_Client(HardwareClientBase, ABC):
|
||||
is_legacy: bool
|
||||
|
||||
def __new__(cls, hidDevice, *args, **kwargs):
|
||||
transport = ledger_bitcoin.TransportClient('hid', hid=hidDevice)
|
||||
@staticmethod
|
||||
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())
|
||||
|
||||
if isinstance(cl, ledger_bitcoin.client.NewClient):
|
||||
return super().__new__(Ledger_Client_New)
|
||||
return Ledger_Client_New(hid_device, *args, **kwargs)
|
||||
else:
|
||||
return super().__new__(Ledger_Client_Legacy)
|
||||
return Ledger_Client_Legacy(hid_device, *args, **kwargs)
|
||||
|
||||
def __init__(self, hidDevice, *, product_key: Tuple[int, int],
|
||||
plugin: HW_PluginBase):
|
||||
def __init__(self, *, plugin: HW_PluginBase):
|
||||
HardwareClientBase.__init__(self, plugin=plugin)
|
||||
|
||||
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."""
|
||||
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):
|
||||
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
|
||||
hidDevice.close()
|
||||
@@ -396,7 +402,7 @@ class Ledger_Client_Legacy(Ledger_Client):
|
||||
return self._soft_device_id
|
||||
|
||||
def is_hw1(self) -> bool:
|
||||
return self._product_key[0] == 0x2581
|
||||
return LedgerPlugin.is_hw1(self._product_key)
|
||||
|
||||
def device_model_name(self):
|
||||
return LedgerPlugin.device_name_from_product_key(self._product_key)
|
||||
@@ -655,6 +661,8 @@ class Ledger_Client_Legacy(Ledger_Client):
|
||||
firstTransaction = True
|
||||
inputIndex = 0
|
||||
rawTx = tx.serialize_to_network()
|
||||
if self.is_hw1():
|
||||
self.dongleObject.enableAlternate2fa(False)
|
||||
if segwitTransaction:
|
||||
self.dongleObject.startUntrustedTransaction(True, inputIndex, chipInputs, redeemScripts[inputIndex], version=tx.version)
|
||||
# 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
|
||||
|
||||
|
||||
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):
|
||||
"""Client based on the ledger_bitcoin library, targeting versions 2.1.* and above."""
|
||||
|
||||
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):
|
||||
Ledger_Client.__init__(self, hidDevice, product_key=product_key, plugin=plugin)
|
||||
Ledger_Client.__init__(self, plugin=plugin)
|
||||
|
||||
transport = ledger_bitcoin.TransportClient('hid', hid=hidDevice)
|
||||
self.client = ledger_bitcoin.client.NewClient(transport, get_chain())
|
||||
@@ -1276,12 +1367,16 @@ class LedgerPlugin(HW_PluginBase):
|
||||
else:
|
||||
raise LibraryFoundButUnusable(library_version=version)
|
||||
|
||||
@classmethod
|
||||
def is_hw1(cls, product_key) -> bool:
|
||||
return product_key[0] == 0x2581
|
||||
|
||||
@classmethod
|
||||
def _recognize_device(cls, product_key) -> Tuple[bool, Optional[str]]:
|
||||
"""Returns (can_recognize, model_name) tuple."""
|
||||
# legacy product_keys
|
||||
if product_key in cls.DEVICE_IDS:
|
||||
if product_key[0] == 0x2581:
|
||||
if cls.is_hw1(product_key):
|
||||
return True, "Ledger HW.1"
|
||||
if product_key == (0x2c97, 0x0000):
|
||||
return True, "Ledger Blue"
|
||||
@@ -1315,34 +1410,12 @@ class LedgerPlugin(HW_PluginBase):
|
||||
return None
|
||||
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
|
||||
def create_client(self, device, handler) -> Optional[Ledger_Client]:
|
||||
hid_device = self.get_btchip_device(device)
|
||||
if hid_device is not None:
|
||||
try:
|
||||
return Ledger_Client(hid_device, product_key=device.product_key, plugin=self)
|
||||
except Exception as e:
|
||||
self.logger.info(f"cannot connect at {device.path} {e}")
|
||||
try:
|
||||
return Ledger_Client.construct_new(device=device, product_key=device.product_key, plugin=self)
|
||||
except Exception as e:
|
||||
self.logger.info(f"cannot connect at {device.path} {e}")
|
||||
return None
|
||||
|
||||
def setup_device(self, device_info, wizard, purpose):
|
||||
|
||||
Reference in New Issue
Block a user