1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-08-05 05:15:27 +00:00

feat(core/sdbackup): shamir backup and recovery

This commit is contained in:
obrusvit 2023-12-11 16:47:07 +01:00
parent 69372af267
commit 6dc9c1dbeb
11 changed files with 231 additions and 161 deletions

View File

@ -51,8 +51,8 @@
// this is a fixed size and should not be changed // this is a fixed size and should not be changed
#define SDCARD_BLOCK_SIZE (512) #define SDCARD_BLOCK_SIZE (512)
// fixed offset for SD seed backup: // fixed offset for SD seed backup:
// Maximal size for FAT16 + overhead + start offset (65525 + 552 + 63) // Maximal size for FAT16 + overhead + start offset
#define SDCARD_BACKUP_BLOCK_START (66140) #define SDCARD_BACKUP_BLOCK_START (65525 + 552 + 63)
void sdcard_init(void); void sdcard_init(void);
secbool __wur sdcard_power_on(void); secbool __wur sdcard_power_on(void);

View File

@ -1,4 +1,4 @@
from storage.sd_salt import SD_CARD_HOT_SWAPPABLE from trezor.sdcard import SD_CARD_HOT_SWAPPABLE
from trezor import io, wire from trezor import io, wire
from trezor.ui.layouts import confirm_action, show_error_and_raise from trezor.ui.layouts import confirm_action, show_error_and_raise
@ -111,15 +111,17 @@ async def ensure_sdcard(
fatfs = io.fatfs # local_cache_attribute fatfs = io.fatfs # local_cache_attribute
while True: while True:
try: try:
try: if not for_sd_backup:
with sdcard.filesystem(mounted=False): # cards for backup must be formatted to keep the unallocated space after partition
fatfs.mount() try:
except fatfs.NoFilesystem: with sdcard.filesystem(mounted=False):
# card not formatted. proceed out of the except clause fatfs.mount()
pass except fatfs.NoFilesystem:
else: # card not formatted. proceed out of the except clause
# no error when mounting pass
return else:
# no error when mounting
return
await _confirm_format_card() await _confirm_format_card()

View File

@ -42,25 +42,57 @@ async def recovery_process() -> Success:
raise wire.ActionCancelled raise wire.ActionCancelled
async def _choose_backup_medium() -> BackupMedium: async def _recover_mnemonic_or_share(
is_first_step: bool,
word_count: int | None,
dry_run: bool,
backup_type: BackupType | None,
) -> tuple[str | None, int | None]:
from trezor import utils from trezor import utils
if utils.USE_SD_CARD: while True:
from apps.management.sd_backup import bip39_choose_backup_medium backup_medium = BackupMedium.Words
if utils.USE_SD_CARD:
from apps.management.sd_backup import choose_recovery_medium
# ask the user for backup type (words/SD card) backup_medium = await choose_recovery_medium(word_count in (20, 33), dry_run)
return await bip39_choose_backup_medium(recovery=True)
else: if backup_medium == BackupMedium.Words:
return BackupMedium.Words if is_first_step:
# If we are starting recovery, ask for word count first...
# 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)
await _request_share_first_screen(word_count)
words = await layout.request_mnemonic(word_count, backup_type)
return words, word_count
else:
# try to recover from SD card
from apps.management.sd_backup import sdcard_recover_seed
try:
mnemonic, _ = await sdcard_recover_seed() # TODO backup type needed?
if mnemonic == None:
# TODO warn and repeat
pass
return mnemonic, len(mnemonic.split())
except ActionCancelled:
# there might have been a backup
# TODO show guidance: Pick different card/choose words
pass
except Exception:
# generic exception, let the user choose again
pass
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
# if utils.USE_SD_CARD:
from apps.management.sd_backup import sdcard_recover_seed
# 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()
@ -77,43 +109,15 @@ async def _continue_recovery_process() -> Success:
await _request_share_first_screen(word_count) await _request_share_first_screen(word_count)
secret = None secret = None
words = None
backup_medium = BackupMedium.Words
while secret is None: while secret is None:
if is_first_step: words, word_count = await _recover_mnemonic_or_share(
backup_medium = await _choose_backup_medium() is_first_step, word_count, dry_run, backup_type
if utils.USE_SD_CARD and backup_medium == BackupMedium.SDCard: )
# attempt to recover words from sd card
words, backup_type = await sdcard_recover_seed()
if words is None:
continue
word_count = len(words.split())
if word_count not in (12, 24):
await layout.show_recovery_warning(
"recovery", "Shamir not yet supported for SD"
)
raise wire.ProcessError("Attempt to recover Shamir from SD card.")
else:
# 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
if backup_medium == BackupMedium.Words:
# 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 they were invalid or some checks failed we continue and request them again
if not words: if not words:
continue continue
assert word_count is not None
try: try:
secret, backup_type = await _process_words(words) secret, backup_type = await _process_words(words)

View File

@ -4,7 +4,7 @@ import storage
import storage.device as storage_device import storage.device as storage_device
from trezor.crypto import slip39 from trezor.crypto import slip39
from trezor.enums import BackupType from trezor.enums import BackupType
from trezor.wire import ProcessError from trezor.wire import ProcessError, ActionCancelled
from . import layout from . import layout
@ -110,6 +110,57 @@ async def reset_device(msg: ResetDevice) -> Success:
return Success(message="Initialized") return Success(message="Initialized")
async def _backup_mnemonic_or_share(
mnemonic: bytes,
backup_type: BackupType,
share_index: int | None = None,
shares_total: int | None = None,
group_index: int | None = None,
groups_total: int | None = None,
):
from trezor import utils
from storage.sd_seed_backup import BackupMedium
while True:
# let the user choose between Words/SDcard backup
backup_medium = BackupMedium.Words
if utils.USE_SD_CARD:
from apps.management.sd_backup import choose_backup_medium
backup_medium = await choose_backup_medium(share_index, group_index)
# proceed with backup
if backup_medium == BackupMedium.Words:
# show words
await layout.show_and_confirm_mnemonic(
mnemonic.decode(),
share_index=share_index,
shares_total=shares_total,
group_index=group_index,
groups_total=groups_total,
)
break
else:
# try to store seed on SD card
from apps.management.sd_backup import sdcard_backup_seed
try:
await sdcard_backup_seed(mnemonic, backup_type)
break
except ActionCancelled:
# there might have been a backup
# TODO show guidance: Pick different card/choose words
pass
except Exception:
# generic exception, let the user choose again
pass
async def _backup_bip39(mnemonic_secret: bytes):
await layout.show_backup_warning()
await _backup_mnemonic_or_share(mnemonic_secret, BAK_T_BIP39)
async def _backup_slip39_basic(encrypted_master_secret: bytes) -> None: async def _backup_slip39_basic(encrypted_master_secret: bytes) -> None:
# get number of shares # get number of shares
await layout.slip39_show_checklist(0, BAK_T_SLIP39_BASIC) await layout.slip39_show_checklist(0, BAK_T_SLIP39_BASIC)
@ -133,9 +184,17 @@ async def _backup_slip39_basic(encrypted_master_secret: bytes) -> None:
encrypted_master_secret, encrypted_master_secret,
)[0] )[0]
# show and confirm individual shares # backup individual shares
await layout.slip39_show_checklist(2, BAK_T_SLIP39_BASIC) await layout.slip39_show_checklist(2, BAK_T_SLIP39_BASIC)
await layout.slip39_basic_show_and_confirm_shares(mnemonics)
await layout.show_backup_warning(True)
for share_index, share in enumerate(mnemonics):
await _backup_mnemonic_or_share(
share.encode(),
BAK_T_SLIP39_BASIC,
share_index,
len(mnemonics),
)
async def _backup_slip39_advanced(encrypted_master_secret: bytes) -> None: async def _backup_slip39_advanced(encrypted_master_secret: bytes) -> None:
@ -169,8 +228,18 @@ async def _backup_slip39_advanced(encrypted_master_secret: bytes) -> None:
encrypted_master_secret, encrypted_master_secret,
) )
# show and confirm individual shares # backup individual shares
await layout.slip39_advanced_show_and_confirm_shares(mnemonics) await layout.show_backup_warning(True)
for group_index, group in enumerate(mnemonics):
for share_index, share in enumerate(group):
await _backup_mnemonic_or_share(
share.encode(),
BAK_T_SLIP39_ADVANCED,
share_index,
len(group),
group_index,
len(mnemonics),
)
def _validate_reset_device(msg: ResetDevice) -> None: def _validate_reset_device(msg: ResetDevice) -> None:
@ -213,33 +282,10 @@ def _compute_secret_from_entropy(
return secret return secret
async def _backup_bip39_sdcard(mnemonic: bytes, backup_type: BackupType) -> None:
from apps.management.sd_backup import sdcard_backup_seed
backup_success: bool = await sdcard_backup_seed(mnemonic, backup_type)
if not backup_success:
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:
from storage.sd_seed_backup import BackupMedium
from trezor import utils
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:
if utils.USE_SD_CARD: await _backup_bip39(mnemonic_secret)
from apps.management.sd_backup import bip39_choose_backup_medium
backup_medium = await bip39_choose_backup_medium()
else:
backup_medium = BackupMedium.Words
if backup_medium == BackupMedium.SDCard:
await _backup_bip39_sdcard(mnemonic_secret, backup_type)
elif backup_medium == BackupMedium.Words:
await layout.bip39_show_and_confirm_mnemonic(mnemonic_secret.decode())
else:
raise ProcessError("Invalid backup medium.")

View File

@ -155,59 +155,18 @@ async def show_backup_success() -> None:
await show_success_backup() await show_success_backup()
# BIP39 async def show_and_confirm_mnemonic(
# === mnemonic: str,
share_index: int | None = None,
shares_total: int | None = None,
async def bip39_show_and_confirm_mnemonic(mnemonic: str) -> None: group_index: int | None = None,
# warn user about mnemonic safety groups_total: int | None = None, # TODO might be unused
await show_backup_warning() ) -> None:
words = mnemonic.split() words = mnemonic.split()
while True: while True:
# display paginated mnemonic on the screen # display paginated mnemonic on the screen
await show_share_words(words) await show_share_words(words, share_index, group_index)
# make the user confirm some words from the mnemonic # make the user confirm some words from the mnemonic
if await _share_words_confirmed(None, words): if await _share_words_confirmed(share_index, words, shares_total, group_index):
break # mnemonic is confirmed, go next break # mnemonic is confirmed, go next
# SLIP39
# ===
async def slip39_basic_show_and_confirm_shares(shares: Sequence[str]) -> None:
# warn user about mnemonic safety
await show_backup_warning(True)
for index, share in enumerate(shares):
share_words = share.split(" ")
while True:
# display paginated share on the screen
await show_share_words(share_words, index)
# make the user confirm words from the share
if await _share_words_confirmed(index, share_words, len(shares)):
break # this share is confirmed, go to next one
async def slip39_advanced_show_and_confirm_shares(
shares: Sequence[Sequence[str]],
) -> None:
# warn user about mnemonic safety
await show_backup_warning(True)
for group_index, group in enumerate(shares):
for share_index, share in enumerate(group):
share_words = share.split(" ")
while True:
# display paginated share on the screen
await show_share_words(share_words, share_index, group_index)
# make the user confirm words from the share
if await _share_words_confirmed(
share_index, share_words, len(group), group_index
):
break # this share is confirmed, go to next one

View File

@ -5,25 +5,46 @@ if TYPE_CHECKING:
from trezor.enums import BackupType from trezor.enums import BackupType
async def bip39_choose_backup_medium(recovery: bool = False) -> BackupMedium: async def choose_recovery_medium(is_slip39: bool, dry_run: bool) -> BackupMedium:
# TODO this will be general, not only for BIP39 from trezor.ui.layouts import choose_recovery_medium
return await choose_recovery_medium(is_slip39, dry_run)
async def choose_backup_medium(
share_index: int | None, group_index: int | None, recovery: bool = False
) -> BackupMedium:
from trezor.ui.layouts import choose_backup_medium from trezor.ui.layouts import choose_backup_medium
return await choose_backup_medium(recovery) return await choose_backup_medium(share_index, group_index)
async def sdcard_backup_seed(mnemonic_secret: bytes, bak_t: BackupType) -> bool: async def sdcard_backup_seed(mnemonic_secret: bytes, backup_type: BackupType):
from storage.sd_seed_backup import store_seed_on_sdcard from storage.sd_seed_backup import store_seed_on_sdcard, is_backup_present
from apps.common.sdcard import ensure_sdcard from apps.common.sdcard import ensure_sdcard
from trezor.ui.layouts import confirm_action, show_success
await ensure_sdcard(ensure_filesystem=False)
if is_backup_present():
await confirm_action(
"warning_sdcard_backup_exists",
"Backup present",
action="There is already a Trezor backup on this card!",
description="Replace the backup?",
verb="Replace",
verb_cancel="Abort",
hold=True,
hold_danger=True,
reverse=True,
)
await ensure_sdcard(ensure_filesystem=True, for_sd_backup=True) await ensure_sdcard(ensure_filesystem=True, for_sd_backup=True)
return store_seed_on_sdcard(mnemonic_secret, bak_t)
store_seed_on_sdcard(mnemonic_secret, backup_type)
await show_success("success_sdcard_backup", "Backup on SD card successful!")
async def sdcard_recover_seed() -> tuple[str | None, BackupType | None]: async def sdcard_recover_seed() -> tuple[str | None, BackupType | None]:
from storage.sd_seed_backup import recover_seed_from_sdcard from storage.sd_seed_backup import recover_seed_from_sdcard
from apps.common.sdcard import ensure_sdcard from apps.common.sdcard import ensure_sdcard
await ensure_sdcard(ensure_filesystem=False) await ensure_sdcard(ensure_filesystem=False)

View File

@ -13,7 +13,6 @@ if TYPE_CHECKING:
if utils.USE_SD_CARD: if utils.USE_SD_CARD:
fatfs = io.fatfs # global_import_cache fatfs = io.fatfs # global_import_cache
SD_CARD_HOT_SWAPPABLE = False
SD_SALT_LEN_BYTES = const(32) SD_SALT_LEN_BYTES = const(32)
_SD_SALT_AUTH_TAG_LEN_BYTES = const(16) _SD_SALT_AUTH_TAG_LEN_BYTES = const(16)

View File

@ -30,13 +30,12 @@ class BackupMedium(IntEnum):
@with_filesystem @with_filesystem
def store_seed_on_sdcard(mnemonic_secret: bytes, backup_type: BackupType) -> bool: def store_seed_on_sdcard(mnemonic_secret: bytes, backup_type: BackupType):
_write_seed_unalloc(mnemonic_secret, backup_type) _write_seed_unalloc(mnemonic_secret, backup_type)
if _verify_backup(mnemonic_secret, backup_type): if _verify_backup(mnemonic_secret, backup_type):
_write_readme() _write_readme()
return True
else: else:
return False raise ProcessError("SD card verification failed")
@with_sdcard @with_sdcard
@ -44,6 +43,12 @@ def recover_seed_from_sdcard() -> tuple[bytes | None, BackupType | None]:
return _read_seed_unalloc() return _read_seed_unalloc()
@with_sdcard
def is_backup_present() -> bool:
decoded_mnemonic, decoded_backup_type = _read_seed_unalloc()
return decoded_mnemonic is not None and decoded_backup_type is not None
def _verify_backup(mnemonic_secret: bytes, backup_type: BackupType) -> bool: def _verify_backup(mnemonic_secret: bytes, backup_type: BackupType) -> bool:
decoded_mnemonic, decoded_backup_type = _read_seed_unalloc() decoded_mnemonic, decoded_backup_type = _read_seed_unalloc()
if decoded_mnemonic is None or decoded_backup_type is None: if decoded_mnemonic is None or decoded_backup_type is None:

View File

@ -25,6 +25,7 @@ if TYPE_CHECKING:
P = ParamSpec("P") P = ParamSpec("P")
R = TypeVar("R") R = TypeVar("R")
SD_CARD_HOT_SWAPPABLE = False
def is_trz_card() -> bool: def is_trz_card() -> bool:
return True return True

View File

@ -577,23 +577,58 @@ async def show_success(
) )
async def choose_backup_medium(recovery: bool = False) -> BackupMedium: async def choose_backup_medium(
# TODO what br type share_index: int | None = None, group_index: int | None = None
) -> BackupMedium:
from storage.sd_seed_backup import BackupMedium from storage.sd_seed_backup import BackupMedium
br_type = "br_type" if share_index is None:
if recovery: action = f"Recovery seed"
br_code: ButtonRequestType = ButtonRequestType.RecoveryHomepage description = "You can backup your wallet using either SD card or word list."
description: str = "Do you have written words or SD card for recovery?" elif group_index is None:
action = f"Recovery share #{share_index + 1}"
description = "You can backup your share using either SD card or word list."
else: else:
br_code: ButtonRequestType = ButtonRequestType.ResetDevice action = f"Group {group_index + 1} - share {share_index + 1}"
description: str = "You will be able to backup on the 2n medium later." description = "You can backup your share using either SD card or word list."
br_type = "choose_backup_medium"
br_code: ButtonRequestType = ButtonRequestType.ResetDevice
result = await interact( result = await interact(
RustLayout( RustLayout(
trezorui2.confirm_action( trezorui2.confirm_action(
title="Backup medium", # TODO naming convention (backup medium?) title="Backup medium", # TODO naming convention
action="Choose backup medium.", action=action,
description=description,
verb="SD card",
verb_cancel="Words",
)
),
br_type,
br_code,
)
if result is CONFIRMED:
return BackupMedium.SDCard
else:
return BackupMedium.Words
async def choose_recovery_medium(is_slip39: bool = False, dry_run: bool = False) -> BackupMedium:
from storage.sd_seed_backup import BackupMedium
action = ""
thing = "share" if is_slip39 else "wallet"
if dry_run:
description = f"You can check your {thing} using either SD card or word list."
else:
description = f"You can recover your {thing} using either SD card or word list."
br_type = "choose_recovery_medium"
br_code: ButtonRequestType = ButtonRequestType.ResetDevice
result = await interact(
RustLayout(
trezorui2.confirm_action(
title="Wallet recovery",
action=action,
description=description, description=description,
verb="SD card", verb="SD card",
verb_cancel="Words", verb_cancel="Words",

View File

@ -18,8 +18,7 @@ class TestStorageSdSeedBackup(unittest.TestCase):
io.fatfs.mkfs(True) io.fatfs.mkfs(True)
io.fatfs.mount() io.fatfs.mount()
success = store_seed_on_sdcard(self.mnemonic, BackupType.Bip39) store_seed_on_sdcard(self.mnemonic, BackupType.Bip39)
self.assertTrue(success)
restored_mnemonic, restored_backup_type = recover_seed_from_sdcard() restored_mnemonic, restored_backup_type = recover_seed_from_sdcard()
self.assertEqual(restored_mnemonic, self.mnemonic) self.assertEqual(restored_mnemonic, self.mnemonic)
@ -28,10 +27,9 @@ class TestStorageSdSeedBackup(unittest.TestCase):
io.fatfs.unmount() io.fatfs.unmount()
io.sdcard.power_off() io.sdcard.power_off()
def test_backup_partlywipe_restore(self): def test_backup_and_partlywipe_then_restore(self):
with sdcard.filesystem(mounted=True): with sdcard.filesystem(mounted=True):
success = store_seed_on_sdcard(self.mnemonic, BackupType.Bip39) store_seed_on_sdcard(self.mnemonic, BackupType.Bip39)
self.assertTrue(success)
# wipe half of the card, restore must succeed # wipe half of the card, restore must succeed
block_buffer = bytearray(SDCARD_BLOCK_SIZE_B) block_buffer = bytearray(SDCARD_BLOCK_SIZE_B)