feat(python): client.ensure_unlocked() works without deriving a session
What changed, and why it matters
This is a small optimization in the Trezor Python library. The `ensure_unlocked()` helper used to pick or create a wallet session, which forced the hardware wallet to derive the master seed just to check if the device was unlocked. The new code instead sends a harmless 'ApplyFlags(0)' command, which requires the user to enter their PIN but does not need seed derivation. This is a performance and usability improvement, not a security fix. There is no evidence in the commit of a vulnerability being patched.
No security action required. Treat as a normal library improvement. Reviewers may optionally verify that `ApplyFlags(flags=0)` is indeed a no-op on all supported firmware versions, as the commit comment claims.
Security signals we found
No security-relevant keywords in commit title or message
No CVE, advisory, or vendor security notice referenced
Change is framed as a feature/performance optimization in changelog
No input validation, cryptographic, or authorization logic changed
No new dependencies or external interfaces introduced
Evidence from the diff
The change refactors TrezorClient.ensure_unlocked() and Session.ensure_unlocked(). Previously, unlocking verification was done indirectly by retrieving the root fingerprint via GET_ROOT_FINGERPRINT_MESSAGE, which required a derived session and seed derivation. The new implementation calls ApplyFlags(flags=0) through _get_any_session(), which requires PIN unlock but does not derive the seed. The commit message and changelog frame this as a feature/performance change. No security boundary is crossed, no sensitive data is exposed, and no bug class is corrected in the diff.
Changed components
python/src/trezorlib/client.pyTrezor Python library: TrezorClient.ensure_unlocked()Trezor Python library: Session.ensure_unlocked()Inspect captured patch +17 / −20
diff --git a/python/.changelog.d/7168.changed b/python/.changelog.d/7168.changed
new file mode 100644
index 00000000..dc3f78c8
--- /dev/null
+++ b/python/.changelog.d/7168.changed
@@ -0,0 +1 @@
+`TrezorClient.ensure_unlocked()` will not allocate a session and will not cause seed derivation. `Session.ensure_unlocked()` now just forwards to `TrezorClient`.
diff --git a/python/src/trezorlib/client.py b/python/src/trezorlib/client.py
index 90a1ad29..eb85da0f 100644
--- a/python/src/trezorlib/client.py
+++ b/python/src/trezorlib/client.py
@@ -173,22 +173,8 @@ class Session(t.Generic[ClientType, SessionIdType]):
@enter_context
def ensure_unlocked(self) -> None:
- """Ensure that the device is unlocked.
-
- This method only works on sessions that have a passphrase derived, and
- transitively, only on an initialized device.
-
- Go through `client.ensure_unlocked()` if you want to abstract away the
- choice of a correct session for this operation.
- """
- resp = self.call(GET_ROOT_FINGERPRINT_MESSAGE, expect=messages.PublicKey)
- # resp.root_fingerprint is not available on <1.9.4 & <2.3.5
- assert resp.node.fingerprint is not None
- root_fingerprint = resp.node.fingerprint.to_bytes(4, "big")
- if self._root_fingerprint is None:
- self._root_fingerprint = root_fingerprint
- assert self._root_fingerprint == root_fingerprint
- self.refresh_features()
+ """Ensure that the device is unlocked."""
+ self.client.ensure_unlocked(_use_session=self)
@enter_context
def lock(self) -> None:
@@ -573,14 +559,24 @@ class TrezorClient(t.Generic[SessionType], metaclass=ABCMeta):
session.call_raw(messages.LockDevice())
self.refresh_features()
- def ensure_unlocked(self) -> None:
- """Ensure the device is unlocked."""
+ def ensure_unlocked(self, *, _use_session: SessionType | None = None) -> None:
+ """Ensure the device is unlocked.
+
+ If the device has PIN set, this will trigger a PIN unlock.
+ """
if not self.features.initialized:
# uninitialized device cannot be locked
return
- session = self.get_session(passphrase=PassphraseSetting.STANDARD_WALLET)
+ session = _use_session or self._get_any_session()
with session:
- session.ensure_unlocked()
+ # ApplyFlags(0) is a no-op because the device (1) ORs the flags into
+ # the current value, which does nothing, then (2) only writes the
+ # flags if modified.
+ # It needs PIN unlock to access the flags value but does not derive seed.
+ session.call(messages.ApplyFlags(flags=0), expect=messages.Success)
+
+ # refresh features to update fields whose state is different after unlock.
+ self.refresh_features()
def _invalidate(self) -> None:
"""Invalidate the client after a device wipe.
Why this scored 19/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.