What changed, and why it matters
This commit updates Electrum's Trezor hardware wallet plugin to work with a newer version of the trezorlib library (0.20.1). It rewrites how Electrum opens sessions with Trezor devices, especially when passphrases are used. The change is a routine dependency upgrade, but it touches sensitive code that handles private keys and device unlocking. There is no direct evidence in the commit that this fixes a known security vulnerability, but any bug in session or passphrase handling could in principle affect wallet security.
Treat as a normal dependency update with security-relevant side effects. Review the new session-handling logic for correct passphrase isolation, ensure the cached session is cleared on wallet lock/device removal, and verify compatibility with all supported Trezor firmware versions. Monitor trezorlib 0.20.1 release notes for any disclosed security fixes.
Security signals we found
Dependency upgrade of hardware-wallet communication library
Refactoring of device session and passphrase handling
Change from init_device() to ping() for liveness check
Introduction of cached session object that may hold unlocked device state
No CVE, security advisory, or bug identifier referenced in commit message
Evidence from the diff
The diff upgrades trezorlib from 0.13.10 to 0.20.1 and adapts the Electrum Trezor plugin to the library’s new session-based API. Key changes: (1) replaces direct TrezorClient instantiation with get_default_client(); (2) introduces a cached self.session property that first unlocks the device with a standard passphrase, then optionally re-derives a session with the user’s passphrase; (3) replaces most self.client call sites with self.session; (4) changes clear_session()/close() to call client.lock() and close the session; (5) updates minimum/maximum library version checks. The commit does not mention any CVE or security bug, and no verified external references were supplied.
Changed components
electrum/plugins/trezor/clientbase.pyelectrum/plugins/trezor/trezor.pycontrib/requirements/requirements-hw.txtcontrib/deterministic-build/requirements-hw.txtInspect captured patch +69 / −46
diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt
index 99956bb..b1d41c6 100644
--- a/contrib/deterministic-build/requirements-hw.txt
+++ b/contrib/deterministic-build/requirements-hw.txt
@@ -216,15 +216,6 @@ charset-normalizer==3.4.2 \
ckcc-protocol==1.4.0 \
--hash=sha256:c5fcc4705b4b78ec515b39549642570a660142407fa684c278cb0aea8122defa \
--hash=sha256:cd93d4d3e3308ea4580aa6be5b4613a8266fd96b0cc1af51e7168def27bbece5
-click==8.1.8 \
- --hash=sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2 \
- --hash=sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a
-construct==2.10.70 \
- --hash=sha256:4d2472f9684731e58cc9c56c463be63baa1447d674e0d66aeb5627b22f512c29 \
- --hash=sha256:c80be81ef595a1a821ec69dc16099550ed22197615f4320b57cc9ce2a672cb30
-construct-classes==0.1.2 \
- --hash=sha256:72ac1abbae5bddb4918688713f991f5a7fb6c9b593646a82f4bf3ac53de7eeb5 \
- --hash=sha256:e82437261790758bda41e45fb3d5622b54cfbf044ceb14774af68346faf5e08e
cryptography==45.0.3 \
--hash=sha256:00094838ecc7c6594171e8c8a9166124c1197b074cfca23645cee573910d76bc \
--hash=sha256:050ce5209d5072472971e6efbfc8ec5a8f9a841de5a4db0ebd9c2e392cb81972 \
@@ -424,9 +415,9 @@ six==1.17.0 \
slip10==1.0.1 \
--hash=sha256:02b350ae557b591791428b17551f95d7ac57e9211f37debdc814c90b4a123a54 \
--hash=sha256:4aa764369db0a261e468160ec1afeeb2b22d26392dd118c49b9daa91f642947b
-trezor==0.13.10 \
- --hash=sha256:7a0b6ae4628dd0c31a5ceb51258918d9bbdd3ad851388837225826b228ee504f \
- --hash=sha256:7c85dc2c47998765c84d309fc753d2b116c943d447289157895488899c95706d
+trezor==0.20.1 \
+ --hash=sha256:06f21ef1b0ad20f8bc220f229f2ff3abfedc15e90ca3bbdafcd967a6031e2cb3 \
+ --hash=sha256:6de50703102f90dc5399d40dd7c8134d13b6c54a617d41178b081baf2aeb2b91
typing-extensions==4.13.2 \
--hash=sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c \
--hash=sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef
diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt
index a0a8a4c..5dfb029 100644
--- a/contrib/requirements/requirements-hw.txt
+++ b/contrib/requirements/requirements-hw.txt
@@ -1,7 +1,7 @@
hidapi
# device plugin: trezor
-trezor[hidapi]>=0.13.0,<0.14
+trezor[hidapi]>=0.20.1,<0.21
# device plugin: safe_t
safet>=0.1.5
diff --git a/electrum/plugins/trezor/clientbase.py b/electrum/plugins/trezor/clientbase.py
index 1863afc..964b652 100644
--- a/electrum/plugins/trezor/clientbase.py
+++ b/electrum/plugins/trezor/clientbase.py
@@ -11,18 +11,13 @@ from electrum.logging import Logger
from electrum.plugin import runs_in_hwd_thread
from electrum.hw_wallet.plugin import OutdatedHwFirmwareException, HardwareClientBase
-from trezorlib.client import TrezorClient, PASSPHRASE_ON_DEVICE
+from trezorlib.client import TrezorClient, PassphraseSetting, get_default_client
from trezorlib.exceptions import TrezorFailure, Cancelled, OutdatedFirmwareError
-from trezorlib.messages import WordRequestType, FailureType, ButtonRequestType
+from trezorlib.messages import WordRequestType, FailureType, ButtonRequestType, Capability
import trezorlib.btc
import trezorlib.device
-try:
- # trezor >= 0.13.9
- from trezorlib.messages import RecoveryDeviceInputMethod
-except ImportError:
- # Backward compatibility for trezor < 0.13.9
- from trezorlib.messages import RecoveryDeviceType as RecoveryDeviceInputMethod
+from trezorlib.messages import RecoveryDeviceInputMethod
MESSAGES = {
@@ -53,7 +48,14 @@ class TrezorClientBase(HardwareClientBase, Logger):
HardwareClientBase.__init__(self, plugin=plugin)
if plugin.is_outdated_fw_ignored():
TrezorClient.is_outdated = lambda *args, **kwargs: False
- self.client = TrezorClient(transport, ui=self)
+
+ self.client = get_default_client(
+ app_name="Electrum",
+ path_or_transport=transport,
+ button_callback=self.button_request,
+ pin_callback=self.get_pin,
+ )
+ self._session = None
self.device = plugin.device
self.handler = handler
Logger.__init__(self)
@@ -65,6 +67,30 @@ class TrezorClientBase(HardwareClientBase, Logger):
self.used()
+ @property
+ def session(self):
+ if self._session is None:
+ assert self.handler is not None
+
+ # If needed, unlock the device (triggering PIN entry dialog for legacy model).
+ with self.client.get_session(passphrase=PassphraseSetting.STANDARD_WALLET) as session:
+ session.ensure_unlocked()
+
+ passphrase = PassphraseSetting.STANDARD_WALLET # (empty passphrase)
+ if self.client.features.passphrase_protection:
+ passphrase = self.get_passphrase(Capability.PassphraseEntry in self.client.features.capabilities)
+
+ # Then, derive a session for this wallet (possibly with a passphrase)
+ if passphrase == PassphraseSetting.STANDARD_WALLET:
+ self._session = session # reuse the session above to avoid re-derivation
+ self.logger.info("Opened standard %s", self._session)
+ else:
+ self._session = self.client.get_session(passphrase)
+ self.logger.info("Re-opened passphrase %s", self._session)
+
+ return self._session
+
+
def run_flow(self, message=None, creating_wallet=False):
if self.in_flow:
raise RuntimeError("Overlapping call to run_flow")
@@ -123,8 +149,9 @@ class TrezorClientBase(HardwareClientBase, Logger):
return True
try:
- self.client.init_device()
+ self.client.ping(message="")
except BaseException:
+ self.logger.exception("Ping failed")
return False
return True
@@ -148,7 +175,7 @@ class TrezorClientBase(HardwareClientBase, Logger):
def get_xpub(self, bip32_path, xtype, creating=False):
address_n = parse_path(bip32_path)
with self.run_flow(creating_wallet=creating):
- node = trezorlib.btc.get_public_node(self.client, address_n).node
+ node = trezorlib.btc.get_public_node(self.session, address_n).node
return BIP32Node(xtype=xtype,
eckey=ecc.ECPubkey(node.public_key),
chaincode=node.chain_code,
@@ -164,17 +191,17 @@ class TrezorClientBase(HardwareClientBase, Logger):
msg = _("Confirm on your {} device to enable passphrases")
enabled = not self.features.passphrase_protection
with self.run_flow(msg):
- trezorlib.device.apply_settings(self.client, use_passphrase=enabled)
+ trezorlib.device.apply_settings(self.session, use_passphrase=enabled)
@runs_in_hwd_thread
def change_label(self, label):
with self.run_flow(_("Confirm the new label on your {} device")):
- trezorlib.device.apply_settings(self.client, label=label)
+ trezorlib.device.apply_settings(self.session, label=label)
@runs_in_hwd_thread
def change_homescreen(self, homescreen):
with self.run_flow(_("Confirm on your {} device to change your home screen")):
- trezorlib.device.apply_settings(self.client, homescreen=homescreen)
+ trezorlib.device.apply_settings(self.session, homescreen=homescreen)
@runs_in_hwd_thread
def set_pin(self, remove):
@@ -185,7 +212,7 @@ class TrezorClientBase(HardwareClientBase, Logger):
else:
msg = _("Confirm on your {} device to set a PIN")
with self.run_flow(msg):
- trezorlib.device.change_pin(self.client, remove)
+ trezorlib.device.change_pin(self.session, remove)
@runs_in_hwd_thread
def clear_session(self):
@@ -194,7 +221,7 @@ class TrezorClientBase(HardwareClientBase, Logger):
self.logger.info(f"clear session: {self}")
self.prevent_timeouts()
try:
- self.client.clear_session()
+ self.close()
except BaseException as e:
# If the device was removed it has the same effect...
self.logger.info(f"clear_session: ignoring error {e}")
@@ -202,8 +229,11 @@ class TrezorClientBase(HardwareClientBase, Logger):
@runs_in_hwd_thread
def close(self):
'''Called when Our wallet was closed or the device removed.'''
- self.logger.info("closing client")
- self.clear_session()
+ self.logger.info("locking: %s", self.client)
+ self.client.lock()
+ self.logger.info("closing: %s", self._session)
+ if self._session is not None:
+ self._session.close()
@runs_in_hwd_thread
def is_uptodate(self):
@@ -233,7 +263,7 @@ class TrezorClientBase(HardwareClientBase, Logger):
address_n = parse_path(address_str)
with self.run_flow():
return trezorlib.btc.get_address(
- self.client,
+ self.session,
coin_name,
address_n,
show_display=True,
@@ -246,7 +276,7 @@ class TrezorClientBase(HardwareClientBase, Logger):
address_n = parse_path(address_str)
with self.run_flow():
return trezorlib.btc.sign_message(
- self.client,
+ self.session,
coin_name,
address_n,
message,
@@ -256,9 +286,9 @@ class TrezorClientBase(HardwareClientBase, Logger):
@runs_in_hwd_thread
def recover_device(self, recovery_type, *args, **kwargs):
input_callback = self.mnemonic_callback(recovery_type)
- with self.run_flow():
+ with self.run_flow(), self.client.get_session(None) as seedless_session:
return trezorlib.device.recover(
- self.client,
+ seedless_session,
*args,
input_callback=input_callback,
type=recovery_type,
@@ -269,27 +299,27 @@ class TrezorClientBase(HardwareClientBase, Logger):
@runs_in_hwd_thread
def sign_tx(self, *args, **kwargs):
with self.run_flow():
- return trezorlib.btc.sign_tx(self.client, *args, **kwargs)
+ return trezorlib.btc.sign_tx(self.session, *args, **kwargs)
@runs_in_hwd_thread
def get_ownership_id(self, *args, **kwargs):
with self.run_flow():
- return trezorlib.btc.get_ownership_id(self.client, *args, **kwargs)
+ return trezorlib.btc.get_ownership_id(self.session, *args, **kwargs)
@runs_in_hwd_thread
def get_ownership_proof(self, *args, **kwargs):
with self.run_flow():
- return trezorlib.btc.get_ownership_proof(self.client, *args, **kwargs)
+ return trezorlib.btc.get_ownership_proof(self.session, *args, **kwargs)
@runs_in_hwd_thread
def reset_device(self, *args, **kwargs):
- with self.run_flow():
- return trezorlib.device.reset(self.client, *args, **kwargs)
+ with self.run_flow(), self.client.get_session(None) as seedless_session:
+ return trezorlib.device.reset(seedless_session, *args, **kwargs)
@runs_in_hwd_thread
def wipe_device(self, *args, **kwargs):
- with self.run_flow():
- return trezorlib.device.wipe(self.client, *args, **kwargs)
+ with self.run_flow(), self.client.get_session(None) as seedless_session:
+ return trezorlib.device.wipe(seedless_session, *args, **kwargs)
# ========= UI methods ==========
@@ -335,7 +365,7 @@ class TrezorClientBase(HardwareClientBase, Logger):
self.handler.passphrase_on_device = available_on_device
passphrase = self.handler.get_passphrase(msg, self.creating_wallet)
- if passphrase is PASSPHRASE_ON_DEVICE:
+ if passphrase is PassphraseSetting.ON_DEVICE:
return passphrase
if passphrase is None:
raise Cancelled
diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py
index 9487707..16c3e79 100644
--- a/electrum/plugins/trezor/trezor.py
+++ b/electrum/plugins/trezor/trezor.py
@@ -33,10 +33,12 @@ try:
InputScriptType, OutputScriptType, MultisigRedeemScriptType,
TxInputType, TxOutputType, TxOutputBinType, TransactionType, AmountUnit)
- from trezorlib.client import PASSPHRASE_ON_DEVICE
+ from trezorlib.client import PassphraseSetting
import trezorlib.log
#trezorlib.log.enable_debug_output()
+ PASSPHRASE_ON_DEVICE = PassphraseSetting.ON_DEVICE
+
TREZORLIB = True
except Exception as e:
if not (isinstance(e, ModuleNotFoundError) and e.name == 'trezorlib'):
@@ -153,8 +155,8 @@ class TrezorPlugin(HW_PluginBase):
libraries_URL = 'https://pypi.org/project/trezor/'
minimum_firmware = (1, 5, 2)
keystore_class = TrezorKeyStore
- minimum_library = (0, 13, 0)
- maximum_library = (0, 14)
+ minimum_library = (0, 20, 0)
+ maximum_library = (0, 21)
SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh')
DEVICE_IDS = (TREZOR_PRODUCT_KEY,)
Why this scored 24/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.