From d6791dcfc737b09638087c06c2cba8fd4aedc28c Mon Sep 17 00:00:00 2001 From: obrusvit Date: Tue, 21 Nov 2023 19:31:34 +0100 Subject: [PATCH] feat(core/sdbackup): SD card backup basic flow - WIP - saving to plain text for now --- core/src/all_modules.py | 2 + .../management/recovery_device/__init__.py | 6 +- .../management/recovery_device/homescreen.py | 89 +++++++++++-------- .../apps/management/reset_device/__init__.py | 16 +++- .../apps/management/reset_device/layout.py | 6 ++ core/src/apps/management/sd_backup.py | 45 ++++++++++ core/src/trezor/ui/layouts/tt/__init__.py | 29 ++++++ tests/input_flows.py | 4 + 8 files changed, 156 insertions(+), 41 deletions(-) create mode 100644 core/src/apps/management/sd_backup.py diff --git a/core/src/all_modules.py b/core/src/all_modules.py index aba9d6a111..bc5d4859c3 100644 --- a/core/src/all_modules.py +++ b/core/src/all_modules.py @@ -345,6 +345,8 @@ apps.management.reset_device import apps.management.reset_device apps.management.reset_device.layout import apps.management.reset_device.layout +apps.management.sd_backup +import apps.management.sd_backup apps.management.sd_protect import apps.management.sd_protect apps.management.set_u2f_counter diff --git a/core/src/apps/management/recovery_device/__init__.py b/core/src/apps/management/recovery_device/__init__.py index d44c5f57ac..95cda93616 100644 --- a/core/src/apps/management/recovery_device/__init__.py +++ b/core/src/apps/management/recovery_device/__init__.py @@ -21,7 +21,11 @@ async def recovery_device(msg: RecoveryDevice) -> Success: import storage.recovery as storage_recovery from trezor import config, wire, workflow from trezor.enums import ButtonRequestType - from trezor.ui.layouts import confirm_action, confirm_reset_device + from trezor.ui.layouts import ( + confirm_action, + confirm_reset_device, + choose_backup_medium, + ) from apps.common.request_pin import ( error_pin_invalid, diff --git a/core/src/apps/management/recovery_device/homescreen.py b/core/src/apps/management/recovery_device/homescreen.py index ef44dfe592..8329d281c5 100644 --- a/core/src/apps/management/recovery_device/homescreen.py +++ b/core/src/apps/management/recovery_device/homescreen.py @@ -44,53 +44,64 @@ async def recovery_process() -> Success: async def _continue_recovery_process() -> Success: from trezor import utils from trezor.errors import MnemonicError + from trezor.ui.layouts import choose_backup_medium # gather the current recovery state from storage dry_run = storage_recovery.is_dry_run() word_count, backup_type = recover.load_slip39_state() - # Both word_count and backup_type are derived from the same data. Both will be - # either set or unset. We use 'backup_type is None' to detect status of both. - # The following variable indicates that we are (re)starting the first recovery step, - # which includes word count selection. - is_first_step = backup_type is None + # ask the user for backup type (words/SD card) + backup_medium: str = await choose_backup_medium(recovery=True) + if backup_medium == "sdcard": + from apps.management.sd_backup import sdcard_recover_seed - if not is_first_step: - assert word_count is not None - # If we continue recovery, show starting screen with word count immediately. - await _request_share_first_screen(word_count) - - secret = None - while secret is None: - if is_first_step: - # If we are starting recovery, ask for word count first... - # _request_word_count - # For TT, just continuing straight to word count keyboard - if utils.INTERNAL_MODEL == "T2B1": - await layout.homescreen_dialog( - "Continue", "Select the number of words in your backup." - ) - # ask for the number of words - word_count = await layout.request_word_count(dry_run) - # ...and only then show the starting screen with word count. - await _request_share_first_screen(word_count) - assert word_count is not None - - # ask for mnemonic words one by one - words = await layout.request_mnemonic(word_count, backup_type) - - # if they were invalid or some checks failed we continue and request them again + words = await sdcard_recover_seed() if not words: - continue + raise wire.ProcessError("SD card backup could not be recovered.") + secret, backup_type = await _process_words(words) + else: + # Both word_count and backup_type are derived from the same data. Both will be + # either set or unset. We use 'backup_type is None' to detect status of both. + # The following variable indicates that we are (re)starting the first recovery step, + # which includes word count selection. + is_first_step = backup_type is None - try: - secret, backup_type = await _process_words(words) - # If _process_words succeeded, we now have both backup_type (from - # its result) and word_count (from _request_word_count earlier), which means - # that the first step is complete. - is_first_step = False - except MnemonicError: - await layout.show_invalid_mnemonic(word_count) + if not is_first_step: + assert word_count is not None + # If we continue recovery, show starting screen with word count immediately. + await _request_share_first_screen(word_count) + + secret = None + while secret is None: + if is_first_step: + # If we are starting recovery, ask for word count first... + # _request_word_count + # For TT, just continuing straight to word count keyboard + if utils.INTERNAL_MODEL == "T2B1": + await layout.homescreen_dialog( + "Continue", "Select the number of words in your backup." + ) + # ask for the number of words + word_count = await layout.request_word_count(dry_run) + # ...and only then show the starting screen with word count. + await _request_share_first_screen(word_count) + assert word_count is not None + + # ask for mnemonic words one by one + words = await layout.request_mnemonic(word_count, backup_type) + + # if they were invalid or some checks failed we continue and request them again + if not words: + continue + + try: + secret, backup_type = await _process_words(words) + # If _process_words succeeded, we now have both backup_type (from + # its result) and word_count (from _request_word_count earlier), which means + # that the first step is complete. + is_first_step = False + except MnemonicError: + await layout.show_invalid_mnemonic(word_count) assert backup_type is not None if dry_run: diff --git a/core/src/apps/management/reset_device/__init__.py b/core/src/apps/management/reset_device/__init__.py index 7a58f9554d..3d84ad2a4e 100644 --- a/core/src/apps/management/reset_device/__init__.py +++ b/core/src/apps/management/reset_device/__init__.py @@ -213,10 +213,24 @@ def _compute_secret_from_entropy( return secret +async def _backup_bip39_sdcard(mnemonic: str) -> None: + from apps.management.sd_backup import sdcard_backup_seed, sdcard_verify_backup + await sdcard_backup_seed(mnemonic) + backup_succes = sdcard_verify_backup(mnemonic) + if not backup_succes: + raise ProcessError("SD Card backup could not be verified.") + + async def backup_seed(backup_type: BackupType, mnemonic_secret: bytes) -> None: if backup_type == BAK_T_SLIP39_BASIC: await _backup_slip39_basic(mnemonic_secret) elif backup_type == BAK_T_SLIP39_ADVANCED: await _backup_slip39_advanced(mnemonic_secret) else: - await layout.bip39_show_and_confirm_mnemonic(mnemonic_secret.decode()) + backup_medium: str = await layout.bip39_choose_backup_medium() + if backup_medium == "sdcard": + await _backup_bip39_sdcard(mnemonic_secret) + elif backup_medium == "words": + await layout.bip39_show_and_confirm_mnemonic(mnemonic_secret.decode()) + else: + raise ProcessError("Invalid backup medium.") diff --git a/core/src/apps/management/reset_device/layout.py b/core/src/apps/management/reset_device/layout.py index 6653cfee14..fdbb5f0d34 100644 --- a/core/src/apps/management/reset_device/layout.py +++ b/core/src/apps/management/reset_device/layout.py @@ -158,6 +158,12 @@ async def show_backup_success() -> None: # BIP39 # === +async def bip39_choose_backup_medium() -> str: + # TODO this will be general, not only for BIP39 + from trezor.ui.layouts import choose_backup_medium + + return await choose_backup_medium() + async def bip39_show_and_confirm_mnemonic(mnemonic: str) -> None: # warn user about mnemonic safety diff --git a/core/src/apps/management/sd_backup.py b/core/src/apps/management/sd_backup.py new file mode 100644 index 0000000000..16de145c3c --- /dev/null +++ b/core/src/apps/management/sd_backup.py @@ -0,0 +1,45 @@ +from trezor import io, utils +from trezor.sdcard import with_filesystem + +if utils.USE_SD_CARD: + fatfs = io.fatfs # global_import_cache + + +async def sdcard_backup_seed(mnemonic_secret: bytes) -> None: + from apps.common.sdcard import ensure_sdcard + + await ensure_sdcard() + _write_seed_plain_text(mnemonic_secret) + + +async def sdcard_recover_seed() -> str | None: + from apps.common.sdcard import ensure_sdcard + + await ensure_sdcard(ensure_filesystem=False) + return _read_seed_plain_text() + + +def sdcard_verify_backup(mnemonic_secret: bytes) -> bool: + mnemonic_read = _read_seed_plain_text() + if mnemonic_read is None: + return False + + return mnemonic_read.encode() == mnemonic_secret + +@with_filesystem +def _write_seed_plain_text(mnemonic_secret: bytes) -> None: + # TODO to be removed, just for testing purposes + fatfs.mkdir("/trezor", True) + with fatfs.open("/trezor/seed.txt", "w") as f: + f.write(mnemonic_secret) + +@with_filesystem +def _read_seed_plain_text() -> str | None: + # TODO to be removed, just for testing purposes + mnemonic_read = bytearray(512) + try: + with fatfs.open("/trezor/seed.txt", "r") as f: + f.read(mnemonic_read) + except fatfs.FatFSError: + return None + return mnemonic_read.decode('utf-8').rstrip('\x00') diff --git a/core/src/trezor/ui/layouts/tt/__init__.py b/core/src/trezor/ui/layouts/tt/__init__.py index 4f5f0d6fc0..d2a993e093 100644 --- a/core/src/trezor/ui/layouts/tt/__init__.py +++ b/core/src/trezor/ui/layouts/tt/__init__.py @@ -575,6 +575,35 @@ async def show_success( ) +async def choose_backup_medium(recovery: bool = False) -> str: + # TODO what br type + br_type = "br_type" + if recovery: + br_code: ButtonRequestType = ButtonRequestType.RecoveryHomepage + description: str = "Do you have written words or SD card for recovery?" + else: + br_code: ButtonRequestType = ButtonRequestType.ResetDevice + description: str = "You will be able to backup on the 2n medium later." + + result = await interact( + RustLayout( + trezorui2.confirm_action( + title="Backup medium", # TODO naming convention (backup medium?) + action="Choose backup medium.", + description=description, + verb="SD card", + verb_cancel="Words", + ) + ), + br_type, + br_code, + ) + if result is CONFIRMED: + return "sdcard" + else: + return "words" + + async def confirm_output( address: str, amount: str, diff --git a/tests/input_flows.py b/tests/input_flows.py index f1a590fbcc..59a6044c4f 100644 --- a/tests/input_flows.py +++ b/tests/input_flows.py @@ -913,6 +913,10 @@ class InputFlowBip39Backup(InputFlowBase): self.mnemonic = None def input_flow_common(self) -> BRGeneratorType: + # choose Words + received = yield + self.debug.press_no() + # 1. Confirm Reset yield from click_through(self.debug, screens=1, code=B.ResetDevice)