1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-06-27 02:12:35 +00:00

feat(core/sdbackup): SD card backup basic flow

- WIP
- saving to plain text for now
This commit is contained in:
obrusvit 2023-11-21 19:31:34 +01:00
parent 071e1fa903
commit d6791dcfc7
8 changed files with 156 additions and 41 deletions

View File

@ -345,6 +345,8 @@ apps.management.reset_device
import apps.management.reset_device import apps.management.reset_device
apps.management.reset_device.layout apps.management.reset_device.layout
import apps.management.reset_device.layout import apps.management.reset_device.layout
apps.management.sd_backup
import apps.management.sd_backup
apps.management.sd_protect apps.management.sd_protect
import apps.management.sd_protect import apps.management.sd_protect
apps.management.set_u2f_counter apps.management.set_u2f_counter

View File

@ -21,7 +21,11 @@ async def recovery_device(msg: RecoveryDevice) -> Success:
import storage.recovery as storage_recovery import storage.recovery as storage_recovery
from trezor import config, wire, workflow from trezor import config, wire, workflow
from trezor.enums import ButtonRequestType 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 ( from apps.common.request_pin import (
error_pin_invalid, error_pin_invalid,

View File

@ -44,53 +44,64 @@ async def recovery_process() -> Success:
async def _continue_recovery_process() -> Success: async def _continue_recovery_process() -> Success:
from trezor import utils from trezor import utils
from trezor.errors import MnemonicError from trezor.errors import MnemonicError
from trezor.ui.layouts import choose_backup_medium
# gather the current recovery state from storage # gather the current recovery state from storage
dry_run = storage_recovery.is_dry_run() dry_run = storage_recovery.is_dry_run()
word_count, backup_type = recover.load_slip39_state() word_count, backup_type = recover.load_slip39_state()
# Both word_count and backup_type are derived from the same data. Both will be # ask the user for backup type (words/SD card)
# either set or unset. We use 'backup_type is None' to detect status of both. backup_medium: str = await choose_backup_medium(recovery=True)
# The following variable indicates that we are (re)starting the first recovery step, if backup_medium == "sdcard":
# which includes word count selection. from apps.management.sd_backup import sdcard_recover_seed
is_first_step = backup_type is None
if not is_first_step: words = await sdcard_recover_seed()
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: 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: if not is_first_step:
secret, backup_type = await _process_words(words) assert word_count is not None
# If _process_words succeeded, we now have both backup_type (from # If we continue recovery, show starting screen with word count immediately.
# its result) and word_count (from _request_word_count earlier), which means await _request_share_first_screen(word_count)
# that the first step is complete.
is_first_step = False secret = None
except MnemonicError: while secret is None:
await layout.show_invalid_mnemonic(word_count) 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 assert backup_type is not None
if dry_run: if dry_run:

View File

@ -213,10 +213,24 @@ def _compute_secret_from_entropy(
return secret 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: async def backup_seed(backup_type: BackupType, mnemonic_secret: bytes) -> None:
if backup_type == BAK_T_SLIP39_BASIC: if backup_type == BAK_T_SLIP39_BASIC:
await _backup_slip39_basic(mnemonic_secret) await _backup_slip39_basic(mnemonic_secret)
elif backup_type == BAK_T_SLIP39_ADVANCED: elif backup_type == BAK_T_SLIP39_ADVANCED:
await _backup_slip39_advanced(mnemonic_secret) await _backup_slip39_advanced(mnemonic_secret)
else: 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.")

View File

@ -158,6 +158,12 @@ async def show_backup_success() -> None:
# BIP39 # 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: async def bip39_show_and_confirm_mnemonic(mnemonic: str) -> None:
# warn user about mnemonic safety # warn user about mnemonic safety

View File

@ -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')

View File

@ -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( async def confirm_output(
address: str, address: str,
amount: str, amount: str,

View File

@ -913,6 +913,10 @@ class InputFlowBip39Backup(InputFlowBase):
self.mnemonic = None self.mnemonic = None
def input_flow_common(self) -> BRGeneratorType: def input_flow_common(self) -> BRGeneratorType:
# choose Words
received = yield
self.debug.press_no()
# 1. Confirm Reset # 1. Confirm Reset
yield from click_through(self.debug, screens=1, code=B.ResetDevice) yield from click_through(self.debug, screens=1, code=B.ResetDevice)