refactor(core): simplify word validity error handling on recovery
What changed, and why it matters
This commit is a straightforward internal code cleanup in the Trezor firmware's wallet recovery flow. It moves the responsibility for showing error screens from the layout code into the exception classes themselves, without changing what errors are detected or how the recovery process behaves. There is no indication this fixes a security bug or introduces a vulnerability.
No security action required. Treat as a normal maintainability refactor and review for code quality if desired.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch refactors error handling during recovery seed entry. Previously, word_validity.check() raised specific WordValidityResult subclasses (AlreadyAdded, IdentifierMismatch, ThresholdReached) and layout.request_mnemonic() caught each to call the corresponding UI error function. After the change, each exception subclass carries its own show_error callback, and homescreen.py catches the base WordValidityResult and invokes exc.show_error(). The control flow is preserved: on a share-related error, the UI error is shown and the loop requests the mnemonic again. No validation logic, return values, or user-visible behavior appear to change.
Changed components
core/src/apps/management/recovery_device/homescreen.pycore/src/apps/management/recovery_device/layout.pycore/src/apps/management/recovery_device/word_validity.pyInspect captured patch +33 / −20
diff --git a/core/src/apps/management/recovery_device/homescreen.py b/core/src/apps/management/recovery_device/homescreen.py
index 6b1b960f..d6b1face 100644
--- a/core/src/apps/management/recovery_device/homescreen.py
+++ b/core/src/apps/management/recovery_device/homescreen.py
@@ -94,6 +94,8 @@ async def _continue_recovery_process() -> Success:
from trezor.enums import RecoveryType
from trezor.errors import MnemonicError
+ from .word_validity import WordValidityResult
+
# gather the current recovery state from storage
recovery_type = storage_recovery.get_type()
word_count, backup_type = recover.load_slip39_state()
@@ -130,7 +132,12 @@ async def _continue_recovery_process() -> Success:
assert word_count is not None
# ask for mnemonic words one by one
- words = await layout.request_mnemonic(word_count, backup_type)
+ try:
+ # returns `None` on cancellation
+ words = await layout.request_mnemonic(word_count, backup_type)
+ except WordValidityResult as exc:
+ await exc.show_error()
+ words = None
# if they were invalid or some checks failed we continue and request them again
if not words:
diff --git a/core/src/apps/management/recovery_device/layout.py b/core/src/apps/management/recovery_device/layout.py
index a03f4281..88d25e33 100644
--- a/core/src/apps/management/recovery_device/layout.py
+++ b/core/src/apps/management/recovery_device/layout.py
@@ -30,6 +30,12 @@ if TYPE_CHECKING:
async def request_mnemonic(
word_count: int, backup_type: BackupType | None
) -> str | None:
+ """
+ Loops until a mnemonic is entered.
+
+ Returns a space-separated mnemonic on success, None on cancellation.
+ Raises `WordValidityResult` on share-related error.
+ """
from trezor.ui.layouts.recovery import request_word
from . import word_validity
@@ -68,21 +74,9 @@ async def request_mnemonic(
i += 1
- try:
- non_empty_words = [word for word in words if word]
- word_validity.check(backup_type, non_empty_words)
- except word_validity.AlreadyAdded:
- # show_share_already_added
- await show_already_added()
- return None
- except word_validity.IdentifierMismatch:
- # show_identifier_mismatch
- await show_identifier_mismatch()
- return None
- except word_validity.ThresholdReached:
- # show_group_threshold_reached
- await show_group_threshold()
- return None
+ non_empty_words = [word for word in words if word]
+ # raises `WordValidityResult` on error.
+ word_validity.check(backup_type, non_empty_words)
return " ".join(words)
diff --git a/core/src/apps/management/recovery_device/word_validity.py b/core/src/apps/management/recovery_device/word_validity.py
index 68fc9c70..07d349c6 100644
--- a/core/src/apps/management/recovery_device/word_validity.py
+++ b/core/src/apps/management/recovery_device/word_validity.py
@@ -1,23 +1,35 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
+ from typing import Awaitable, Callable
+
from trezor.enums import BackupType
class WordValidityResult(Exception):
- pass
+ def __init__(self, show_error: Callable[[], Awaitable[None]]) -> None:
+ self.show_error = show_error
class IdentifierMismatch(WordValidityResult):
- pass
+ def __init__(self) -> None:
+ from .layout import show_identifier_mismatch
+
+ super().__init__(show_identifier_mismatch)
class AlreadyAdded(WordValidityResult):
- pass
+ def __init__(self) -> None:
+ from .layout import show_already_added
+
+ super().__init__(show_already_added)
class ThresholdReached(WordValidityResult):
- pass
+ def __init__(self) -> None:
+ from .layout import show_group_threshold
+
+ super().__init__(show_group_threshold)
def check(backup_type: BackupType | None, partial_mnemonic: list[str]) -> None:
Why this scored 13/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.