mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-12-22 14:28: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:
parent
2c390a2030
commit
ffe07f2ca6
@ -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")
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user