chore: propagate `BackupMethod` from `trezorctl` to recovery handler
What changed, and why it matters
This commit adds a way for the Trezor host software (trezorctl) to tell the device which backup/recovery method to use during wallet recovery. Right now only the existing display-based method is actually supported; the new parameter is mainly plumbing so the host can skip an extra user-choice screen in future versions. There is no obvious security bug in the change itself.
No immediate action required. If this code is a precursor to additional backup methods, ensure the firmware validates and enforces the method consistently, fails closed on unknown methods, and documents any trust boundary between host choice and device recovery flow.
Security signals we found
New host-to-device message field propagation
Firmware logs unsupported backup method but does not fail closed
Trezor One explicitly rejects backup_method usage
No validation that propagated method matches persisted recovery state
Evidence from the diff
The change propagates a new backup_method field from the Python trezorctl recover command through trezorlib.device.recover() into the RecoveryDevice protobuf message, and on the firmware side passes it into recovery_process(). The firmware currently only implements _DisplayHandler, logs a warning if a non-Display method is supplied, and rejects backup_method on Trezor One. It is preparatory/refactoring work with a [no changelog] tag.
Changed components
core/src/apps/management/recovery_device/__init__.pycore/src/apps/management/recovery_device/homescreen.pypython/src/trezorlib/cli/device.pypython/src/trezorlib/device.pyInspect captured patch +42 / −12
diff --git a/core/src/apps/management/recovery_device/__init__.py b/core/src/apps/management/recovery_device/__init__.py
index a8ac1c3c..d5a5a48e 100644
--- a/core/src/apps/management/recovery_device/__init__.py
+++ b/core/src/apps/management/recovery_device/__init__.py
@@ -77,7 +77,7 @@ async def recovery_device(msg: RecoveryDevice) -> Success:
# --------------------------------------------------------
if storage_recovery.is_in_progress():
- return await recovery_process()
+ return await recovery_process(method=msg.backup_method)
if recovery_type == RecoveryType.NormalRecovery:
await confirm_reset_device(recovery=True)
@@ -112,4 +112,4 @@ async def recovery_device(msg: RecoveryDevice) -> Success:
workflow.set_default(recovery_homescreen)
- return await recovery_process()
+ return await recovery_process(method=msg.backup_method)
diff --git a/core/src/apps/management/recovery_device/homescreen.py b/core/src/apps/management/recovery_device/homescreen.py
index 53783ec9..ded87fd9 100644
--- a/core/src/apps/management/recovery_device/homescreen.py
+++ b/core/src/apps/management/recovery_device/homescreen.py
@@ -1,4 +1,4 @@
-from typing import TYPE_CHECKING, Awaitable
+from typing import TYPE_CHECKING, Awaitable, Protocol
import storage.device as storage_device
import storage.recovery as storage_recovery
@@ -13,7 +13,7 @@ from apps.management.recovery_device.recover import RecoveryAborted
from . import layout, recover
if TYPE_CHECKING:
- from trezor.enums import BackupType, RecoveryType
+ from trezor.enums import BackupMethod, BackupType, RecoveryType
from .layout import RemainingSharesInfo
@@ -29,10 +29,11 @@ async def recovery_homescreen() -> None:
elif not storage_recovery.is_in_progress():
workflow.set_default(homescreen)
else:
- await recovery_process()
+ # backup method will be chosen by the user
+ await recovery_process(None)
-async def recovery_process() -> Success:
+async def recovery_process(method: BackupMethod | None) -> Success:
import storage
from trezor.enums import MessageType, RecoveryType
@@ -52,7 +53,7 @@ async def recovery_process() -> Success:
MessageType.EndSession,
)
try:
- return await _continue_recovery_process()
+ return await _continue_recovery_process(method)
except recover.RecoveryAborted:
storage_recovery.end_progress()
backup.deactivate_repeated_backup()
@@ -89,6 +90,17 @@ async def _continue_repeated_backup() -> None:
backup.deactivate_repeated_backup()
+if TYPE_CHECKING:
+
+ class RecoveryHandler(Protocol):
+ @classmethod
+ async def load(cls, recovery_type: RecoveryType) -> "RecoveryHandler": ...
+
+ async def show_state(self, is_retry: bool) -> None: ...
+ async def request_mnemonic(self) -> str | None: ...
+ def show_invalid_mnemonic(self) -> Awaitable[None]: ...
+
+
class _DisplayHandler:
def __init__(
self,
@@ -101,7 +113,7 @@ class _DisplayHandler:
self.backup_type = backup_type
@classmethod
- async def load(cls, recovery_type: RecoveryType) -> "_DisplayHandler":
+ async def load(cls, recovery_type: RecoveryType) -> "RecoveryHandler":
# `slip39_state is None` indicates that we are (re)starting the first recovery step,
# which includes word count selection.
if (slip39_state := recover.load_slip39_state()) is None:
@@ -143,15 +155,25 @@ class _DisplayHandler:
return show_invalid_mnemonic(self.word_count)
-async def _recover_secret(recovery_type: RecoveryType) -> tuple[bytes, BackupType]:
+async def _recover_secret(
+ recovery_type: RecoveryType, method: BackupMethod | None
+) -> tuple[bytes, BackupType]:
+ from trezor.enums import BackupMethod
from trezor.errors import MnemonicError
+ if method not in (None, BackupMethod.Display):
+ from trezor import log
+
+ log.warning(__name__, "Unsupported backup method: %s", method)
+
+ handler_type = _DisplayHandler
+
# Show recovery state in the beginning, on some failures, and after a successful share entry.
is_retry = False
while True:
# Load existing recovery state (persisted by _process_words below).
- handler = await _DisplayHandler.load(recovery_type)
+ handler = await handler_type.load(recovery_type)
await handler.show_state(is_retry)
is_retry = False
@@ -168,14 +190,14 @@ async def _recover_secret(recovery_type: RecoveryType) -> tuple[bytes, BackupTyp
is_retry = True # Retry share entry (without showing recovery state)
-async def _continue_recovery_process() -> Success:
+async def _continue_recovery_process(method: BackupMethod | None) -> Success:
from trezor.enums import RecoveryType
# gather the current recovery state from storage
recovery_type = storage_recovery.get_type()
# run recovery process - may raise RecoveryAborted
- secret, backup_type = await _recover_secret(recovery_type)
+ secret, backup_type = await _recover_secret(recovery_type, method)
# finish recovery
if recovery_type == RecoveryType.DryRun:
diff --git a/python/src/trezorlib/cli/device.py b/python/src/trezorlib/cli/device.py
index c43dc894..5deebb76 100644
--- a/python/src/trezorlib/cli/device.py
+++ b/python/src/trezorlib/cli/device.py
@@ -176,6 +176,7 @@ def load(
type=ChoiceType(RECOVERY_DEVICE_INPUT_METHOD),
default=None,
)
+@click.option("-m", "--backup-method", type=ChoiceType(BACKUP_METHOD))
@click.option("-d", "--dry-run", is_flag=True)
@click.option("-b", "--unlock-repeated-backup", is_flag=True)
@with_session(seedless=True)
@@ -188,6 +189,7 @@ def recover(
label: str | None,
u2f_counter: int,
input_method: messages.RecoveryDeviceInputMethod | None,
+ backup_method: messages.BackupMethod | None,
dry_run: bool,
unlock_repeated_backup: bool,
) -> None:
@@ -223,6 +225,7 @@ def recover(
u2f_counter=u2f_counter,
input_callback=input_callback,
input_method=input_method,
+ backup_method=backup_method,
type=type,
)
diff --git a/python/src/trezorlib/device.py b/python/src/trezorlib/device.py
index 508d1d8a..e172ab53 100644
--- a/python/src/trezorlib/device.py
+++ b/python/src/trezorlib/device.py
@@ -150,6 +150,7 @@ def recover(
language: Optional[str] = None,
input_callback: Optional[Callable] = None,
input_method: messages.RecoveryDeviceInputMethod = messages.RecoveryDeviceInputMethod.ScrambledWords,
+ backup_method: Optional[messages.BackupMethod] = None,
dry_run: Optional[bool] = None,
u2f_counter: Optional[int] = None,
*,
@@ -181,6 +182,9 @@ def recover(
if session.features.model == "1" and input_callback is None:
raise RuntimeError("Input callback required for Trezor One")
+ if session.features.model == "1" and backup_method is not None:
+ raise RuntimeError("Backup method cannot be set for Trezor One")
+
if word_count not in (12, 18, 24):
raise ValueError("Invalid word count. Use 12/18/24")
@@ -196,6 +200,7 @@ def recover(
word_count=word_count,
enforce_wordlist=True,
input_method=input_method,
+ backup_method=backup_method,
type=type,
)
Why this scored 18/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.