Skip to content

Commit

Permalink
feat(core): improve repeated backup
Browse files Browse the repository at this point in the history
* allow upgrading from Single to Basic
* do not skip confirmation screen when sending BackupDevice from Suite
  • Loading branch information
matejcik committed Jun 5, 2024
1 parent fcb7a35 commit 26d20e1
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 46 deletions.
95 changes: 70 additions & 25 deletions core/src/apps/management/backup_device.py
Original file line number Diff line number Diff line change
@@ -1,63 +1,108 @@
from typing import TYPE_CHECKING

import storage.device as storage_device
from trezor.enums import BackupType

if TYPE_CHECKING:
from typing import Sequence

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,
groups: Sequence[tuple[int, int]] = (),
) -> None:
from trezor import TR
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 .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:
import storage.device as storage_device
from trezor import wire
from trezor.messages import Success

from apps.common import backup, backup_types, mnemonic

from .reset_device import backup_seed, backup_slip39_custom, layout
from apps.common import backup, mnemonic

# do this early before we show any UI
# the homescreen will clear the flag right after its own UI is gone
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():
raise wire.NotInitialized("Device is not initialized")
if not storage_device.needs_backup() and not repeated_backup_enabled:
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
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 < 1:
raise wire.DataError("group_threshold must be a positive integer")
if len(groups) < 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")
elif len(groups) > 0:
raise wire.DataError("group_threshold is missing")

if not repeated_backup_enabled:
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()
await perform_backup(is_repeated_backup, group_threshold, groups)

return Success(message="Seed successfully backed up")
21 changes: 4 additions & 17 deletions core/src/apps/management/recovery_device/homescreen.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,10 @@ async def recovery_process() -> Success:


async def _continue_repeated_backup() -> None:
from trezor.enums import ButtonRequestType, MessageType
from trezor.ui.layouts import confirm_action
from trezor.enums import MessageType

from apps.common import backup, mnemonic
from apps.management.reset_device import backup_seed
from apps.common import backup
from apps.management.backup_device import perform_backup

wire.AVOID_RESTARTING_FOR = (
MessageType.Initialize,
Expand All @@ -65,19 +64,7 @@ async def _continue_repeated_backup() -> None:
)

try:
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, backup_type = mnemonic.get()
if mnemonic_secret is None:
raise RuntimeError

await backup_seed(backup_type, mnemonic_secret)
await perform_backup(is_repeated_backup=True)
finally:
backup.deactivate_repeated_backup()

Expand Down
4 changes: 4 additions & 0 deletions core/src/storage/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ def get_backup_type() -> BackupType:
return backup_type


def set_backup_type(backup_type: BackupType) -> None:
common.set_uint8(_NAMESPACE, _BACKUP_TYPE, backup_type)


def is_passphrase_enabled() -> bool:
return common.get_bool(_NAMESPACE, _USE_PASSPHRASE)

Expand Down
3 changes: 3 additions & 0 deletions tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@
]
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_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 = b"zlutoucky kun upel divoke ody" * 2
# fmt: on
Expand Down
49 changes: 47 additions & 2 deletions tests/device_tests/test_repeated_backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@
from trezorlib.debuglink import TrezorClientDebugLink as Client
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


Expand Down Expand Up @@ -65,10 +70,50 @@ def test_repeated_backup(client: Client):

# we can now perform another backup
with client:
IF = InputFlowSlip39BasicBackup(client, False)
IF = InputFlowSlip39BasicBackup(client, False, repeated=True)
client.set_input_flow(IF.get())
device.backup(client)

# 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(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
Expand Down
33 changes: 31 additions & 2 deletions tests/input_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -1421,12 +1421,18 @@ def load_N_shares(


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)
self.mnemonics: list[str] = []
self.click_info = click_info
self.repeated = repeated

def input_flow_tt(self) -> BRGeneratorType:
if self.repeated:
# intro confirmation screen
yield
self.debug.press_yes()

yield # 1. Backup intro
self.debug.press_yes()
yield # 2. Checklist
Expand Down Expand Up @@ -1454,6 +1460,11 @@ def input_flow_tt(self) -> BRGeneratorType:
self.debug.press_yes()

def input_flow_tr(self) -> BRGeneratorType:
if self.repeated:
# intro confirmation screen
yield
self.debug.press_yes()

yield # 1. Backup intro
self.debug.press_yes()
yield # 2. Checklist
Expand Down Expand Up @@ -1481,6 +1492,11 @@ def input_flow_tr(self) -> BRGeneratorType:
self.debug.press_yes()

def input_flow_t3t1(self) -> BRGeneratorType:
if self.repeated:
# intro confirmation screen
yield
self.debug.press_yes()

yield # 1. Backup intro
self.debug.wait_layout()
self.debug.swipe_up()
Expand Down Expand Up @@ -1587,12 +1603,17 @@ def input_flow_t3t1(self) -> BRGeneratorType:


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)
self.mnemonics: list[str] = []
self.share_count = share_count
self.repeated = repeated

def input_flow_tt(self) -> BRGeneratorType:
if self.repeated:
yield
self.debug.press_yes()

if self.share_count > 1:
yield # Checklist
self.debug.press_yes()
Expand All @@ -1611,6 +1632,10 @@ def input_flow_tt(self) -> BRGeneratorType:
self.debug.press_yes()

def input_flow_tr(self) -> BRGeneratorType:
if self.repeated:
yield
self.debug.press_yes()

if self.share_count > 1:
yield # Checklist
self.debug.press_yes()
Expand All @@ -1629,6 +1654,10 @@ def input_flow_tr(self) -> BRGeneratorType:
self.debug.press_yes()

def input_flow_t3t1(self) -> BRGeneratorType:
if self.repeated:
yield
self.debug.press_yes()

if self.share_count > 1:
yield # Checklist
self.debug.press_yes()
Expand Down

0 comments on commit 26d20e1

Please sign in to comment.