You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
283 lines
9.1 KiB
283 lines
9.1 KiB
import storage.recovery
|
|
from trezor import strings, ui, wire
|
|
from trezor.crypto.slip39 import MAX_SHARE_COUNT
|
|
from trezor.enums import ButtonRequestType
|
|
from trezor.ui.components.tt.scroll import Paginated
|
|
from trezor.ui.components.tt.text import Text
|
|
from trezor.ui.components.tt.word_select import WordSelector
|
|
from trezor.ui.layouts import confirm_action, show_success, show_warning
|
|
|
|
from apps.common import button_request
|
|
from apps.common.confirm import confirm, info_confirm, require_confirm
|
|
|
|
from .. import backup_types
|
|
from . import word_validity
|
|
from .keyboard_bip39 import Bip39Keyboard
|
|
from .keyboard_slip39 import Slip39Keyboard
|
|
from .recover import RecoveryAborted
|
|
|
|
if False:
|
|
from typing import Callable, Iterable
|
|
from trezor.enums import BackupType
|
|
|
|
|
|
async def confirm_abort(ctx: wire.GenericContext, dry_run: bool = False) -> None:
|
|
if dry_run:
|
|
await confirm_action(
|
|
ctx,
|
|
"abort_recovery",
|
|
"Abort seed check",
|
|
description="Do you really want to abort the seed check?",
|
|
icon=ui.ICON_WIPE,
|
|
br_code=ButtonRequestType.ProtectCall,
|
|
)
|
|
else:
|
|
await confirm_action(
|
|
ctx,
|
|
"abort_recovery",
|
|
"Abort recovery",
|
|
description="Do you really want to abort the recovery process?",
|
|
action="All progress will be lost.",
|
|
reverse=True,
|
|
icon=ui.ICON_WIPE,
|
|
br_code=ButtonRequestType.ProtectCall,
|
|
)
|
|
|
|
|
|
async def request_word_count(ctx: wire.GenericContext, dry_run: bool) -> int:
|
|
await button_request(ctx, code=ButtonRequestType.MnemonicWordCount)
|
|
|
|
if dry_run:
|
|
text = Text("Seed check", ui.ICON_RECOVERY)
|
|
else:
|
|
text = Text("Recovery mode", ui.ICON_RECOVERY)
|
|
text.normal("Number of words?")
|
|
|
|
count = await ctx.wait(WordSelector(text))
|
|
# WordSelector can return int, or string if the value came from debuglink
|
|
# ctx.wait has a return type Any
|
|
# Hence, it is easier to convert the returned value to int explicitly
|
|
return int(count)
|
|
|
|
|
|
async def request_mnemonic(
|
|
ctx: wire.GenericContext, word_count: int, backup_type: BackupType | None
|
|
) -> str | None:
|
|
await button_request(ctx, code=ButtonRequestType.MnemonicInput)
|
|
|
|
words: list[str] = []
|
|
for i in range(word_count):
|
|
if backup_types.is_slip39_word_count(word_count):
|
|
keyboard: Slip39Keyboard | Bip39Keyboard = Slip39Keyboard(
|
|
"Type word %s of %s:" % (i + 1, word_count)
|
|
)
|
|
else:
|
|
keyboard = Bip39Keyboard("Type word %s of %s:" % (i + 1, word_count))
|
|
|
|
word = await ctx.wait(keyboard)
|
|
words.append(word)
|
|
|
|
try:
|
|
word_validity.check(backup_type, words)
|
|
except word_validity.AlreadyAdded:
|
|
await show_share_already_added(ctx)
|
|
return None
|
|
except word_validity.IdentifierMismatch:
|
|
await show_identifier_mismatch(ctx)
|
|
return None
|
|
except word_validity.ThresholdReached:
|
|
await show_group_threshold_reached(ctx)
|
|
return None
|
|
|
|
return " ".join(words)
|
|
|
|
|
|
async def show_remaining_shares(
|
|
ctx: wire.GenericContext,
|
|
groups: Iterable[tuple[int, tuple[str, ...]]], # remaining + list 3 words
|
|
shares_remaining: list[int],
|
|
group_threshold: int,
|
|
) -> None:
|
|
pages: list[ui.Component] = []
|
|
for remaining, group in groups:
|
|
if 0 < remaining < MAX_SHARE_COUNT:
|
|
text = Text("Remaining Shares")
|
|
text.bold(
|
|
strings.format_plural(
|
|
"{count} more {plural} starting", remaining, "share"
|
|
)
|
|
)
|
|
for word in group:
|
|
text.normal(word)
|
|
pages.append(text)
|
|
elif (
|
|
remaining == MAX_SHARE_COUNT and shares_remaining.count(0) < group_threshold
|
|
):
|
|
text = Text("Remaining Shares")
|
|
groups_remaining = group_threshold - shares_remaining.count(0)
|
|
text.bold(
|
|
strings.format_plural(
|
|
"{count} more {plural} starting", groups_remaining, "group"
|
|
)
|
|
)
|
|
for word in group:
|
|
text.normal(word)
|
|
pages.append(text)
|
|
await confirm(ctx, Paginated(pages), cancel=None)
|
|
|
|
|
|
async def show_group_share_success(
|
|
ctx: wire.GenericContext, share_index: int, group_index: int
|
|
) -> None:
|
|
text = Text("Success", ui.ICON_CONFIRM)
|
|
text.bold("You have entered")
|
|
text.bold("Share %s" % (share_index + 1))
|
|
text.normal("from")
|
|
text.bold("Group %s" % (group_index + 1))
|
|
|
|
await confirm(ctx, text, confirm="Continue", cancel=None)
|
|
|
|
|
|
async def show_dry_run_result(
|
|
ctx: wire.GenericContext, result: bool, is_slip39: bool
|
|
) -> None:
|
|
if result:
|
|
if is_slip39:
|
|
text = "The entered recovery\nshares are valid and\nmatch what is currently\nin the device."
|
|
else:
|
|
text = "The entered recovery\nseed is valid and\nmatches the one\nin the device."
|
|
await show_success(ctx, "success_dry_recovery", text, button="Continue")
|
|
else:
|
|
if is_slip39:
|
|
text = "The entered recovery\nshares are valid but\ndo not match what is\ncurrently in the device."
|
|
else:
|
|
text = "The entered recovery\nseed is valid but does\nnot match the one\nin the device."
|
|
await show_warning(ctx, "warning_dry_recovery", text, button="Continue")
|
|
|
|
|
|
async def show_dry_run_different_type(ctx: wire.GenericContext) -> None:
|
|
text = Text("Dry run failure", ui.ICON_CANCEL)
|
|
text.normal("Seed in the device was")
|
|
text.normal("created using another")
|
|
text.normal("backup mechanism.")
|
|
await require_confirm(
|
|
ctx, text, ButtonRequestType.ProtectCall, cancel=None, confirm="Continue"
|
|
)
|
|
|
|
|
|
async def show_invalid_mnemonic(ctx: wire.GenericContext, word_count: int) -> None:
|
|
if backup_types.is_slip39_word_count(word_count):
|
|
await show_warning(
|
|
ctx,
|
|
"warning_invalid_share",
|
|
"You have entered\nan invalid recovery\nshare.",
|
|
)
|
|
else:
|
|
await show_warning(
|
|
ctx,
|
|
"warning_invalid_seed",
|
|
"You have entered\nan invalid recovery\nseed.",
|
|
)
|
|
|
|
|
|
async def show_share_already_added(ctx: wire.GenericContext) -> None:
|
|
await show_warning(
|
|
ctx,
|
|
"warning_known_share",
|
|
"Share already entered,\nplease enter\na different share.",
|
|
)
|
|
|
|
|
|
async def show_identifier_mismatch(ctx: wire.GenericContext) -> None:
|
|
await show_warning(
|
|
ctx,
|
|
"warning_mismatched_share",
|
|
"You have entered\na share from another\nShamir Backup.",
|
|
)
|
|
|
|
|
|
async def show_group_threshold_reached(ctx: wire.GenericContext) -> None:
|
|
await show_warning(
|
|
ctx,
|
|
"warning_group_threshold",
|
|
"Threshold of this\ngroup has been reached.\nInput share from\ndifferent group.",
|
|
)
|
|
|
|
|
|
class RecoveryHomescreen(ui.Component):
|
|
def __init__(self, text: str, subtext: str | None = None):
|
|
super().__init__()
|
|
self.text = text
|
|
self.subtext = subtext
|
|
self.dry_run = storage.recovery.is_dry_run()
|
|
|
|
def on_render(self) -> None:
|
|
if not self.repaint:
|
|
return
|
|
|
|
if self.dry_run:
|
|
heading = "SEED CHECK"
|
|
else:
|
|
heading = "RECOVERY MODE"
|
|
ui.header_warning(heading, clear=False)
|
|
|
|
if not self.subtext:
|
|
ui.display.text_center(ui.WIDTH // 2, 80, self.text, ui.BOLD, ui.FG, ui.BG)
|
|
else:
|
|
ui.display.text_center(ui.WIDTH // 2, 65, self.text, ui.BOLD, ui.FG, ui.BG)
|
|
ui.display.text_center(
|
|
ui.WIDTH // 2, 92, self.subtext, ui.NORMAL, ui.FG, ui.BG
|
|
)
|
|
|
|
ui.display.text_center(
|
|
ui.WIDTH // 2, 130, "It is safe to eject Trezor", ui.NORMAL, ui.GREY, ui.BG
|
|
)
|
|
ui.display.text_center(
|
|
ui.WIDTH // 2, 155, "and continue later", ui.NORMAL, ui.GREY, ui.BG
|
|
)
|
|
|
|
self.repaint = False
|
|
|
|
if __debug__:
|
|
|
|
def read_content(self) -> list[str]:
|
|
return [self.__class__.__name__, self.text, self.subtext or ""]
|
|
|
|
|
|
async def homescreen_dialog(
|
|
ctx: wire.GenericContext,
|
|
homepage: RecoveryHomescreen,
|
|
button_label: str,
|
|
info_func: Callable | None = None,
|
|
) -> None:
|
|
while True:
|
|
if info_func:
|
|
continue_recovery = await info_confirm(
|
|
ctx,
|
|
homepage,
|
|
code=ButtonRequestType.RecoveryHomepage,
|
|
confirm=button_label,
|
|
info_func=info_func,
|
|
info="Info",
|
|
cancel="Abort",
|
|
)
|
|
else:
|
|
continue_recovery = await confirm(
|
|
ctx,
|
|
homepage,
|
|
code=ButtonRequestType.RecoveryHomepage,
|
|
confirm=button_label,
|
|
major_confirm=True,
|
|
)
|
|
if continue_recovery:
|
|
# go forward in the recovery process
|
|
break
|
|
# user has chosen to abort, confirm the choice
|
|
dry_run = storage.recovery.is_dry_run()
|
|
try:
|
|
await confirm_abort(ctx, dry_run)
|
|
except wire.ActionCancelled:
|
|
pass
|
|
else:
|
|
raise RecoveryAborted
|