fix(python): improve `trezorctl` error handling
What changed, and why it matters
This commit refactors error handling in the Trezor Python client library and command-line tool. It removes some custom exception types and moves error printing into a shared helper. The changes appear to be a cleanup following an earlier pull request, not a fix for an active security vulnerability. There is no indication in the commit that this addresses a security issue.
No immediate action required. Treat as routine code-quality/maintenance change. If reviewing for defense in depth, verify that the removed uninitialized-device check and the removed 'Unexpected Code Entry Tag' failure translation are adequately covered by device-side firmware checks or other callers, so that downstream CLI behavior does not regress.
Security signals we found
Removal of explicit uninitialized-device guard in get_session()
Removal of custom exception types for uninitialized derivation and invalid code-entry tag
Refactoring only; no changelog entry requested
Commit message does not describe security relevance
Evidence from the diff
The patch consolidates duplicated connection/session context error handling in trezorlib/cli/__init__.py into a single _connection_context helper and a _print_exception method. It removes DerivationOnUninitaizedDeviceError and UnexpectedCodeEntryTagException from exceptions.py, removes the uninitialized-device check from TrezorClient.get_session(), and removes the special-case translation of ‘Unexpected Code Entry Tag’ TrezorFailure in the THP code-entry flow. Error messages are now printed with the exception class name and description. The commit message frames this as ‘improve trezorctl error handling’ following PR #5613 and explicitly marks ‘[no changelog]’.
Changed components
python/src/trezorlib/cli/__init__.pypython/src/trezorlib/client.pypython/src/trezorlib/exceptions.pyInspect captured patch +41 / −96
diff --git a/python/src/trezorlib/cli/__init__.py b/python/src/trezorlib/cli/__init__.py
index 4509e8637..695f48619 100644
--- a/python/src/trezorlib/cli/__init__.py
+++ b/python/src/trezorlib/cli/__init__.py
@@ -238,28 +238,15 @@ class TrezorConnection:
seedless_session = client.get_seedless_session()
return seedless_session
- @contextmanager
- def client_context(self):
- """Get a client instance as a context manager. Handle errors in a manner
- appropriate for end-users.
-
- Usage:
- >>> with obj.client_context() as client:
- >>> do_your_actions_here()
- """
+ def _connection_context(self, connect_fn: t.Callable[[], t.Any]):
try:
- client = self.get_client()
- except transport.DeviceIsBusy:
- click.echo("Device is in use by another process.")
- sys.exit(1)
- except Exception:
- click.echo("Failed to find a Trezor device.")
- if self.path is not None:
- click.echo(f"Using path: {self.path}")
+ conn = connect_fn()
+ except Exception as e:
+ self._print_exception(e, "Failed to connect")
sys.exit(1)
try:
- yield client
+ yield conn
except exceptions.Cancelled:
# handle cancel action
click.echo("Action was cancelled.")
@@ -269,6 +256,17 @@ class TrezorConnection:
raise click.ClickException(str(e)) from e
# other exceptions may cause a traceback
+ @contextmanager
+ def client_context(self):
+ """Get a client instance as a context manager. Handle errors in a manner
+ appropriate for end-users.
+
+ Usage:
+ >>> with obj.client_context() as client:
+ >>> do_your_actions_here()
+ """
+ yield from self._connection_context(self.get_client)
+
@contextmanager
def session_context(
self,
@@ -277,55 +275,25 @@ class TrezorConnection:
seedless: bool = False,
must_resume: bool = False,
):
- """Get a session instance as a context manager. Handle errors in a manner
- appropriate for end-users.
-
- Usage:
- >>> with obj.session_context() as session:
- >>> do_your_actions_here()
- """
- try:
- if seedless:
- session = self.get_seedless_session()
- else:
- session = self.get_session(
- derive_cardano=derive_cardano,
- empty_passphrase=empty_passphrase,
- must_resume=must_resume,
- )
- except exceptions.DeviceLocked:
- click.echo(
- "Device is locked, enter a pin on the device.",
- err=True,
+ yield from self._connection_context(
+ self.get_seedless_session
+ if seedless
+ else lambda: self.get_session(
+ derive_cardano=derive_cardano,
+ empty_passphrase=empty_passphrase,
+ must_resume=must_resume,
)
- sys.exit(1)
- except transport.DeviceIsBusy:
- click.echo("Device is in use by another process.")
- sys.exit(1)
- except exceptions.UnexpectedCodeEntryTagException:
- click.echo("Entered Code is invalid.")
- sys.exit(1)
- except exceptions.FailedSessionResumption:
- sys.exit(1)
- except exceptions.DerivationOnUninitaizedDeviceError:
- click.echo("Device is not initialized.")
- sys.exit(1)
- except Exception:
- click.echo("Failed to find a Trezor device.")
- if self.path is not None:
- click.echo(f"Using path: {self.path}")
- sys.exit(1)
+ )
- try:
- yield session
- except exceptions.Cancelled:
- # handle cancel action
- click.echo("Action was cancelled.")
- sys.exit(1)
- except exceptions.TrezorException as e:
- # handle any Trezor-sent exceptions as user-readable
- raise click.ClickException(str(e)) from e
- # other exceptions may cause a traceback
+ def _print_exception(self, exc: Exception, message: str):
+ LOG.debug(message, exc_info=True)
+ message = f"{message}: {exc.__class__.__name__}"
+ if description := str(exc):
+ message = f"{message} ({description})"
+
+ click.echo(message)
+ if self.path is not None:
+ click.echo(f"Using path: {self.path}")
def with_session(
diff --git a/python/src/trezorlib/client.py b/python/src/trezorlib/client.py
index d45d1679b..9abd05b96 100644
--- a/python/src/trezorlib/client.py
+++ b/python/src/trezorlib/client.py
@@ -173,20 +173,14 @@ class TrezorClient:
sha_ctx = sha256(cpace.shared_secret)
tag = sha_ctx.digest()
- try:
- secret_msg = session.call(
- messages.ThpCodeEntryCpaceHostTag(
- cpace_host_public_key=cpace.host_public_key,
- tag=tag,
- ),
- expect=messages.ThpCodeEntrySecret,
- skip_firmware_version_check=True,
- )
- except exceptions.TrezorFailure as e:
- if e.message == "Unexpected Code Entry Tag":
- raise exceptions.UnexpectedCodeEntryTagException
- else:
- raise e
+ secret_msg = session.call(
+ messages.ThpCodeEntryCpaceHostTag(
+ cpace_host_public_key=cpace.host_public_key,
+ tag=tag,
+ ),
+ expect=messages.ThpCodeEntrySecret,
+ skip_firmware_version_check=True,
+ )
# Check `commitment` and `code`
assert secret_msg.secret is not None
@@ -219,14 +213,7 @@ class TrezorClient:
) -> Session:
"""
Returns a new session.
-
- In the case of seed derivation, the function will fail if the device is not initialized.
"""
- if self.features.initialized is False and passphrase is not SEEDLESS:
- raise exceptions.DerivationOnUninitaizedDeviceError(
- "Calling uninitialized device with a passphrase. Call get_seedless_session instead."
- )
-
if isinstance(self.protocol, ProtocolV1Channel):
from .transport.session import SessionV1, derive_seed
diff --git a/python/src/trezorlib/exceptions.py b/python/src/trezorlib/exceptions.py
index 0f8c0d1f2..81faf337b 100644
--- a/python/src/trezorlib/exceptions.py
+++ b/python/src/trezorlib/exceptions.py
@@ -105,16 +105,6 @@ class InvalidSessionError(TrezorException):
Raised when Trezor returns unexpected PassphraseRequest"""
-class DerivationOnUninitaizedDeviceError(TrezorException):
- """Tried to derive seed on uninitialized device.
-
- To communicate with uninitialized device, use seedless session instead."""
-
-
-class UnexpectedCodeEntryTagException(TrezorException):
- pass
-
-
class ThpError(TrezorException):
pass
Why this scored 20/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.