feat(core/sdbackup): shamir backup and recovery

pull/3441/head
obrusvit 6 months ago
parent 69372af267
commit 6dc9c1dbeb

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

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

@ -42,25 +42,57 @@ async def recovery_process() -> Success:
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
if utils.USE_SD_CARD:
from apps.management.sd_backup import bip39_choose_backup_medium
while True:
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)
return await bip39_choose_backup_medium(recovery=True)
else:
return BackupMedium.Words
backup_medium = await choose_recovery_medium(word_count in (20, 33), dry_run)
if backup_medium == 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:
from trezor import utils
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
dry_run = storage_recovery.is_dry_run()
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)
secret = None
words = None
backup_medium = BackupMedium.Words
while secret is None:
if is_first_step:
backup_medium = await _choose_backup_medium()
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)
words, word_count = await _recover_mnemonic_or_share(
is_first_step, word_count, dry_run, backup_type
)
# if they were invalid or some checks failed we continue and request them again
if not words:
continue
assert word_count is not None
try:
secret, backup_type = await _process_words(words)

@ -4,7 +4,7 @@ import storage
import storage.device as storage_device
from trezor.crypto import slip39
from trezor.enums import BackupType
from trezor.wire import ProcessError
from trezor.wire import ProcessError, ActionCancelled
from . import layout
@ -110,6 +110,57 @@ async def reset_device(msg: ResetDevice) -> Success:
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:
# get number of shares
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,
)[0]
# show and confirm individual shares
# backup individual shares
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:
@ -169,8 +228,18 @@ async def _backup_slip39_advanced(encrypted_master_secret: bytes) -> None:
encrypted_master_secret,
)
# show and confirm individual shares
await layout.slip39_advanced_show_and_confirm_shares(mnemonics)
# backup individual shares
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:
@ -213,33 +282,10 @@ def _compute_secret_from_entropy(
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:
from storage.sd_seed_backup import BackupMedium
from trezor import utils
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:
if utils.USE_SD_CARD:
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.")
await _backup_bip39(mnemonic_secret)

@ -155,59 +155,18 @@ async def show_backup_success() -> None:
await show_success_backup()
# BIP39
# ===
async def bip39_show_and_confirm_mnemonic(mnemonic: str) -> None:
# warn user about mnemonic safety
await show_backup_warning()
async def show_and_confirm_mnemonic(
mnemonic: str,
share_index: int | None = None,
shares_total: int | None = None,
group_index: int | None = None,
groups_total: int | None = None, # TODO might be unused
) -> None:
words = mnemonic.split()
while True:
# 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
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
# 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

@ -5,25 +5,46 @@ if TYPE_CHECKING:
from trezor.enums import BackupType
async def bip39_choose_backup_medium(recovery: bool = False) -> BackupMedium:
# TODO this will be general, not only for BIP39
from trezor.ui.layouts import choose_backup_medium
async def choose_recovery_medium(is_slip39: bool, dry_run: bool) -> BackupMedium:
from trezor.ui.layouts import choose_recovery_medium
return await choose_recovery_medium(is_slip39, dry_run)
return await choose_backup_medium(recovery)
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
return await choose_backup_medium(share_index, group_index)
async def sdcard_backup_seed(mnemonic_secret: bytes, bak_t: BackupType) -> bool:
from storage.sd_seed_backup import store_seed_on_sdcard
async def sdcard_backup_seed(mnemonic_secret: bytes, backup_type: BackupType):
from storage.sd_seed_backup import store_seed_on_sdcard, is_backup_present
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)
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]:
from storage.sd_seed_backup import recover_seed_from_sdcard
from apps.common.sdcard import ensure_sdcard
await ensure_sdcard(ensure_filesystem=False)

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

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

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

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

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

Loading…
Cancel
Save