diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec
index e7fb55270..f03b932b2 100644
--- a/contrib/build-wine/deterministic.spec
+++ b/contrib/build-wine/deterministic.spec
@@ -41,7 +41,6 @@ datas = [
datas += collect_data_files(f"{PYPKG}.plugins")
datas += collect_data_files('trezorlib') # TODO is this needed? and same question for other hww libs
datas += collect_data_files('safetlib')
-datas += collect_data_files('btchip')
datas += collect_data_files('ckcc')
datas += collect_data_files('bitbox02')
diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt
index 0dbef4fff..ffcc26784 100644
--- a/contrib/deterministic-build/requirements-hw.txt
+++ b/contrib/deterministic-build/requirements-hw.txt
@@ -4,8 +4,6 @@ base58==2.1.1 \
bitbox02==6.2.0 \
--hash=sha256:5a8290bd270468ccdf2e6ff7174d25ea2b2f191e19734a79aa573c2b982c266f \
--hash=sha256:cede06e399c98ed536fed6d8a421208daa00f97b697bd8363a941ac5f33309bf
-btchip-python==0.1.32 \
- --hash=sha256:34f5e0c161c08f65dc0d070ba2ff4c315ed21c4b7e0faa32a46862d0dc1b8f55
cbor2==5.4.6 \
--hash=sha256:b893500db0fe033e570c3adc956af6eefc57e280026bd2d86fd53da9f1e594d7
certifi==2024.2.2 \
diff --git a/contrib/osx/osx.spec b/contrib/osx/osx.spec
index 0d57467cb..e6501b395 100644
--- a/contrib/osx/osx.spec
+++ b/contrib/osx/osx.spec
@@ -44,7 +44,6 @@ datas = [
datas += collect_data_files(f"{PYPKG}.plugins")
datas += collect_data_files('trezorlib') # TODO is this needed? and same question for other hww libs
datas += collect_data_files('safetlib')
-datas += collect_data_files('btchip')
datas += collect_data_files('ckcc')
datas += collect_data_files('bitbox02')
diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt
index e552142a0..6f8b9dfb8 100644
--- a/contrib/requirements/requirements-hw.txt
+++ b/contrib/requirements/requirements-hw.txt
@@ -14,8 +14,6 @@ hidapi>=0.7.99.post15
libusb1>=1.6
# device plugin: ledger
-# note: btchip-python only needed for "legacy" protocol and HW.1 support
-btchip-python>=0.1.32
ledger-bitcoin>=0.2.0
hidapi
diff --git a/electrum/plugins/ledger/auth2fa.py b/electrum/plugins/ledger/auth2fa.py
deleted file mode 100644
index 9579116e7..000000000
--- a/electrum/plugins/ledger/auth2fa.py
+++ /dev/null
@@ -1,168 +0,0 @@
-import copy
-from typing import TYPE_CHECKING
-
-from PyQt6.QtWidgets import (QDialog, QLineEdit, QTextEdit, QVBoxLayout, QLabel,
- QWidget, QHBoxLayout, QComboBox)
-
-from btchip.btchip import BTChipException
-
-from electrum.gui.qt.util import PasswordLineEdit
-
-from electrum.i18n import _
-from electrum import constants, bitcoin
-from electrum.logging import get_logger
-
-if TYPE_CHECKING:
- from .ledger import Ledger_Client
-
-
-_logger = get_logger(__name__)
-
-
-DEBUG = False
-
-helpTxt = [_("Your Ledger Wallet wants to tell you a one-time PIN code.
" \
- "For best security you should unplug your device, open a text editor on another computer, " \
- "put your cursor into it, and plug your device into that computer. " \
- "It will output a summary of the transaction being signed and a one-time PIN.
" \
- "Verify the transaction summary and type the PIN code here.
" \
- "Before pressing enter, plug the device back into this computer.
"),
- _("Verify the address below.
Type the character from your security card corresponding to the BOLD character."),
- ]
-
-class LedgerAuthDialog(QDialog):
- def __init__(self, handler, data, *, client: 'Ledger_Client'):
- '''Ask user for 2nd factor authentication. Support text and security card methods.
- Use last method from settings, but support downgrade.
- '''
- QDialog.__init__(self, handler.top_level_window())
- self.handler = handler
- self.txdata = data
- self.idxs = self.txdata['keycardData'] if self.txdata['confirmationType'] > 1 else ''
- self.setMinimumWidth(650)
- self.setWindowTitle(_("Ledger Wallet Authentication"))
- self.cfg = copy.deepcopy(self.handler.win.wallet.get_keystore().cfg)
- self.dongle = client.dongleObject.dongle
- self.pin = ''
-
- self.devmode = self.getDevice2FAMode()
- if self.devmode == 0x11 or self.txdata['confirmationType'] == 1:
- self.cfg['mode'] = 0
-
- vbox = QVBoxLayout()
- self.setLayout(vbox)
-
- def on_change_mode(idx):
- self.cfg['mode'] = 0 if self.devmode == 0x11 else idx if idx > 0 else 1
- if self.cfg['mode'] > 0:
- self.handler.win.wallet.get_keystore().cfg = self.cfg
- self.handler.win.wallet.save_keystore()
- self.update_dlg()
- def return_pin():
- self.pin = self.pintxt.text() if self.txdata['confirmationType'] == 1 else self.cardtxt.text()
- if self.cfg['mode'] == 1:
- self.pin = ''.join(chr(int(str(i),16)) for i in self.pin)
- self.accept()
-
- self.modebox = QWidget()
- modelayout = QHBoxLayout()
- self.modebox.setLayout(modelayout)
- modelayout.addWidget(QLabel(_("Method:")))
- self.modes = QComboBox()
- modelayout.addWidget(self.modes, 2)
- modelayout.addStretch(1)
- self.modebox.setMaximumHeight(50)
- vbox.addWidget(self.modebox)
-
- self.populate_modes()
- self.modes.currentIndexChanged.connect(on_change_mode)
-
- self.helpmsg = QTextEdit()
- self.helpmsg.setStyleSheet("QTextEdit { color:black; background-color: lightgray; }")
- self.helpmsg.setReadOnly(True)
- vbox.addWidget(self.helpmsg)
-
- self.pinbox = QWidget()
- pinlayout = QHBoxLayout()
- self.pinbox.setLayout(pinlayout)
- self.pintxt = PasswordLineEdit()
- self.pintxt.setMaxLength(4)
- self.pintxt.returnPressed.connect(return_pin)
- pinlayout.addWidget(QLabel(_("Enter PIN:")))
- pinlayout.addWidget(self.pintxt)
- pinlayout.addWidget(QLabel(_("NOT DEVICE PIN - see above")))
- pinlayout.addStretch(1)
- self.pinbox.setVisible(self.cfg['mode'] == 0)
- vbox.addWidget(self.pinbox)
-
- self.cardbox = QWidget()
- card = QVBoxLayout()
- self.cardbox.setLayout(card)
- self.addrtext = QTextEdit()
- self.addrtext.setStyleSheet('''
- QTextEdit {
- color:blue; background-color:lightgray; padding:15px 10px; border:none;
- font-size:20pt; font-family: "Courier New", monospace; }
- ''')
- self.addrtext.setReadOnly(True)
- self.addrtext.setMaximumHeight(130)
- card.addWidget(self.addrtext)
-
- def pin_changed(s):
- if len(s) < len(self.idxs):
- i = self.idxs[len(s)]
- addr = self.txdata['address']
- if not constants.net.TESTNET:
- text = addr[:i] + '' + addr[i:i+1] + '' + addr[i+1:]
- else:
- # pin needs to be created from mainnet address
- addr_mainnet = bitcoin.script_to_address(bitcoin.address_to_script(addr), net=constants.BitcoinMainnet)
- addr_mainnet = addr_mainnet[:i] + '' + addr_mainnet[i:i+1] + '' + addr_mainnet[i+1:]
- text = str(addr) + '\n' + str(addr_mainnet)
- self.addrtext.setHtml(str(text))
- else:
- self.addrtext.setHtml(_("Press Enter"))
-
- pin_changed('')
- cardpin = QHBoxLayout()
- cardpin.addWidget(QLabel(_("Enter PIN:")))
- self.cardtxt = PasswordLineEdit()
- self.cardtxt.setMaxLength(len(self.idxs))
- self.cardtxt.textChanged.connect(pin_changed)
- self.cardtxt.returnPressed.connect(return_pin)
- cardpin.addWidget(self.cardtxt)
- cardpin.addWidget(QLabel(_("NOT DEVICE PIN - see above")))
- cardpin.addStretch(1)
- card.addLayout(cardpin)
- self.cardbox.setVisible(self.cfg['mode'] == 1)
- vbox.addWidget(self.cardbox)
-
- self.update_dlg()
-
- def populate_modes(self):
- self.modes.blockSignals(True)
- self.modes.clear()
- self.modes.addItem(_("Summary Text PIN (requires dongle replugging)") if self.txdata['confirmationType'] == 1 else _("Summary Text PIN is Disabled"))
- if self.txdata['confirmationType'] > 1:
- self.modes.addItem(_("Security Card Challenge"))
- self.modes.blockSignals(False)
-
- def update_dlg(self):
- self.modes.setCurrentIndex(self.cfg['mode'])
- self.modebox.setVisible(True)
- self.helpmsg.setText(helpTxt[self.cfg['mode']])
- self.helpmsg.setMinimumHeight(180 if self.txdata['confirmationType'] == 1 else 100)
- self.helpmsg.setVisible(True)
- self.pinbox.setVisible(self.cfg['mode'] == 0)
- self.cardbox.setVisible(self.cfg['mode'] == 1)
- self.pintxt.setFocus() if self.cfg['mode'] == 0 else self.cardtxt.setFocus()
- self.setMaximumHeight(400)
-
- def getDevice2FAMode(self):
- apdu = [0xe0, 0x24, 0x01, 0x00, 0x00, 0x01] # get 2fa mode
- try:
- mode = self.dongle.exchange(bytearray(apdu))
- return mode
- except BTChipException as e:
- _logger.debug('Device getMode Failed')
- return 0x11
diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py
index 53524ee53..74ab74596 100644
--- a/electrum/plugins/ledger/ledger.py
+++ b/electrum/plugins/ledger/ledger.py
@@ -39,13 +39,12 @@ 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 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
+ 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
LEDGER_BITCOIN = True
except ImportError as e:
@@ -310,7 +309,7 @@ class Ledger_Client(HardwareClientBase, ABC):
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)
+ raise Exception("ledger hw.1 devices are no longer supported")
# 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
@@ -328,8 +327,10 @@ class Ledger_Client(HardwareClientBase, ABC):
_logger.info(f"ledger_bitcoin.createClient() got exc: {e}. falling back to old plugin.")
cl = None
if isinstance(cl, ledger_bitcoin.client.NewClient):
+ _logger.debug(f"Ledger_Client.construct_new(). creating NewClient for {device=}.")
return Ledger_Client_New(hid_device, *args, **kwargs)
else:
+ _logger.debug(f"Ledger_Client.construct_new(). creating LegacyClient for {device=}.")
return Ledger_Client_Legacy(hid_device, *args, **kwargs)
def __init__(self, *, plugin: HW_PluginBase):
@@ -552,7 +553,6 @@ class Ledger_Client_Legacy(Ledger_Client):
chipInputs = []
redeemScripts = []
changePath = ""
- output = None
p2shTransaction = False
segwitTransaction = False
pin = ""
@@ -606,30 +606,16 @@ class Ledger_Client_Legacy(Ledger_Client):
if not is_txin_legacy_multisig(txin):
self.give_error("P2SH / regular input mixed in same transaction not supported") # should never happen
- txOutput = bytearray()
- txOutput += var_int(len(tx.outputs()))
- for o in tx.outputs():
- txOutput += int.to_bytes(o.value, length=8, byteorder="little", signed=False)
- script = o.scriptpubkey
- txOutput += var_int(len(script))
- txOutput += script
- txOutput = bytes(txOutput)
-
if not self.supports_multi_output():
if len(tx.outputs()) > 2:
self.give_error("Transaction with more than 2 outputs not supported")
for txout in tx.outputs():
- if self.is_hw1() and txout.address and not is_b58_address(txout.address):
- self.give_error(_("This {} device can only send to base58 addresses.").format(keystore.device))
if not txout.address:
- if self.is_hw1():
- self.give_error(_("Only address outputs are supported by {}").format(keystore.device))
# note: max_size based on https://github.com/LedgerHQ/ledger-app-btc/commit/3a78dee9c0484821df58975803e40d58fbfc2c38#diff-c61ccd96a6d8b54d48f54a3bc4dfa7e2R26
validate_op_return_output(txout, max_size=190)
# Output "change" detection
- # - only one output and one change is authorized (for hw.1 and nano)
- # - at most one output can bypass confirmation (~change) (for all)
+ # - at most one output can bypass confirmation (~change)
if not p2shTransaction:
has_change = False
any_output_on_change_branch = is_any_tx_output_on_change_branch(tx)
@@ -643,10 +629,6 @@ class Ledger_Client_Legacy(Ledger_Client):
assert changePath
changePath = convert_bip32_intpath_to_strpath(changePath)[2:]
has_change = True
- else:
- output = txout.address
- else:
- output = txout.address
try:
# Get trusted inputs from the original transactions
@@ -681,22 +663,11 @@ class Ledger_Client_Legacy(Ledger_Client):
firstTransaction = True
inputIndex = 0
rawTx = tx.serialize_to_network(include_sigs=False)
- 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
# as we only care about the alternateEncoding==True branch
outputData = self.dongleObject.finalizeInput(b'', 0, 0, changePath, bfh(rawTx))
- outputData['outputData'] = txOutput
- if outputData['confirmationNeeded']:
- outputData['address'] = output
- self.handler.finished()
- # do the authenticate dialog and get pin:
- pin = self.handler.get_auth(outputData, client=self)
- if not pin:
- raise UserWarning()
- self.handler.show_message(_("Confirmed. Signing Transaction..."))
while inputIndex < len(inputs):
self.handler.show_message(_("Signing transaction...") + f" (phase2, {inputIndex}/{len(inputs)})")
singleInput = [chipInputs[inputIndex]]
@@ -716,24 +687,14 @@ class Ledger_Client_Legacy(Ledger_Client):
# we don't set meaningful outputAddress, amount and fees
# as we only care about the alternateEncoding==True branch
outputData = self.dongleObject.finalizeInput(b'', 0, 0, changePath, bfh(rawTx))
- outputData['outputData'] = txOutput
- if outputData['confirmationNeeded']:
- outputData['address'] = output
- self.handler.finished()
- # do the authenticate dialog and get pin:
- pin = self.handler.get_auth(outputData, client=self)
- if not pin:
- raise UserWarning()
- self.handler.show_message(_("Confirmed. Signing Transaction..."))
- else:
- # Sign input with the provided PIN
- inputSignature = self.dongleObject.untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime)
- inputSignature[0] = 0x30 # force for 1.4.9+
- my_pubkey = inputs[inputIndex][4]
- tx.add_signature_to_txin(txin_idx=inputIndex,
- signing_pubkey=my_pubkey,
- sig=inputSignature)
- inputIndex = inputIndex + 1
+ # Sign input with the provided PIN
+ inputSignature = self.dongleObject.untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime)
+ inputSignature[0] = 0x30 # force for 1.4.9+
+ my_pubkey = inputs[inputIndex][4]
+ tx.add_signature_to_txin(txin_idx=inputIndex,
+ signing_pubkey=my_pubkey,
+ sig=inputSignature)
+ inputIndex = inputIndex + 1
firstTransaction = False
except UserWarning:
self.handler.show_error(_('Cancelled by user'))
@@ -770,12 +731,6 @@ class Ledger_Client_Legacy(Ledger_Client):
try:
info = self.dongleObject.signMessagePrepare(address_path, message)
pin = ""
- if info['confirmationNeeded']:
- # do the authenticate dialog and get pin:
- pin = self.handler.get_auth(info, client=self)
- if not pin:
- raise UserWarning(_('Cancelled by user'))
- pin = str(pin).encode()
signature = self.dongleObject.signMessageSign(pin)
except BTChipException as e:
if e.sw == 0x6a80:
@@ -813,88 +768,6 @@ 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.\nPlease 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."""
@@ -1333,10 +1206,10 @@ class LedgerPlugin(HW_PluginBase):
keystore_class = Ledger_KeyStore
minimum_library = (0, 2, 0)
maximum_library = (0, 4, 0)
- DEVICE_IDS = [(0x2581, 0x1807), # HW.1 legacy btchip
- (0x2581, 0x2b7c), # HW.1 transitional production
- (0x2581, 0x3b7c), # HW.1 ledger production
- (0x2581, 0x4b7c), # HW.1 ledger test
+ DEVICE_IDS = [(0x2581, 0x1807), # HW.1 legacy btchip # not supported anymore (but we log an exception)
+ (0x2581, 0x2b7c), # HW.1 transitional production # not supported anymore
+ (0x2581, 0x3b7c), # HW.1 ledger production # not supported anymore
+ (0x2581, 0x4b7c), # HW.1 ledger test # not supported anymore
(0x2c97, 0x0000), # Blue
(0x2c97, 0x0001), # Nano-S
(0x2c97, 0x0004), # Nano-X
diff --git a/electrum/plugins/ledger/qt.py b/electrum/plugins/ledger/qt.py
index 6e2d1f19e..e36d38a13 100644
--- a/electrum/plugins/ledger/qt.py
+++ b/electrum/plugins/ledger/qt.py
@@ -52,46 +52,9 @@ class Plugin(LedgerPlugin, QtPluginBase):
class Ledger_Handler(QtHandlerBase):
- setup_signal = pyqtSignal()
- auth_signal = pyqtSignal(object, object)
MESSAGE_DIALOG_TITLE = _("Ledger Status")
def __init__(self, win):
super(Ledger_Handler, self).__init__(win, 'Ledger')
- self.setup_signal.connect(self.setup_dialog)
- self.auth_signal.connect(self.auth_dialog)
- def word_dialog(self, msg):
- response = QInputDialog.getText(self.top_level_window(), "Ledger Wallet Authentication", msg, QLineEdit.EchoMode.Password)
- if not response[1]:
- self.word = None
- else:
- self.word = str(response[0])
- self.done.set()
-
- def auth_dialog(self, data, client: 'Ledger_Client'):
- try:
- from .auth2fa import LedgerAuthDialog
- except ImportError as e:
- self.message_dialog(repr(e))
- return
- dialog = LedgerAuthDialog(self, data, client=client)
- dialog.exec()
- self.word = dialog.pin
- self.done.set()
-
- def get_auth(self, data, *, client: 'Ledger_Client'):
- self.done.clear()
- self.auth_signal.emit(data, client)
- self.done.wait()
- return self.word
-
- def get_setup(self):
- self.done.clear()
- self.setup_signal.emit()
- self.done.wait()
- return
-
- def setup_dialog(self):
- self.show_error(_('Initialization of Ledger HW devices is currently disabled.'))