diff --git a/python/.changelog.d/3993.added b/python/.changelog.d/3993.added new file mode 100644 index 0000000000..db05c85e57 --- /dev/null +++ b/python/.changelog.d/3993.added @@ -0,0 +1 @@ +Added support for Trezor models not known by the current version of the library. diff --git a/python/src/trezorlib/client.py b/python/src/trezorlib/client.py index 4e432bd012..529992dfb0 100644 --- a/python/src/trezorlib/client.py +++ b/python/src/trezorlib/client.py @@ -278,19 +278,10 @@ class TrezorClient(Generic[UI]): """Update internal fields based on passed-in Features message.""" if not self.model: - # Trezor Model One bootloader 1.8.0 or older does not send model name - model = models.by_internal_name(features.internal_model) - if model is None: - model = models.by_name(features.model or "1") - if model is None: - raise RuntimeError( - "Unsupported Trezor model" - f" (internal_model: {features.internal_model}, model: {features.model})" - ) - self.model = model + self.model = models.detect(features) if features.vendor not in self.model.vendors: - raise RuntimeError("Unsupported device") + raise exceptions.TrezorException(f"Unrecognized vendor: {features.vendor}") self.features = features self.version = ( diff --git a/python/src/trezorlib/models.py b/python/src/trezorlib/models.py index 2b96218bf1..e7a4d9ea75 100644 --- a/python/src/trezorlib/models.py +++ b/python/src/trezorlib/models.py @@ -19,7 +19,7 @@ from __future__ import annotations from dataclasses import dataclass from typing import Collection, Tuple -from . import mapping +from . import mapping, messages UsbId = Tuple[int, int] @@ -35,6 +35,8 @@ class TrezorModel: usb_ids: Collection[UsbId] default_mapping: mapping.ProtobufMapping + is_unknown: bool = False + # ==== internal names ==== @@ -129,6 +131,11 @@ TREZORS = frozenset({T1B1, T2T1, T2B1, T3T1, T3B1, T3W1, DISC1, DISC2}) def by_name(name: str | None) -> TrezorModel | None: + """Try to find a TrezorModel by its name. + + This is a fallback function in case `internal_model` is not available. For general + model detection, prefer `detect()`. + """ if name is None: return T1B1 for model in TREZORS: @@ -138,9 +145,45 @@ def by_name(name: str | None) -> TrezorModel | None: def by_internal_name(name: str | None) -> TrezorModel | None: + """Try to find a TrezorModel by its internal name. + + Used internally as part of `detect()` routine. For general model detection, prefer + calling `detect()`. + """ if name is None: return None for model in TREZORS: if model.internal_name == name: return model return None + + +def detect(features: messages.Features) -> TrezorModel: + """Detect Trezor model from its Features response. + + If `internal_name` is sent, tries to detect model based on it. If not (in older + firmwares), falls back to `model` field. + + If no match is found, returns an ad-hoc TrezorModel instance whose fields are set + based on the provided model and/or internal model. This can either represent a newer + model that is not recognized by the current version of the library, or a fork that + responds to Trezor wire protocol but is not actually a Trezor. + """ + model = by_internal_name(features.internal_model) + if model is not None: + return model + model = by_name(features.model) + if model is not None: + return model + + return TrezorModel( + name=features.model or "Unknown", + internal_name=features.internal_model or "????", + minimum_version=(0, 0, 0), + # Allowed vendors are the internal VENDORS list instead of trusting features.vendor. + # That way, an unrecognized non-Trezor device will fail the check in TrezorClient. + vendors=VENDORS, + usb_ids=(), + default_mapping=mapping.DEFAULT_MAPPING, + is_unknown=True, + ) diff --git a/python/tools/pybridge.py b/python/tools/pybridge.py index 30d69bbc9b..eac2fe0150 100644 --- a/python/tools/pybridge.py +++ b/python/tools/pybridge.py @@ -104,9 +104,7 @@ class Transport: self.transport = transport client = TrezorClient(transport, ui=SilentUI()) - self.model = ( - trezorlib.models.by_name(client.features.model) or trezorlib.models.TREZOR_T - ) + self.model = client.model client.end_session() def acquire(self, sid: str) -> str: