1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-22 06:18:07 +00:00

feat(core): improve repeated backup

* allow upgrading from Single to Basic
* do not skip confirmation screen when sending BackupDevice from Suite
This commit is contained in:
matejcik 2024-06-05 17:41:40 +02:00 committed by matejcik
parent 2c390a2030
commit ffe07f2ca6
6 changed files with 161 additions and 48 deletions

View File

@ -1,63 +1,108 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import storage.device as storage_device
from trezor.enums import BackupType from trezor.enums import BackupType
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Sequence
from trezor.messages import BackupDevice, Success from trezor.messages import BackupDevice, Success
BAK_T_BIP39 = BackupType.Bip39 # global_import_cache async def perform_backup(
is_repeated_backup: bool,
group_threshold: int | None = None,
async def backup_device(msg: BackupDevice) -> Success: groups: Sequence[tuple[int, int]] = (),
import storage.device as storage_device ) -> None:
from trezor import wire from trezor import TR
from trezor.messages import Success from trezor.enums import ButtonRequestType
from trezor.ui.layouts import confirm_action
from trezor.utils import ensure
from apps.common import backup, backup_types, mnemonic from apps.common import backup, backup_types, mnemonic
from .reset_device import backup_seed, backup_slip39_custom, layout from .reset_device import backup_seed, backup_slip39_custom, layout
# Ask the user to confirm backup. The user can still escape here.
if is_repeated_backup:
await confirm_action(
"confirm_repeated_backup",
TR.recovery__title_unlock_repeated_backup,
description=TR.recovery__unlock_repeated_backup,
br_code=ButtonRequestType.ProtectCall,
verb=TR.recovery__unlock_repeated_backup_verb,
)
mnemonic_secret = mnemonic.get_secret()
ensure(mnemonic_secret is not None) # checked at run-time
assert mnemonic_secret is not None # checked at type-check time
backup_type = mnemonic.get_type()
# upgrade Single to Basic if necessary
if is_repeated_backup and backup_type == BackupType.Slip39_Single_Extendable:
# TODO upgrade to Advanced if appropriate
backup_type = BackupType.Slip39_Basic_Extendable
storage_device.set_backup_type(backup_type)
# set unfinished flag -- if the process gets interrupted, the unfinished flag stays
if not is_repeated_backup:
storage_device.set_unfinished_backup(True)
# Deactivate repeated backup, set backed up flag, before showing anything to the
# user. If anything bad happens from now on, the backup counts as "already done".
backup.deactivate_repeated_backup()
storage_device.set_backed_up()
if group_threshold is not None:
# Parameters provided from host side.
assert backup_types.is_slip39_backup_type(backup_type)
extendable = backup_types.is_extendable_backup_type(backup_type)
# Run the backup process directly.
await backup_slip39_custom(mnemonic_secret, group_threshold, groups, extendable)
else:
# No parameters provided, allow the user to configure them on screen.
await backup_seed(backup_type, mnemonic_secret)
# If the backup was successful, clear the unfinished flag and show success.
# (NOTE that if the user manages to enable repeated backup while unfinished flag is
# set, the unfinished flag is cleared here. That is the correct thing to do -- the
# user _has_ finished the backup because they were able to unlock the repeated
# backup -- and now they finished another one.)
storage_device.set_unfinished_backup(False)
await layout.show_backup_success()
async def backup_device(msg: BackupDevice) -> Success:
from trezor import wire
from trezor.messages import Success
from apps.common import backup, mnemonic
# do this early before we show any UI # do this early before we show any UI
# the homescreen will clear the flag right after its own UI is gone # the homescreen will clear the flag right after its own UI is gone
repeated_backup_enabled = backup.repeated_backup_enabled() repeated_backup_enabled = backup.repeated_backup_enabled()
is_repeated_backup = repeated_backup_enabled and not storage_device.needs_backup()
if not storage_device.is_initialized(): if not storage_device.is_initialized():
raise wire.NotInitialized("Device is not initialized") raise wire.NotInitialized("Device is not initialized")
if not storage_device.needs_backup() and not repeated_backup_enabled: if not storage_device.needs_backup() and not repeated_backup_enabled:
raise wire.ProcessError("Seed already backed up") raise wire.ProcessError("Seed already backed up")
mnemonic_secret, backup_type = mnemonic.get()
if mnemonic_secret is None:
raise RuntimeError
group_threshold = msg.group_threshold group_threshold = msg.group_threshold
groups = [(g.member_threshold, g.member_count) for g in msg.groups] groups = [(g.member_threshold, g.member_count) for g in msg.groups]
# validate host-side SLIP39 parameters
if group_threshold is not None: if group_threshold is not None:
if group_threshold < 1: if group_threshold < 1:
raise wire.DataError("group_threshold must be a positive integer") raise wire.DataError("group_threshold must be a positive integer")
if len(groups) < group_threshold: if len(groups) < group_threshold:
raise wire.DataError("Not enough groups provided for group_threshold") raise wire.DataError("Not enough groups provided for group_threshold")
if backup_type == BAK_T_BIP39: if mnemonic.is_bip39():
raise wire.ProcessError("Expected SLIP39 backup") raise wire.ProcessError("Expected SLIP39 backup")
elif len(groups) > 0: elif len(groups) > 0:
raise wire.DataError("group_threshold is missing") raise wire.DataError("group_threshold is missing")
if not repeated_backup_enabled: await perform_backup(is_repeated_backup, group_threshold, groups)
storage_device.set_unfinished_backup(True)
backup.deactivate_repeated_backup()
storage_device.set_backed_up()
if group_threshold is not None:
extendable = backup_types.is_extendable_backup_type(backup_type)
await backup_slip39_custom(mnemonic_secret, group_threshold, groups, extendable)
else:
await backup_seed(backup_type, mnemonic_secret)
storage_device.set_unfinished_backup(False)
await layout.show_backup_success()
return Success(message="Seed successfully backed up") return Success(message="Seed successfully backed up")

View File

@ -52,11 +52,10 @@ async def recovery_process() -> Success:
async def _continue_repeated_backup() -> None: async def _continue_repeated_backup() -> None:
from trezor.enums import ButtonRequestType, MessageType from trezor.enums import MessageType
from trezor.ui.layouts import confirm_action
from apps.common import backup, mnemonic from apps.common import backup
from apps.management.reset_device import backup_seed from apps.management.backup_device import perform_backup
wire.AVOID_RESTARTING_FOR = ( wire.AVOID_RESTARTING_FOR = (
MessageType.Initialize, MessageType.Initialize,
@ -65,19 +64,7 @@ async def _continue_repeated_backup() -> None:
) )
try: try:
await confirm_action( await perform_backup(is_repeated_backup=True)
"confirm_repeated_backup",
TR.recovery__title_unlock_repeated_backup,
description=TR.recovery__unlock_repeated_backup,
br_code=ButtonRequestType.ProtectCall,
verb=TR.recovery__unlock_repeated_backup_verb,
)
mnemonic_secret, backup_type = mnemonic.get()
if mnemonic_secret is None:
raise RuntimeError
await backup_seed(backup_type, mnemonic_secret)
finally: finally:
backup.deactivate_repeated_backup() backup.deactivate_repeated_backup()

View File

@ -146,6 +146,10 @@ def get_backup_type() -> BackupType:
return backup_type return backup_type
def set_backup_type(backup_type: BackupType) -> None:
common.set_uint8(_NAMESPACE, _BACKUP_TYPE, backup_type)
def is_passphrase_enabled() -> bool: def is_passphrase_enabled() -> bool:
return common.get_bool(_NAMESPACE, _USE_PASSPHRASE) return common.get_bool(_NAMESPACE, _USE_PASSPHRASE)

View File

@ -67,6 +67,9 @@ MNEMONIC_SLIP39_ADVANCED_33 = [
] ]
MNEMONIC_SLIP39_CUSTOM_1of1 = ["tolerate flexible academic academic average dwarf square home promise aspect temple cluster roster forward hand unfair tenant emperor ceramic element forget perfect knit adapt review usual formal receiver typical pleasure duke yield party"] MNEMONIC_SLIP39_CUSTOM_1of1 = ["tolerate flexible academic academic average dwarf square home promise aspect temple cluster roster forward hand unfair tenant emperor ceramic element forget perfect knit adapt review usual formal receiver typical pleasure duke yield party"]
MNEMONIC_SLIP39_CUSTOM_SECRET = "3439316237393562383066633231636364663436366330666263393863386663" MNEMONIC_SLIP39_CUSTOM_SECRET = "3439316237393562383066633231636364663436366330666263393863386663"
MNEMONIC_SLIP39_SINGLE_EXT_20 = ["academic again academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic pecan provide remember"]
# External entropy mocked as received from trezorlib. # External entropy mocked as received from trezorlib.
EXTERNAL_ENTROPY = b"zlutoucky kun upel divoke ody" * 2 EXTERNAL_ENTROPY = b"zlutoucky kun upel divoke ody" * 2
# fmt: on # fmt: on

View File

@ -21,7 +21,12 @@ from trezorlib import device, messages
from trezorlib.debuglink import TrezorClientDebugLink as Client from trezorlib.debuglink import TrezorClientDebugLink as Client
from trezorlib.exceptions import Cancelled, TrezorFailure from trezorlib.exceptions import Cancelled, TrezorFailure
from ..common import TEST_ADDRESS_N, WITH_MOCK_URANDOM, MNEMONIC_SLIP39_BASIC_20_3of6 from ..common import (
MNEMONIC_SLIP39_SINGLE_EXT_20,
TEST_ADDRESS_N,
WITH_MOCK_URANDOM,
MNEMONIC_SLIP39_BASIC_20_3of6,
)
from ..input_flows import InputFlowSlip39BasicBackup, InputFlowSlip39BasicRecoveryDryRun from ..input_flows import InputFlowSlip39BasicBackup, InputFlowSlip39BasicRecoveryDryRun
@ -65,7 +70,7 @@ def test_repeated_backup(client: Client):
# we can now perform another backup # we can now perform another backup
with client: with client:
IF = InputFlowSlip39BasicBackup(client, False) IF = InputFlowSlip39BasicBackup(client, False, repeated=True)
client.set_input_flow(IF.get()) client.set_input_flow(IF.get())
device.backup(client) device.backup(client)
@ -78,6 +83,46 @@ def test_repeated_backup(client: Client):
device.backup(client) device.backup(client)
@pytest.mark.setup_client(mnemonic=MNEMONIC_SLIP39_SINGLE_EXT_20)
@pytest.mark.skip_t1b1
@WITH_MOCK_URANDOM
def test_repeated_backup_upgrade_single(client: Client):
assert (
client.features.backup_availability == messages.BackupAvailability.NotAvailable
)
assert client.features.recovery_status == messages.RecoveryStatus.Nothing
assert client.features.backup_type == messages.BackupType.Slip39_Single_Extendable
# unlock repeated backup by entering the single share
with client:
IF = InputFlowSlip39BasicRecoveryDryRun(
client, MNEMONIC_SLIP39_SINGLE_EXT_20, unlock_repeated_backup=True
)
client.set_input_flow(IF.get())
ret = device.recover(client, type=messages.RecoveryType.UnlockRepeatedBackup)
assert ret == messages.Success(message="Backup unlocked")
assert (
client.features.backup_availability == messages.BackupAvailability.Available
)
assert client.features.recovery_status == messages.RecoveryStatus.Backup
# we can now perform another backup
with client:
IF = InputFlowSlip39BasicBackup(client, False, repeated=True)
client.set_input_flow(IF.get())
device.backup(client)
# backup type was upgraded:
assert client.features.backup_type == messages.BackupType.Slip39_Basic_Extendable
# the backup feature is locked again...
assert (
client.features.backup_availability == messages.BackupAvailability.NotAvailable
)
assert client.features.recovery_status == messages.RecoveryStatus.Nothing
with pytest.raises(TrezorFailure, match=r".*Seed already backed up"):
device.backup(client)
@pytest.mark.setup_client(needs_backup=True, mnemonic=MNEMONIC_SLIP39_BASIC_20_3of6) @pytest.mark.setup_client(needs_backup=True, mnemonic=MNEMONIC_SLIP39_BASIC_20_3of6)
@pytest.mark.skip_t1b1 @pytest.mark.skip_t1b1
@WITH_MOCK_URANDOM @WITH_MOCK_URANDOM

View File

@ -1421,12 +1421,18 @@ def load_N_shares(
class InputFlowSlip39BasicBackup(InputFlowBase): class InputFlowSlip39BasicBackup(InputFlowBase):
def __init__(self, client: Client, click_info: bool): def __init__(self, client: Client, click_info: bool, repeated: bool = False):
super().__init__(client) super().__init__(client)
self.mnemonics: list[str] = [] self.mnemonics: list[str] = []
self.click_info = click_info self.click_info = click_info
self.repeated = repeated
def input_flow_tt(self) -> BRGeneratorType: def input_flow_tt(self) -> BRGeneratorType:
if self.repeated:
# intro confirmation screen
yield
self.debug.press_yes()
yield # 1. Backup intro yield # 1. Backup intro
self.debug.press_yes() self.debug.press_yes()
yield # 2. Checklist yield # 2. Checklist
@ -1454,6 +1460,11 @@ class InputFlowSlip39BasicBackup(InputFlowBase):
self.debug.press_yes() self.debug.press_yes()
def input_flow_tr(self) -> BRGeneratorType: def input_flow_tr(self) -> BRGeneratorType:
if self.repeated:
# intro confirmation screen
yield
self.debug.press_yes()
yield # 1. Backup intro yield # 1. Backup intro
self.debug.press_yes() self.debug.press_yes()
yield # 2. Checklist yield # 2. Checklist
@ -1481,6 +1492,11 @@ class InputFlowSlip39BasicBackup(InputFlowBase):
self.debug.press_yes() self.debug.press_yes()
def input_flow_t3t1(self) -> BRGeneratorType: def input_flow_t3t1(self) -> BRGeneratorType:
if self.repeated:
# intro confirmation screen
yield
self.debug.press_yes()
yield # 1. Backup intro yield # 1. Backup intro
self.debug.wait_layout() self.debug.wait_layout()
self.debug.swipe_up() self.debug.swipe_up()
@ -1587,12 +1603,17 @@ class InputFlowSlip39BasicResetRecovery(InputFlowBase):
class InputFlowSlip39CustomBackup(InputFlowBase): class InputFlowSlip39CustomBackup(InputFlowBase):
def __init__(self, client: Client, share_count: int): def __init__(self, client: Client, share_count: int, repeated: bool = False):
super().__init__(client) super().__init__(client)
self.mnemonics: list[str] = [] self.mnemonics: list[str] = []
self.share_count = share_count self.share_count = share_count
self.repeated = repeated
def input_flow_tt(self) -> BRGeneratorType: def input_flow_tt(self) -> BRGeneratorType:
if self.repeated:
yield
self.debug.press_yes()
if self.share_count > 1: if self.share_count > 1:
yield # Checklist yield # Checklist
self.debug.press_yes() self.debug.press_yes()
@ -1611,6 +1632,10 @@ class InputFlowSlip39CustomBackup(InputFlowBase):
self.debug.press_yes() self.debug.press_yes()
def input_flow_tr(self) -> BRGeneratorType: def input_flow_tr(self) -> BRGeneratorType:
if self.repeated:
yield
self.debug.press_yes()
if self.share_count > 1: if self.share_count > 1:
yield # Checklist yield # Checklist
self.debug.press_yes() self.debug.press_yes()
@ -1629,6 +1654,10 @@ class InputFlowSlip39CustomBackup(InputFlowBase):
self.debug.press_yes() self.debug.press_yes()
def input_flow_t3t1(self) -> BRGeneratorType: def input_flow_t3t1(self) -> BRGeneratorType:
if self.repeated:
yield
self.debug.press_yes()
if self.share_count > 1: if self.share_count > 1:
yield # Checklist yield # Checklist
self.debug.press_yes() self.debug.press_yes()