Skip to content

Commit

Permalink
feat(core): implement repeated backup
Browse files Browse the repository at this point in the history
  • Loading branch information
ibz committed May 3, 2024
1 parent a4f8d2b commit 1e6ed37
Show file tree
Hide file tree
Showing 40 changed files with 713 additions and 163 deletions.
14 changes: 10 additions & 4 deletions common/protob/messages-management.proto
Original file line number Diff line number Diff line change
Expand Up @@ -427,16 +427,16 @@ message EntropyAck {
* @next WordRequest
*/
message RecoveryDevice {
optional uint32 word_count = 1; // number of words in BIP-39 mnemonic
optional uint32 word_count = 1; // number of words in BIP-39 mnemonic (T1 only)
optional bool passphrase_protection = 2; // enable master node encryption using passphrase
optional bool pin_protection = 3; // enable PIN protection
optional string language = 4 [deprecated=true]; // deprecated (use ChangeLanguage)
optional string label = 5; // device label
optional bool enforce_wordlist = 6; // enforce BIP-39 wordlist during the process
optional bool enforce_wordlist = 6; // enforce BIP-39 wordlist during the process (T1 only)
// 7 reserved for unused recovery method
optional RecoveryDeviceType type = 8; // supported recovery type
optional RecoveryDeviceType type = 8; // supported recovery type (T1 only)
optional uint32 u2f_counter = 9; // U2F counter
optional bool dry_run = 10; // perform dry-run recovery workflow (for safe mnemonic validation)
optional RecoveryKind kind = 10; // the kind of recovery to perform
/**
* Type of recovery procedure. These should be used as bitmask, e.g.,
* `RecoveryDeviceType_ScrambledWords | RecoveryDeviceType_Matrix`
Expand All @@ -450,6 +450,12 @@ message RecoveryDevice {
RecoveryDeviceType_ScrambledWords = 0; // words in scrambled order
RecoveryDeviceType_Matrix = 1; // matrix recovery type
}

enum RecoveryKind {
RecoveryKind_NormalRecovery = 0; // recovery from seedphrase on an uninitialized device
RecoveryKind_DryRun = 1; // mnemonic validation
RecoveryKind_UnlockRepeatedBackup = 2; // unlock SLIP-39 repeated backup
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions core/.changelog.d/3640.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added support for repeated backups.
3 changes: 3 additions & 0 deletions core/embed/rust/librust_qstr.h
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,10 @@ static void _librust_qstrs(void) {
MP_QSTR_recovery__title_dry_run;
MP_QSTR_recovery__title_recover;
MP_QSTR_recovery__title_remaining_shares;
MP_QSTR_recovery__title_unlock_repeated_backup;
MP_QSTR_recovery__type_word_x_of_y_template;
MP_QSTR_recovery__unlock_repeated_backup;
MP_QSTR_recovery__unlock_repeated_backup_verb;
MP_QSTR_recovery__wallet_recovered;
MP_QSTR_recovery__wanna_cancel_dry_run;
MP_QSTR_recovery__wanna_cancel_recovery;
Expand Down
17 changes: 13 additions & 4 deletions core/embed/rust/src/translations/generated/translated_string.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion core/embed/rust/src/ui/model_tt/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1439,7 +1439,7 @@ extern "C" fn new_select_word_count(n_args: usize, args: *const Obj, kwargs: *mu

let paragraphs = Paragraphs::new(Paragraph::new(
&theme::TEXT_DEMIBOLD,
TR::recovery__select_num_of_words,
TR::recovery__num_of_words,
));

let obj = LayoutObj::new(Frame::left_aligned(
Expand Down
4 changes: 3 additions & 1 deletion core/mocks/trezortranslate_keys.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,6 @@ class TR:
recovery__num_of_words: str = "Select the number of words in your backup."
recovery__only_first_n_letters: str = "You'll only have to select the first 2-4 letters of each word."
recovery__progress_will_be_lost: str = "All progress will be lost."
recovery__select_num_of_words: str = "Select the number of words in your backup."
recovery__share_already_entered: str = "Share already entered"
recovery__share_from_another_shamir: str = "You have entered a share from another Shamir Backup."
recovery__share_num_template: str = "Share {0}"
Expand All @@ -535,7 +534,10 @@ class TR:
recovery__title_dry_run: str = "BACKUP CHECK"
recovery__title_recover: str = "RECOVER WALLET"
recovery__title_remaining_shares: str = "REMAINING SHARES"
recovery__title_unlock_repeated_backup: str = "UNLOCK BACKUP"
recovery__type_word_x_of_y_template: str = "Type word {0} of {1}"
recovery__unlock_repeated_backup: str = "Do you want to unlock the backup?"
recovery__unlock_repeated_backup_verb: str = "Unlock backup"
recovery__wallet_recovered: str = "Wallet recovered successfully"
recovery__wanna_cancel_dry_run: str = "Are you sure you want to cancel the backup check?"
recovery__wanna_cancel_recovery: str = "Are you sure you want to cancel the recovery process?"
Expand Down
2 changes: 2 additions & 0 deletions core/src/all_modules.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 14 additions & 2 deletions core/src/apps/management/backup_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@


async def backup_device(msg: BackupDevice) -> Success:
import storage.cache as storage_cache
import storage.device as storage_device
from trezor import wire
from trezor.messages import Success
Expand All @@ -18,9 +19,16 @@ async def backup_device(msg: BackupDevice) -> Success:

from .reset_device import backup_seed, backup_slip39_custom, layout

# do this early before we show any UI
# the homescreen will clear the flag right after its own UI is gone
repeated_backup_unlocked = (
storage_cache.get(storage_cache.APP_RECOVERY_REPEATED_BACKUP_UNLOCKED)
== b"\x01"
)

if not storage_device.is_initialized():
raise wire.NotInitialized("Device is not initialized")
if not storage_device.needs_backup():
if not storage_device.needs_backup() and not repeated_backup_unlocked:
raise wire.ProcessError("Seed already backed up")

mnemonic_secret, backup_type = mnemonic.get()
Expand All @@ -40,7 +48,9 @@ async def backup_device(msg: BackupDevice) -> Success:
elif len(groups) > 0:
raise wire.DataError("group_threshold is missing")

storage_device.set_unfinished_backup(True)
if not repeated_backup_unlocked:
storage_device.set_unfinished_backup(True)

storage_device.set_backed_up()

if group_threshold is not None:
Expand All @@ -52,4 +62,6 @@ async def backup_device(msg: BackupDevice) -> Success:

await layout.show_backup_success()

storage_cache.delete(storage_cache.APP_RECOVERY_REPEATED_BACKUP_UNLOCKED)

return Success(message="Seed successfully backed up")
77 changes: 43 additions & 34 deletions core/src/apps/management/recovery_device/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from typing import TYPE_CHECKING

from trezor.enums import RecoveryKind

if TYPE_CHECKING:
from trezor.messages import RecoveryDevice, Success

# List of RecoveryDevice fields that can be set when doing dry-run recovery.
# All except `dry_run` are allowed for T1 compatibility, but their values are ignored.
# All except `kind` are allowed for T1 compatibility, but their values are ignored.
# If set, `enforce_wordlist` must be True, because we do not support non-enforcing.
DRY_RUN_ALLOWED_FIELDS = ("dry_run", "word_count", "enforce_wordlist", "type")
DRY_RUN_ALLOWED_FIELDS = ("kind", "word_count", "enforce_wordlist", "type")


async def recovery_device(msg: RecoveryDevice) -> Success:
Expand All @@ -31,68 +33,75 @@ async def recovery_device(msg: RecoveryDevice) -> Success:

from .homescreen import recovery_homescreen, recovery_process

dry_run = msg.dry_run # local_cache_attribute
recovery_kind = msg.kind or RecoveryKind.NormalRecovery # local_cache_attribute

# --------------------------------------------------------
# validate
if not dry_run and storage_device.is_initialized():
raise wire.UnexpectedMessage("Already initialized")
if dry_run and not storage_device.is_initialized():
raise wire.NotInitialized("Device is not initialized")
if recovery_kind == RecoveryKind.NormalRecovery:
if storage_device.is_initialized():
raise wire.UnexpectedMessage("Already initialized")
elif recovery_kind in (RecoveryKind.DryRun, RecoveryKind.UnlockRepeatedBackup):
if not storage_device.is_initialized():
raise wire.NotInitialized("Device is not initialized")
# check that only allowed fields are set
for key, value in msg.__dict__.items():
if key not in DRY_RUN_ALLOWED_FIELDS and value is not None:
raise wire.ProcessError(
f"Forbidden field set in DryRun or UnlockRepeatedBackup: {key}"
)
else:
raise ValueError("Unknown RecoveryKind")

if msg.enforce_wordlist is False:
raise wire.ProcessError(
"Value enforce_wordlist must be True, Trezor Core enforces words automatically."
)
if dry_run:
# check that only allowed fields are set
for key, value in msg.__dict__.items():
if key not in DRY_RUN_ALLOWED_FIELDS and value is not None:
raise wire.ProcessError(f"Forbidden field set in dry-run: {key}")
# END validate
# --------------------------------------------------------

if storage_recovery.is_in_progress():
return await recovery_process()

# --------------------------------------------------------
# _continue_dialog
if not dry_run:
if recovery_kind == RecoveryKind.NormalRecovery:
await confirm_reset_device(TR.recovery__title_recover, recovery=True)
else:
await confirm_action(
"confirm_seedcheck",
TR.recovery__title_dry_run,
description=TR.recovery__check_dry_run,
br_code=ButtonRequestType.ProtectCall,
verb=TR.buttons__check,
)
# END _continue_dialog
# --------------------------------------------------------

if not dry_run:
# wipe storage to make sure the device is in a clear state
storage.reset()

# for dry run pin needs to be entered
if dry_run:
curpin, salt = await request_pin_and_sd_salt(TR.pin__enter)
if not config.check_pin(curpin, salt):
await error_pin_invalid()

if not dry_run:
# set up pin if requested
if msg.pin_protection:
newpin = await request_pin_confirm(allow_cancel=False)
config.change_pin("", newpin, None, None)

storage_device.set_passphrase_enabled(bool(msg.passphrase_protection))

if msg.u2f_counter is not None:
storage_device.set_u2f_counter(msg.u2f_counter)

if msg.label is not None:
storage_device.set_label(msg.label)
elif recovery_kind in (RecoveryKind.DryRun, RecoveryKind.UnlockRepeatedBackup):
title = (
TR.recovery__title_dry_run
if recovery_kind == RecoveryKind.DryRun
else TR.recovery__title_unlock_repeated_backup
)
await confirm_action(
"confirm_seedcheck",
title,
description=TR.recovery__check_dry_run,
br_code=ButtonRequestType.ProtectCall,
verb=TR.buttons__check,
)

curpin, salt = await request_pin_and_sd_salt(TR.pin__enter)
if not config.check_pin(curpin, salt):
await error_pin_invalid()

storage_recovery.set_in_progress(True)
storage_recovery.set_dry_run(bool(dry_run))

storage_recovery.set_kind(int(recovery_kind))

workflow.set_default(recovery_homescreen)

return await recovery_process()
Loading

0 comments on commit 1e6ed37

Please sign in to comment.