From 3006224bff034a32dd3efb894e83e18695dcac46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ioan=20Biz=C4=83u?= Date: Thu, 11 Apr 2024 16:21:17 +0200 Subject: [PATCH] feat(core): add ability to request backups with any number of groups/shares --- common/protob/messages-management.proto | 6 + core/.changelog.d/3636.added | 1 + core/src/apps/management/backup_device.py | 27 +- .../apps/management/reset_device/__init__.py | 72 ++-- core/src/trezor/crypto/slip39.py | 6 +- core/src/trezor/messages.py | 26 ++ .../protob/messages-management.options | 2 + python/.changelog.d/3636.added | 1 + python/src/trezorlib/cli/device.py | 13 +- python/src/trezorlib/device.py | 18 +- python/src/trezorlib/messages.py | 30 ++ .../protos/generated/messages_management.rs | 337 +++++++++++++++--- tests/click_tests/reset.py | 7 + .../click_tests/test_backup_slip39_custom.py | 118 ++++++ tests/common.py | 2 + tests/device_tests/test_msg_backup_device.py | 33 ++ tests/input_flows.py | 63 +++- tests/ui_tests/fixtures.json | 35 +- 18 files changed, 705 insertions(+), 92 deletions(-) create mode 100644 core/.changelog.d/3636.added create mode 100644 python/.changelog.d/3636.added create mode 100644 tests/click_tests/test_backup_slip39_custom.py diff --git a/common/protob/messages-management.proto b/common/protob/messages-management.proto index b0e623310c..4c5c8f6003 100644 --- a/common/protob/messages-management.proto +++ b/common/protob/messages-management.proto @@ -397,6 +397,12 @@ message ResetDevice { * @next Success */ message BackupDevice { + optional uint32 group_threshold = 1; + message Slip39Group { + required uint32 member_threshold = 1; + required uint32 member_count = 2; + } + repeated Slip39Group groups = 2; } /** diff --git a/core/.changelog.d/3636.added b/core/.changelog.d/3636.added new file mode 100644 index 0000000000..2d5938422a --- /dev/null +++ b/core/.changelog.d/3636.added @@ -0,0 +1 @@ +Added ability to request Shamir backups with any number of groups/shares. diff --git a/core/src/apps/management/backup_device.py b/core/src/apps/management/backup_device.py index b4c37afc5c..95a60eb9ec 100644 --- a/core/src/apps/management/backup_device.py +++ b/core/src/apps/management/backup_device.py @@ -1,9 +1,14 @@ from typing import TYPE_CHECKING +from trezor.enums import BackupType + if TYPE_CHECKING: from trezor.messages import BackupDevice, Success +BAK_T_BIP39 = BackupType.Bip39 # global_import_cache + + async def backup_device(msg: BackupDevice) -> Success: import storage.device as storage_device from trezor import wire @@ -11,21 +16,37 @@ async def backup_device(msg: BackupDevice) -> Success: from apps.common import mnemonic - from .reset_device import backup_seed, layout + from .reset_device import backup_seed, backup_slip39_custom, layout if not storage_device.is_initialized(): raise wire.NotInitialized("Device is not initialized") if not storage_device.needs_backup(): raise wire.ProcessError("Seed already backed up") - mnemonic_secret, mnemonic_type = mnemonic.get() + 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] + + 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: + raise wire.ProcessError("Expected SLIP39 backup") + elif len(groups) > 0: + raise wire.DataError("group_threshold is missing") + storage_device.set_unfinished_backup(True) storage_device.set_backed_up() - await backup_seed(mnemonic_type, mnemonic_secret) + if group_threshold is not None: + await backup_slip39_custom(mnemonic_secret, group_threshold, groups) + else: + await backup_seed(backup_type, mnemonic_secret) storage_device.set_unfinished_backup(False) diff --git a/core/src/apps/management/reset_device/__init__.py b/core/src/apps/management/reset_device/__init__.py index 9f24d167a6..d429480c05 100644 --- a/core/src/apps/management/reset_device/__init__.py +++ b/core/src/apps/management/reset_device/__init__.py @@ -1,9 +1,11 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Sequence import storage import storage.device as storage_device +from trezor import TR from trezor.crypto import slip39 from trezor.enums import BackupType +from trezor.ui.layouts import confirm_action from trezor.wire import ProcessError from . import layout @@ -111,31 +113,19 @@ async def reset_device(msg: ResetDevice) -> Success: async def _backup_slip39_basic(encrypted_master_secret: bytes) -> None: + group_threshold = 1 + # get number of shares await layout.slip39_show_checklist(0, BAK_T_SLIP39_BASIC) - shares_count = await layout.slip39_prompt_number_of_shares() + share_count = await layout.slip39_prompt_number_of_shares() # get threshold await layout.slip39_show_checklist(1, BAK_T_SLIP39_BASIC) - threshold = await layout.slip39_prompt_threshold(shares_count) + share_threshold = await layout.slip39_prompt_threshold(share_count) - identifier = storage_device.get_slip39_identifier() - iteration_exponent = storage_device.get_slip39_iteration_exponent() - if identifier is None or iteration_exponent is None: - raise ValueError - - # generate the mnemonics - mnemonics = slip39.split_ems( - 1, # Single Group threshold - [(threshold, shares_count)], # Single Group threshold/count - identifier, - iteration_exponent, - encrypted_master_secret, - )[0] - - # show and confirm individual shares - await layout.slip39_show_checklist(2, BAK_T_SLIP39_BASIC) - await layout.slip39_basic_show_and_confirm_shares(mnemonics) + await backup_slip39_custom( + encrypted_master_secret, group_threshold, ((share_threshold, share_count),) + ) async def _backup_slip39_advanced(encrypted_master_secret: bytes) -> None: @@ -155,6 +145,40 @@ async def _backup_slip39_advanced(encrypted_master_secret: bytes) -> None: share_threshold = await layout.slip39_prompt_threshold(share_count, i) groups.append((share_threshold, share_count)) + await backup_slip39_custom(encrypted_master_secret, group_threshold, groups) + + +async def backup_slip39_custom( + encrypted_master_secret: bytes, + group_threshold: int, + groups: Sequence[tuple[int, int]], +) -> None: + mnemonics = await _get_slip39_mnemonics(encrypted_master_secret, group_threshold, groups) + + # show and confirm individual shares + if len(groups) == 1 and groups[0][0] == 1 and groups[0][1] == 1: + # for a single 1-of-1 group, we use the same layouts as for BIP39 + await layout.show_and_confirm_mnemonic(mnemonics[0][0]) + else: + await confirm_action( + "warning_shamir_backup", + TR.reset__title_shamir_backup, + description=TR.reset__create_x_of_y_shamir_backup_template.format( + groups[0][0], groups[0][1] + ), + verb=TR.buttons__continue, + ) + if len(groups) == 1: + await layout.slip39_basic_show_and_confirm_shares(mnemonics[0]) + else: + await layout.slip39_advanced_show_and_confirm_shares(mnemonics) + + +async def _get_slip39_mnemonics( + encrypted_master_secret: bytes, + group_threshold: int, + groups: Sequence[tuple[int, int]], +): identifier = storage_device.get_slip39_identifier() iteration_exponent = storage_device.get_slip39_iteration_exponent() if identifier is None or iteration_exponent is None: @@ -170,7 +194,11 @@ async def _backup_slip39_advanced(encrypted_master_secret: bytes) -> None: ) # show and confirm individual shares - await layout.slip39_advanced_show_and_confirm_shares(mnemonics) + if len(groups) == 1: + await layout.slip39_show_checklist(2, BAK_T_SLIP39_BASIC) + await layout.slip39_basic_show_and_confirm_shares(mnemonics[0]) + else: + await layout.slip39_advanced_show_and_confirm_shares(mnemonics) def _validate_reset_device(msg: ResetDevice) -> None: @@ -184,7 +212,7 @@ def _validate_reset_device(msg: ResetDevice) -> None: BAK_T_SLIP39_BASIC, BAK_T_SLIP39_ADVANCED, ): - raise ProcessError("Backup type not implemented.") + raise ProcessError("Backup type not implemented") if backup_types.is_slip39_backup_type(backup_type): if msg.strength not in (128, 256): raise ProcessError("Invalid strength (has to be 128 or 256 bits)") diff --git a/core/src/trezor/crypto/slip39.py b/core/src/trezor/crypto/slip39.py index 304f4bdeef..b2ee985979 100644 --- a/core/src/trezor/crypto/slip39.py +++ b/core/src/trezor/crypto/slip39.py @@ -38,7 +38,7 @@ from trezor.crypto import random from trezor.errors import MnemonicError if TYPE_CHECKING: - from typing import Callable, Iterable + from typing import Callable, Collection, Iterable Indices = tuple[int, ...] MnemonicGroups = dict[int, tuple[int, set[tuple[int, bytes]]]] @@ -174,7 +174,9 @@ def generate_random_identifier() -> int: def split_ems( group_threshold: int, # The number of groups required to reconstruct the master secret. - groups: list[tuple[int, int]], # A list of (member_threshold, member_count). + groups: Collection[ + tuple[int, int] + ], # A collection of (member_threshold, member_count). identifier: int, iteration_exponent: int, encrypted_master_secret: bytes, # The encrypted master secret to split. diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py index a4d6b4ee21..0335bb53da 100644 --- a/core/src/trezor/messages.py +++ b/core/src/trezor/messages.py @@ -2528,6 +2528,16 @@ if TYPE_CHECKING: return isinstance(msg, cls) class BackupDevice(protobuf.MessageType): + group_threshold: "int | None" + groups: "list[Slip39Group]" + + def __init__( + self, + *, + groups: "list[Slip39Group] | None" = None, + group_threshold: "int | None" = None, + ) -> None: + pass @classmethod def is_type_of(cls, msg: Any) -> TypeGuard["BackupDevice"]: @@ -2741,6 +2751,22 @@ if TYPE_CHECKING: def is_type_of(cls, msg: Any) -> TypeGuard["UnlockBootloader"]: return isinstance(msg, cls) + class Slip39Group(protobuf.MessageType): + member_threshold: "int" + member_count: "int" + + def __init__( + self, + *, + member_threshold: "int", + member_count: "int", + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: Any) -> TypeGuard["Slip39Group"]: + return isinstance(msg, cls) + class DebugLinkDecision(protobuf.MessageType): button: "DebugButton | None" swipe: "DebugSwipeDirection | None" diff --git a/legacy/firmware/protob/messages-management.options b/legacy/firmware/protob/messages-management.options index bd28e3f444..cb1000258a 100644 --- a/legacy/firmware/protob/messages-management.options +++ b/legacy/firmware/protob/messages-management.options @@ -26,6 +26,8 @@ LoadDevice.label max_size:33 ResetDevice.language max_size:17 ResetDevice.label max_size:33 +BackupDevice.groups type:FT_IGNORE + Entropy.entropy max_size:1024 EntropyAck.entropy max_size:128 diff --git a/python/.changelog.d/3636.added b/python/.changelog.d/3636.added new file mode 100644 index 0000000000..2d5938422a --- /dev/null +++ b/python/.changelog.d/3636.added @@ -0,0 +1 @@ +Added ability to request Shamir backups with any number of groups/shares. diff --git a/python/src/trezorlib/cli/device.py b/python/src/trezorlib/cli/device.py index e8d365da1e..1949749f64 100644 --- a/python/src/trezorlib/cli/device.py +++ b/python/src/trezorlib/cli/device.py @@ -16,7 +16,7 @@ import secrets import sys -from typing import TYPE_CHECKING, Optional, Sequence +from typing import TYPE_CHECKING, Optional, Sequence, Tuple import click @@ -237,10 +237,17 @@ def setup( @cli.command() +@click.option("-t", "--group-threshold", type=int) +@click.option("-g", "--group", "groups", type=(int, int), multiple=True, metavar="T N") @with_client -def backup(client: "TrezorClient") -> str: +def backup( + client: "TrezorClient", + group_threshold: Optional[int] = None, + groups: Sequence[Tuple[int, int]] = (), +) -> str: """Perform device seed backup.""" - return device.backup(client) + + return device.backup(client, group_threshold, groups) @cli.command() diff --git a/python/src/trezorlib/device.py b/python/src/trezorlib/device.py index dbea87e808..bdebdc06b6 100644 --- a/python/src/trezorlib/device.py +++ b/python/src/trezorlib/device.py @@ -19,7 +19,7 @@ from __future__ import annotations import os import time import warnings -from typing import TYPE_CHECKING, Callable, Optional +from typing import TYPE_CHECKING, Callable, Iterable, Optional from . import messages from .exceptions import Cancelled, TrezorException @@ -264,8 +264,20 @@ def reset( @expect(messages.Success, field="message", ret_type=str) @session -def backup(client: "TrezorClient") -> "MessageType": - ret = client.call(messages.BackupDevice()) +def backup( + client: "TrezorClient", + group_threshold: Optional[int] = None, + groups: Iterable[tuple[int, int]] = (), +) -> "MessageType": + ret = client.call( + messages.BackupDevice( + group_threshold=group_threshold, + groups=[ + messages.Slip39Group(member_threshold=t, member_count=c) + for t, c in groups + ], + ) + ) client.refresh_features() return ret diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py index d52619a5e3..b5f52bce1f 100644 --- a/python/src/trezorlib/messages.py +++ b/python/src/trezorlib/messages.py @@ -3688,6 +3688,19 @@ class ResetDevice(protobuf.MessageType): class BackupDevice(protobuf.MessageType): MESSAGE_WIRE_TYPE = 34 + FIELDS = { + 1: protobuf.Field("group_threshold", "uint32", repeated=False, required=False, default=None), + 2: protobuf.Field("groups", "Slip39Group", repeated=True, required=False, default=None), + } + + def __init__( + self, + *, + groups: Optional[Sequence["Slip39Group"]] = None, + group_threshold: Optional["int"] = None, + ) -> None: + self.groups: Sequence["Slip39Group"] = groups if groups is not None else [] + self.group_threshold = group_threshold class EntropyRequest(protobuf.MessageType): @@ -3895,6 +3908,23 @@ class UnlockBootloader(protobuf.MessageType): MESSAGE_WIRE_TYPE = 96 +class Slip39Group(protobuf.MessageType): + MESSAGE_WIRE_TYPE = None + FIELDS = { + 1: protobuf.Field("member_threshold", "uint32", repeated=False, required=True), + 2: protobuf.Field("member_count", "uint32", repeated=False, required=True), + } + + def __init__( + self, + *, + member_threshold: "int", + member_count: "int", + ) -> None: + self.member_threshold = member_threshold + self.member_count = member_count + + class DebugLinkDecision(protobuf.MessageType): MESSAGE_WIRE_TYPE = 100 FIELDS = { diff --git a/rust/trezor-client/src/protos/generated/messages_management.rs b/rust/trezor-client/src/protos/generated/messages_management.rs index 3fb8023261..799111f5a8 100644 --- a/rust/trezor-client/src/protos/generated/messages_management.rs +++ b/rust/trezor-client/src/protos/generated/messages_management.rs @@ -7049,6 +7049,11 @@ impl ::protobuf::reflect::ProtobufValue for ResetDevice { // @@protoc_insertion_point(message:hw.trezor.messages.management.BackupDevice) #[derive(PartialEq,Clone,Default,Debug)] pub struct BackupDevice { + // message fields + // @@protoc_insertion_point(field:hw.trezor.messages.management.BackupDevice.group_threshold) + pub group_threshold: ::std::option::Option, + // @@protoc_insertion_point(field:hw.trezor.messages.management.BackupDevice.groups) + pub groups: ::std::vec::Vec, // special fields // @@protoc_insertion_point(special_field:hw.trezor.messages.management.BackupDevice.special_fields) pub special_fields: ::protobuf::SpecialFields, @@ -7065,9 +7070,38 @@ impl BackupDevice { ::std::default::Default::default() } + // optional uint32 group_threshold = 1; + + pub fn group_threshold(&self) -> u32 { + self.group_threshold.unwrap_or(0) + } + + pub fn clear_group_threshold(&mut self) { + self.group_threshold = ::std::option::Option::None; + } + + pub fn has_group_threshold(&self) -> bool { + self.group_threshold.is_some() + } + + // Param is passed by value, moved + pub fn set_group_threshold(&mut self, v: u32) { + self.group_threshold = ::std::option::Option::Some(v); + } + fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(0); + let mut fields = ::std::vec::Vec::with_capacity(2); let mut oneofs = ::std::vec::Vec::with_capacity(0); + fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( + "group_threshold", + |m: &BackupDevice| { &m.group_threshold }, + |m: &mut BackupDevice| { &mut m.group_threshold }, + )); + fields.push(::protobuf::reflect::rt::v2::make_vec_simpler_accessor::<_, _>( + "groups", + |m: &BackupDevice| { &m.groups }, + |m: &mut BackupDevice| { &mut m.groups }, + )); ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( "BackupDevice", fields, @@ -7080,12 +7114,23 @@ impl ::protobuf::Message for BackupDevice { const NAME: &'static str = "BackupDevice"; fn is_initialized(&self) -> bool { + for v in &self.groups { + if !v.is_initialized() { + return false; + } + }; true } fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { while let Some(tag) = is.read_raw_tag_or_eof()? { match tag { + 8 => { + self.group_threshold = ::std::option::Option::Some(is.read_uint32()?); + }, + 18 => { + self.groups.push(is.read_message()?); + }, tag => { ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; }, @@ -7098,12 +7143,25 @@ impl ::protobuf::Message for BackupDevice { #[allow(unused_variables)] fn compute_size(&self) -> u64 { let mut my_size = 0; + if let Some(v) = self.group_threshold { + my_size += ::protobuf::rt::uint32_size(1, v); + } + for value in &self.groups { + let len = value.compute_size(); + my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; + }; my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); self.special_fields.cached_size().set(my_size as u32); my_size } fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { + if let Some(v) = self.group_threshold { + os.write_uint32(1, v)?; + } + for v in &self.groups { + ::protobuf::rt::write_message_field_with_cached_size(2, v, os)?; + }; os.write_unknown_fields(self.special_fields.unknown_fields())?; ::std::result::Result::Ok(()) } @@ -7121,11 +7179,15 @@ impl ::protobuf::Message for BackupDevice { } fn clear(&mut self) { + self.group_threshold = ::std::option::Option::None; + self.groups.clear(); self.special_fields.clear(); } fn default_instance() -> &'static BackupDevice { static instance: BackupDevice = BackupDevice { + group_threshold: ::std::option::Option::None, + groups: ::std::vec::Vec::new(), special_fields: ::protobuf::SpecialFields::new(), }; &instance @@ -7149,6 +7211,193 @@ impl ::protobuf::reflect::ProtobufValue for BackupDevice { type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; } +/// Nested message and enums of message `BackupDevice` +pub mod backup_device { + // @@protoc_insertion_point(message:hw.trezor.messages.management.BackupDevice.Slip39Group) + #[derive(PartialEq,Clone,Default,Debug)] + pub struct Slip39Group { + // message fields + // @@protoc_insertion_point(field:hw.trezor.messages.management.BackupDevice.Slip39Group.member_threshold) + pub member_threshold: ::std::option::Option, + // @@protoc_insertion_point(field:hw.trezor.messages.management.BackupDevice.Slip39Group.member_count) + pub member_count: ::std::option::Option, + // special fields + // @@protoc_insertion_point(special_field:hw.trezor.messages.management.BackupDevice.Slip39Group.special_fields) + pub special_fields: ::protobuf::SpecialFields, + } + + impl<'a> ::std::default::Default for &'a Slip39Group { + fn default() -> &'a Slip39Group { + ::default_instance() + } + } + + impl Slip39Group { + pub fn new() -> Slip39Group { + ::std::default::Default::default() + } + + // required uint32 member_threshold = 1; + + pub fn member_threshold(&self) -> u32 { + self.member_threshold.unwrap_or(0) + } + + pub fn clear_member_threshold(&mut self) { + self.member_threshold = ::std::option::Option::None; + } + + pub fn has_member_threshold(&self) -> bool { + self.member_threshold.is_some() + } + + // Param is passed by value, moved + pub fn set_member_threshold(&mut self, v: u32) { + self.member_threshold = ::std::option::Option::Some(v); + } + + // required uint32 member_count = 2; + + pub fn member_count(&self) -> u32 { + self.member_count.unwrap_or(0) + } + + pub fn clear_member_count(&mut self) { + self.member_count = ::std::option::Option::None; + } + + pub fn has_member_count(&self) -> bool { + self.member_count.is_some() + } + + // Param is passed by value, moved + pub fn set_member_count(&mut self, v: u32) { + self.member_count = ::std::option::Option::Some(v); + } + + pub(in super) fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { + let mut fields = ::std::vec::Vec::with_capacity(2); + let mut oneofs = ::std::vec::Vec::with_capacity(0); + fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( + "member_threshold", + |m: &Slip39Group| { &m.member_threshold }, + |m: &mut Slip39Group| { &mut m.member_threshold }, + )); + fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( + "member_count", + |m: &Slip39Group| { &m.member_count }, + |m: &mut Slip39Group| { &mut m.member_count }, + )); + ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( + "BackupDevice.Slip39Group", + fields, + oneofs, + ) + } + } + + impl ::protobuf::Message for Slip39Group { + const NAME: &'static str = "Slip39Group"; + + fn is_initialized(&self) -> bool { + if self.member_threshold.is_none() { + return false; + } + if self.member_count.is_none() { + return false; + } + true + } + + fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { + while let Some(tag) = is.read_raw_tag_or_eof()? { + match tag { + 8 => { + self.member_threshold = ::std::option::Option::Some(is.read_uint32()?); + }, + 16 => { + self.member_count = ::std::option::Option::Some(is.read_uint32()?); + }, + tag => { + ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; + }, + }; + } + ::std::result::Result::Ok(()) + } + + // Compute sizes of nested messages + #[allow(unused_variables)] + fn compute_size(&self) -> u64 { + let mut my_size = 0; + if let Some(v) = self.member_threshold { + my_size += ::protobuf::rt::uint32_size(1, v); + } + if let Some(v) = self.member_count { + my_size += ::protobuf::rt::uint32_size(2, v); + } + my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); + self.special_fields.cached_size().set(my_size as u32); + my_size + } + + fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { + if let Some(v) = self.member_threshold { + os.write_uint32(1, v)?; + } + if let Some(v) = self.member_count { + os.write_uint32(2, v)?; + } + os.write_unknown_fields(self.special_fields.unknown_fields())?; + ::std::result::Result::Ok(()) + } + + fn special_fields(&self) -> &::protobuf::SpecialFields { + &self.special_fields + } + + fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { + &mut self.special_fields + } + + fn new() -> Slip39Group { + Slip39Group::new() + } + + fn clear(&mut self) { + self.member_threshold = ::std::option::Option::None; + self.member_count = ::std::option::Option::None; + self.special_fields.clear(); + } + + fn default_instance() -> &'static Slip39Group { + static instance: Slip39Group = Slip39Group { + member_threshold: ::std::option::Option::None, + member_count: ::std::option::Option::None, + special_fields: ::protobuf::SpecialFields::new(), + }; + &instance + } + } + + impl ::protobuf::MessageFull for Slip39Group { + fn descriptor() -> ::protobuf::reflect::MessageDescriptor { + static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); + descriptor.get(|| super::file_descriptor().message_by_package_relative_name("BackupDevice.Slip39Group").unwrap()).clone() + } + } + + impl ::std::fmt::Display for Slip39Group { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + ::protobuf::text_format::fmt(self, f) + } + } + + impl ::protobuf::reflect::ProtobufValue for Slip39Group { + type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; + } +} + // @@protoc_insertion_point(message:hw.trezor.messages.management.EntropyRequest) #[derive(PartialEq,Clone,Default,Debug)] pub struct EntropyRequest { @@ -10458,45 +10707,50 @@ static file_descriptor_proto_data: &'static [u8] = b"\ \n\x0bu2f_counter\x18\x07\x20\x01(\rR\nu2fCounter\x12\x1f\n\x0bskip_back\ up\x18\x08\x20\x01(\x08R\nskipBackup\x12\x1b\n\tno_backup\x18\t\x20\x01(\ \x08R\x08noBackup\x12Q\n\x0bbackup_type\x18\n\x20\x01(\x0e2).hw.trezor.m\ - essages.management.BackupType:\x05Bip39R\nbackupType\"\x0e\n\x0cBackupDe\ - vice\"\x10\n\x0eEntropyRequest\"&\n\nEntropyAck\x12\x18\n\x07entropy\x18\ - \x01\x20\x02(\x0cR\x07entropy\"\xd8\x03\n\x0eRecoveryDevice\x12\x1d\n\nw\ - ord_count\x18\x01\x20\x01(\rR\twordCount\x123\n\x15passphrase_protection\ - \x18\x02\x20\x01(\x08R\x14passphraseProtection\x12%\n\x0epin_protection\ - \x18\x03\x20\x01(\x08R\rpinProtection\x12\x1e\n\x08language\x18\x04\x20\ - \x01(\tR\x08languageB\x02\x18\x01\x12\x14\n\x05label\x18\x05\x20\x01(\tR\ - \x05label\x12)\n\x10enforce_wordlist\x18\x06\x20\x01(\x08R\x0fenforceWor\ - dlist\x12T\n\x04type\x18\x08\x20\x01(\x0e2@.hw.trezor.messages.managemen\ - t.RecoveryDevice.RecoveryDeviceTypeR\x04type\x12\x1f\n\x0bu2f_counter\ - \x18\t\x20\x01(\rR\nu2fCounter\x12\x17\n\x07dry_run\x18\n\x20\x01(\x08R\ - \x06dryRun\"Z\n\x12RecoveryDeviceType\x12%\n!RecoveryDeviceType_Scramble\ - dWords\x10\0\x12\x1d\n\x19RecoveryDeviceType_Matrix\x10\x01\"\xc5\x01\n\ - \x0bWordRequest\x12N\n\x04type\x18\x01\x20\x02(\x0e2:.hw.trezor.messages\ - .management.WordRequest.WordRequestTypeR\x04type\"f\n\x0fWordRequestType\ - \x12\x19\n\x15WordRequestType_Plain\x10\0\x12\x1b\n\x17WordRequestType_M\ - atrix9\x10\x01\x12\x1b\n\x17WordRequestType_Matrix6\x10\x02\"\x1d\n\x07W\ - ordAck\x12\x12\n\x04word\x18\x01\x20\x02(\tR\x04word\"0\n\rSetU2FCounter\ - \x12\x1f\n\x0bu2f_counter\x18\x01\x20\x02(\rR\nu2fCounter\"\x13\n\x11Get\ - NextU2FCounter\"1\n\x0eNextU2FCounter\x12\x1f\n\x0bu2f_counter\x18\x01\ - \x20\x02(\rR\nu2fCounter\"\x11\n\x0fDoPreauthorized\"\x16\n\x14Preauthor\ - izedRequest\"\x15\n\x13CancelAuthorization\"\x9a\x02\n\x12RebootToBootlo\ - ader\x12o\n\x0cboot_command\x18\x01\x20\x01(\x0e2=.hw.trezor.messages.ma\ - nagement.RebootToBootloader.BootCommand:\rSTOP_AND_WAITR\x0bbootCommand\ - \x12'\n\x0ffirmware_header\x18\x02\x20\x01(\x0cR\x0efirmwareHeader\x123\ - \n\x14language_data_length\x18\x03\x20\x01(\r:\x010R\x12languageDataLeng\ - th\"5\n\x0bBootCommand\x12\x11\n\rSTOP_AND_WAIT\x10\0\x12\x13\n\x0fINSTA\ - LL_UPGRADE\x10\x01\"\x10\n\x08GetNonce:\x04\x88\xb2\x19\x01\"#\n\x05Nonc\ - e\x12\x14\n\x05nonce\x18\x01\x20\x02(\x0cR\x05nonce:\x04\x88\xb2\x19\x01\ - \";\n\nUnlockPath\x12\x1b\n\taddress_n\x18\x01\x20\x03(\rR\x08addressN\ - \x12\x10\n\x03mac\x18\x02\x20\x01(\x0cR\x03mac\"'\n\x13UnlockedPathReque\ - st\x12\x10\n\x03mac\x18\x01\x20\x01(\x0cR\x03mac\"\x14\n\x12ShowDeviceTu\ - torial\"\x12\n\x10UnlockBootloader*>\n\nBackupType\x12\t\n\x05Bip39\x10\ - \0\x12\x10\n\x0cSlip39_Basic\x10\x01\x12\x13\n\x0fSlip39_Advanced\x10\ - \x02*G\n\x10SafetyCheckLevel\x12\n\n\x06Strict\x10\0\x12\x10\n\x0cPrompt\ - Always\x10\x01\x12\x15\n\x11PromptTemporarily\x10\x02*0\n\x10HomescreenF\ - ormat\x12\x08\n\x04Toif\x10\x01\x12\x08\n\x04Jpeg\x10\x02\x12\x08\n\x04T\ - oiG\x10\x03BB\n#com.satoshilabs.trezor.lib.protobufB\x17TrezorMessageMan\ - agement\x80\xa6\x1d\x01\ + essages.management.BackupType:\x05Bip39R\nbackupType\"\xe5\x01\n\x0cBack\ + upDevice\x12'\n\x0fgroup_threshold\x18\x01\x20\x01(\rR\x0egroupThreshold\ + \x12O\n\x06groups\x18\x02\x20\x03(\x0b27.hw.trezor.messages.management.B\ + ackupDevice.Slip39GroupR\x06groups\x1a[\n\x0bSlip39Group\x12)\n\x10membe\ + r_threshold\x18\x01\x20\x02(\rR\x0fmemberThreshold\x12!\n\x0cmember_coun\ + t\x18\x02\x20\x02(\rR\x0bmemberCount\"\x10\n\x0eEntropyRequest\"&\n\nEnt\ + ropyAck\x12\x18\n\x07entropy\x18\x01\x20\x02(\x0cR\x07entropy\"\xd8\x03\ + \n\x0eRecoveryDevice\x12\x1d\n\nword_count\x18\x01\x20\x01(\rR\twordCoun\ + t\x123\n\x15passphrase_protection\x18\x02\x20\x01(\x08R\x14passphrasePro\ + tection\x12%\n\x0epin_protection\x18\x03\x20\x01(\x08R\rpinProtection\ + \x12\x1e\n\x08language\x18\x04\x20\x01(\tR\x08languageB\x02\x18\x01\x12\ + \x14\n\x05label\x18\x05\x20\x01(\tR\x05label\x12)\n\x10enforce_wordlist\ + \x18\x06\x20\x01(\x08R\x0fenforceWordlist\x12T\n\x04type\x18\x08\x20\x01\ + (\x0e2@.hw.trezor.messages.management.RecoveryDevice.RecoveryDeviceTypeR\ + \x04type\x12\x1f\n\x0bu2f_counter\x18\t\x20\x01(\rR\nu2fCounter\x12\x17\ + \n\x07dry_run\x18\n\x20\x01(\x08R\x06dryRun\"Z\n\x12RecoveryDeviceType\ + \x12%\n!RecoveryDeviceType_ScrambledWords\x10\0\x12\x1d\n\x19RecoveryDev\ + iceType_Matrix\x10\x01\"\xc5\x01\n\x0bWordRequest\x12N\n\x04type\x18\x01\ + \x20\x02(\x0e2:.hw.trezor.messages.management.WordRequest.WordRequestTyp\ + eR\x04type\"f\n\x0fWordRequestType\x12\x19\n\x15WordRequestType_Plain\ + \x10\0\x12\x1b\n\x17WordRequestType_Matrix9\x10\x01\x12\x1b\n\x17WordReq\ + uestType_Matrix6\x10\x02\"\x1d\n\x07WordAck\x12\x12\n\x04word\x18\x01\ + \x20\x02(\tR\x04word\"0\n\rSetU2FCounter\x12\x1f\n\x0bu2f_counter\x18\ + \x01\x20\x02(\rR\nu2fCounter\"\x13\n\x11GetNextU2FCounter\"1\n\x0eNextU2\ + FCounter\x12\x1f\n\x0bu2f_counter\x18\x01\x20\x02(\rR\nu2fCounter\"\x11\ + \n\x0fDoPreauthorized\"\x16\n\x14PreauthorizedRequest\"\x15\n\x13CancelA\ + uthorization\"\x9a\x02\n\x12RebootToBootloader\x12o\n\x0cboot_command\ + \x18\x01\x20\x01(\x0e2=.hw.trezor.messages.management.RebootToBootloader\ + .BootCommand:\rSTOP_AND_WAITR\x0bbootCommand\x12'\n\x0ffirmware_header\ + \x18\x02\x20\x01(\x0cR\x0efirmwareHeader\x123\n\x14language_data_length\ + \x18\x03\x20\x01(\r:\x010R\x12languageDataLength\"5\n\x0bBootCommand\x12\ + \x11\n\rSTOP_AND_WAIT\x10\0\x12\x13\n\x0fINSTALL_UPGRADE\x10\x01\"\x10\n\ + \x08GetNonce:\x04\x88\xb2\x19\x01\"#\n\x05Nonce\x12\x14\n\x05nonce\x18\ + \x01\x20\x02(\x0cR\x05nonce:\x04\x88\xb2\x19\x01\";\n\nUnlockPath\x12\ + \x1b\n\taddress_n\x18\x01\x20\x03(\rR\x08addressN\x12\x10\n\x03mac\x18\ + \x02\x20\x01(\x0cR\x03mac\"'\n\x13UnlockedPathRequest\x12\x10\n\x03mac\ + \x18\x01\x20\x01(\x0cR\x03mac\"\x14\n\x12ShowDeviceTutorial\"\x12\n\x10U\ + nlockBootloader*>\n\nBackupType\x12\t\n\x05Bip39\x10\0\x12\x10\n\x0cSlip\ + 39_Basic\x10\x01\x12\x13\n\x0fSlip39_Advanced\x10\x02*G\n\x10SafetyCheck\ + Level\x12\n\n\x06Strict\x10\0\x12\x10\n\x0cPromptAlways\x10\x01\x12\x15\ + \n\x11PromptTemporarily\x10\x02*0\n\x10HomescreenFormat\x12\x08\n\x04Toi\ + f\x10\x01\x12\x08\n\x04Jpeg\x10\x02\x12\x08\n\x04ToiG\x10\x03BB\n#com.sa\ + toshilabs.trezor.lib.protobufB\x17TrezorMessageManagement\x80\xa6\x1d\ + \x01\ "; /// `FileDescriptorProto` object which was a source for this generated file @@ -10515,7 +10769,7 @@ pub fn file_descriptor() -> &'static ::protobuf::reflect::FileDescriptor { let generated_file_descriptor = generated_file_descriptor_lazy.get(|| { let mut deps = ::std::vec::Vec::with_capacity(1); deps.push(super::messages::file_descriptor().clone()); - let mut messages = ::std::vec::Vec::with_capacity(44); + let mut messages = ::std::vec::Vec::with_capacity(45); messages.push(Initialize::generated_message_descriptor_data()); messages.push(GetFeatures::generated_message_descriptor_data()); messages.push(Features::generated_message_descriptor_data()); @@ -10560,6 +10814,7 @@ pub fn file_descriptor() -> &'static ::protobuf::reflect::FileDescriptor { messages.push(UnlockedPathRequest::generated_message_descriptor_data()); messages.push(ShowDeviceTutorial::generated_message_descriptor_data()); messages.push(UnlockBootloader::generated_message_descriptor_data()); + messages.push(backup_device::Slip39Group::generated_message_descriptor_data()); let mut enums = ::std::vec::Vec::with_capacity(8); enums.push(BackupType::generated_enum_descriptor_data()); enums.push(SafetyCheckLevel::generated_enum_descriptor_data()); diff --git a/tests/click_tests/reset.py b/tests/click_tests/reset.py index 72260ca7e4..3650b88091 100644 --- a/tests/click_tests/reset.py +++ b/tests/click_tests/reset.py @@ -38,6 +38,13 @@ def confirm_read(debug: "DebugLink", middle_r: bool = False) -> None: debug.press_right(wait=True) +def cancel_backup(debug: "DebugLink", middle_r: bool = False) -> None: + if debug.model in (models.T2T1, models.T3T1): + debug.click(buttons.CANCEL, wait=True) + elif debug.model in (models.T2B1,): + debug.press_left(wait=True) + + def set_selection(debug: "DebugLink", button: tuple[int, int], diff: int) -> None: if debug.model in (models.T2T1, models.T3T1): assert "NumberInputDialog" in debug.read_layout().all_components() diff --git a/tests/click_tests/test_backup_slip39_custom.py b/tests/click_tests/test_backup_slip39_custom.py new file mode 100644 index 0000000000..56f62be1c1 --- /dev/null +++ b/tests/click_tests/test_backup_slip39_custom.py @@ -0,0 +1,118 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2024 SatoshiLabs and contributors +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the License along with this library. +# If not, see . + +from typing import TYPE_CHECKING + +import pytest + +from trezorlib import device, messages + +from ..common import EXTERNAL_ENTROPY, WITH_MOCK_URANDOM, generate_entropy +from . import reset + +if TYPE_CHECKING: + from ..device_handler import BackgroundDeviceHandler + + +pytestmark = [pytest.mark.skip_t1b1] + + +@pytest.mark.parametrize( + "group_threshold, share_threshold, share_count", + [ + pytest.param(1, 1, 1, id="1of1"), + pytest.param(1, 2, 3, id="2of3"), + pytest.param(1, 5, 5, id="5of5"), + ], +) +@pytest.mark.setup_client(uninitialized=True) +@WITH_MOCK_URANDOM +def test_backup_slip39_custom( + device_handler: "BackgroundDeviceHandler", + group_threshold: int, + share_threshold: int, + share_count: int, +): + features = device_handler.features() + debug = device_handler.debuglink() + + assert features.initialized is False + + device_handler.run( + device.reset, + strength=128, + backup_type=messages.BackupType.Slip39_Basic, + pin_protection=False, + ) + + # confirm new wallet + reset.confirm_new_wallet(debug) + + # cancel back up + reset.cancel_backup(debug) + + # confirm cancel + reset.cancel_backup(debug) + + assert device_handler.result() == "Initialized" + + device_handler.run( + device.backup, + group_threshold=group_threshold, + groups=[(share_threshold, share_count)], + ) + + # confirm checklist + reset.confirm_read(debug) + + # confirm backup warning + reset.confirm_read(debug, middle_r=True) + + if share_count > 1: + # confirm shamir warning + reset.confirm_read(debug, middle_r=True) + + all_words: list[str] = [] + for _ in range(share_count): + # read words + words = reset.read_words(debug, messages.BackupType.Slip39_Basic) + + # confirm words + reset.confirm_words(debug, words) + + # confirm share checked + reset.confirm_read(debug) + + all_words.append(" ".join(words)) + + # confirm backup done + reset.confirm_read(debug) + + # generate secret locally + internal_entropy = debug.state().reset_entropy + assert internal_entropy is not None + secret = generate_entropy(128, internal_entropy, EXTERNAL_ENTROPY) + + # validate that all combinations will result in the correct master secret + reset.validate_mnemonics(all_words[:share_threshold], secret) + + assert device_handler.result() == "Seed successfully backed up" + features = device_handler.features() + assert features.initialized is True + assert features.needs_backup is False + assert features.pin_protection is False + assert features.passphrase_protection is False + assert features.backup_type is messages.BackupType.Slip39_Basic diff --git a/tests/common.py b/tests/common.py index 6061008959..be00dff91c 100644 --- a/tests/common.py +++ b/tests/common.py @@ -58,6 +58,8 @@ MNEMONIC_SLIP39_ADVANCED_33 = [ "wildlife deal beard romp alcohol space mild usual clothes union nuclear testify course research heat listen task location thank hospital slice smell failure fawn helpful priest ambition average recover lecture process dough stadium", "wildlife deal acrobat romp anxiety axis starting require metric flexible geology game drove editor edge screw helpful have huge holy making pitch unknown carve holiday numb glasses survive already tenant adapt goat fangs", ] +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" # External entropy mocked as received from trezorlib. EXTERNAL_ENTROPY = b"zlutoucky kun upel divoke ody" * 2 # fmt: on diff --git a/tests/device_tests/test_msg_backup_device.py b/tests/device_tests/test_msg_backup_device.py index ee49396592..1cce57d96f 100644 --- a/tests/device_tests/test_msg_backup_device.py +++ b/tests/device_tests/test_msg_backup_device.py @@ -25,12 +25,15 @@ from trezorlib.exceptions import TrezorFailure from ..common import ( MNEMONIC12, MNEMONIC_SLIP39_ADVANCED_20, + MNEMONIC_SLIP39_CUSTOM_SECRET, MNEMONIC_SLIP39_BASIC_20_3of6, + MNEMONIC_SLIP39_CUSTOM_1of1, ) from ..input_flows import ( InputFlowBip39Backup, InputFlowSlip39AdvancedBackup, InputFlowSlip39BasicBackup, + InputFlowSlip39CustomBackup, ) @@ -111,6 +114,36 @@ def test_backup_slip39_advanced(client: Client, click_info: bool): assert expected_ms == actual_ms +@pytest.mark.skip_t1b1 +@pytest.mark.setup_client(needs_backup=True, mnemonic=MNEMONIC_SLIP39_CUSTOM_1of1[0]) +@pytest.mark.parametrize( + "share_threshold,share_count", + [(1, 1), (2, 2), (3, 5)], + ids=["1_of_1", "2_of_2", "3_of_5"], +) +def test_backup_slip39_custom(client: Client, share_threshold, share_count): + assert client.features.needs_backup is True + + with client: + IF = InputFlowSlip39CustomBackup(client, share_count) + client.set_input_flow(IF.get()) + device.backup( + client, group_threshold=1, groups=[(share_threshold, share_count)] + ) + + client.init_device() + assert client.features.initialized is True + assert client.features.needs_backup is False + assert client.features.unfinished_backup is False + assert client.features.no_backup is False + + assert len(IF.mnemonics) == share_count + assert ( + shamir.combine_mnemonics(IF.mnemonics[-share_threshold:]).hex() + == MNEMONIC_SLIP39_CUSTOM_SECRET + ) + + # we only test this with bip39 because the code path is always the same @pytest.mark.setup_client(no_backup=True) def test_no_backup_fails(client: Client): diff --git a/tests/input_flows.py b/tests/input_flows.py index 44a2e010ab..be44747457 100644 --- a/tests/input_flows.py +++ b/tests/input_flows.py @@ -1211,12 +1211,13 @@ class InputFlowBip39ResetFailedCheck(InputFlowBase): self.debug.press_yes() -def load_5_shares( +def load_N_shares( debug: DebugLink, + n: int, ) -> Generator[None, "messages.ButtonRequest", list[str]]: mnemonics: list[str] = [] - for _ in range(5): + for _ in range(n): # Phrase screen mnemonic = yield from read_and_confirm_mnemonic(debug) assert mnemonic is not None @@ -1254,7 +1255,7 @@ class InputFlowSlip39BasicBackup(InputFlowBase): self.debug.press_yes() # Mnemonic phrases - self.mnemonics = yield from load_5_shares(self.debug) + self.mnemonics = yield from load_N_shares(self.debug, 5) br = yield # Confirm backup assert br.code == B.Success @@ -1279,7 +1280,7 @@ class InputFlowSlip39BasicBackup(InputFlowBase): self.debug.press_yes() # Mnemonic phrases - self.mnemonics = yield from load_5_shares(self.debug) + self.mnemonics = yield from load_N_shares(self.debug, 5) br = yield # Confirm backup assert br.code == B.Success @@ -1304,7 +1305,7 @@ class InputFlowSlip39BasicBackup(InputFlowBase): self.debug.press_yes() # Mnemonic phrases - self.mnemonics = yield from load_5_shares(self.debug) + self.mnemonics = yield from load_N_shares(self.debug, 5) br = yield # Confirm backup assert br.code == B.Success @@ -1328,7 +1329,7 @@ class InputFlowSlip39BasicResetRecovery(InputFlowBase): yield from click_through(self.debug, screens=8, code=B.ResetDevice) # Mnemonic phrases - self.mnemonics = yield from load_5_shares(self.debug) + self.mnemonics = yield from load_N_shares(self.debug, 5) br = yield # safety warning assert br.code == B.Success @@ -1357,7 +1358,7 @@ class InputFlowSlip39BasicResetRecovery(InputFlowBase): self.debug.press_yes() # Mnemonic phrases - self.mnemonics = yield from load_5_shares(self.debug) + self.mnemonics = yield from load_N_shares(self.debug, 5) br = yield # Confirm backup assert br.code == B.Success @@ -1375,13 +1376,59 @@ class InputFlowSlip39BasicResetRecovery(InputFlowBase): yield from click_through(self.debug, screens=8, code=B.ResetDevice) # Mnemonic phrases - self.mnemonics = yield from load_5_shares(self.debug) + self.mnemonics = yield from load_N_shares(self.debug, 5) br = yield # safety warning assert br.code == B.Success self.debug.press_yes() +class InputFlowSlip39CustomBackup(InputFlowBase): + def __init__(self, client: Client, share_count: int): + super().__init__(client) + self.mnemonics: list[str] = [] + self.share_count = share_count + + def input_flow_tt(self) -> BRGeneratorType: + yield # Checklist + self.debug.press_yes() + yield # Confirm show seeds + self.debug.press_yes() + + # Mnemonic phrases + self.mnemonics = yield from load_N_shares(self.debug, self.share_count) + + br = yield # Confirm backup + assert br.code == B.Success + self.debug.press_yes() + + def input_flow_tr(self) -> BRGeneratorType: + yield # Checklist + self.debug.press_yes() + yield # Confirm show seeds + self.debug.press_yes() + + # Mnemonic phrases + self.mnemonics = yield from load_N_shares(self.debug, self.share_count) + + br = yield # Confirm backup + assert br.code == B.Success + self.debug.press_yes() + + def input_flow_t3t1(self) -> BRGeneratorType: + yield # Checklist + self.debug.press_yes() + yield # Confirm show seeds + self.debug.press_yes() + + # Mnemonic phrases + self.mnemonics = yield from load_N_shares(self.debug, self.share_count) + + br = yield # Confirm backup + assert br.code == B.Success + self.debug.press_yes() + + def load_5_groups_5_shares( debug: DebugLink, ) -> Generator[None, "messages.ButtonRequest", list[str]]: diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json index 1cc6ba14fd..afc61c22b1 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -835,6 +835,9 @@ "T3T1_en_test_autolock.py::test_dryrun_enter_word_slowly": "be557c3e4e0492d0b884d4ed549d3ae18e6175b74b5945e7ab6f2f96aab58748", "T3T1_en_test_autolock.py::test_dryrun_locks_at_number_of_words": "700aa42142055535b4123d84f6d307a0589b43600c2dec525312d06c2af9aa18", "T3T1_en_test_autolock.py::test_dryrun_locks_at_word_entry": "736652b5298a7a4ee2e51282323d1150c9844b6f7b738f421ac4ad3a83d0788d", +"T3T1_en_test_backup_slip39_custom.py::test_backup_slip39_custom[1of1]": "b13e22bddd80296bb0d08dff4bc94dad287c40247045eb779b88ba5c53050507", +"T3T1_en_test_backup_slip39_custom.py::test_backup_slip39_custom[2of3]": "ebab6dc1bbf5648693ff34ffece0b67d829d44ac03a751c72dcf623b2cd2c8ba", +"T3T1_en_test_backup_slip39_custom.py::test_backup_slip39_custom[5of5]": "aadf5b948d034af305b275fd0d837619b8f133039b4ed431fa31fd65cc2e143e", "T3T1_en_test_lock.py::test_hold_to_lock": "9d60d7aa2fbe6a0de14379e02ea825fbf9e21471596498f7be686f2538391f1d", "T3T1_en_test_passphrase_tt.py::test_cycle_through_last_character": "2a8d54c8014cc0c1bf46c0e4b58d6a002009b62aa8b92db663f88af0ad2f5e19", "T3T1_en_test_passphrase_tt.py::test_passphrase_click_same_button_many_times": "6a579067b4395a260d173e78643b67ac701304ea833a112cb2da1bce94cbb102", @@ -5096,11 +5099,14 @@ "T3T1_en_test_msg_applysettings.py::test_experimental_features": "e774f4de022f48caacc2445b1994ac19dce7a6155b4224ae7eb727ffc8975ca9", "T3T1_en_test_msg_applysettings.py::test_label_too_long": "80a6e289138a604cf351a29511cf6f85e2243591317894703152787e1351a1a3", "T3T1_en_test_msg_applysettings.py::test_safety_checks": "b129e0ba2e3c7ac836b3bb4b0bc8ded1c922eba054b42b434d10499fc780ea2b", -"T3T1_en_test_msg_backup_device.py::test_backup_bip39": "dfb5ff96727714c4d8115f9dee8d76d9cb40c46a6a5e693118cb8e332e0124ab", -"T3T1_en_test_msg_backup_device.py::test_backup_slip39_advanced[click_info]": "24ef4ff564a1d024deb4efa7b2fff91fa69892a6373f566cc5c4d86045c0c853", -"T3T1_en_test_msg_backup_device.py::test_backup_slip39_advanced[no_click_info]": "ca4836dd39f2398401fe2001a6cb7bf389f83fcd8e7770a65ca79b6840fe176a", -"T3T1_en_test_msg_backup_device.py::test_backup_slip39_basic[click_info]": "01af0780579b900211698fdaa68a34f4339fa516f152dfd013e210f96b02f5e1", -"T3T1_en_test_msg_backup_device.py::test_backup_slip39_basic[no_click_info]": "efd5fa4a4a87e6ac6e6b6b2c819d3d40711e7aff0340d1bab27794c333caa8d0", +"T3T1_en_test_msg_backup_device.py::test_backup_bip39": "429a141f97ad5a7b857985fcba60a7ac35e09e6ea54013203d80cb63deef8638", +"T3T1_en_test_msg_backup_device.py::test_backup_slip39_advanced[click_info]": "75f8fb63ed22a2e9983a9d60488d6cb87bde792ed7b00b67f1754ef5bfa8910a", +"T3T1_en_test_msg_backup_device.py::test_backup_slip39_advanced[no_click_info]": "31f143785472dad5adc46df21cf80ae3210c30c22fc719d9f56446d1915a36e8", +"T3T1_en_test_msg_backup_device.py::test_backup_slip39_basic[click_info]": "63db4c01cc1abddf73ece3c61d9d0de6bfd6499559e01bc58c2981f416a922f8", +"T3T1_en_test_msg_backup_device.py::test_backup_slip39_basic[no_click_info]": "f1cab327533697803793725144e42200106e05f1d81b75ff0a8eb55453a37dfa", +"T3T1_en_test_msg_backup_device.py::test_backup_slip39_custom[2_of_2]": "b0c0eccecc9e7a84ce9af7fca76d6edce2b1fd0ea48b41354cd0a2f31b8ff604", +"T3T1_en_test_msg_backup_device.py::test_backup_slip39_custom[1_of_1]": "8e6156bc4caf5f75de7c164844de9405e6c08bb74352dc6976a8d93dbc994e53", +"T3T1_en_test_msg_backup_device.py::test_backup_slip39_custom[3_of_5]": "61014a41b3c84831b6c82d0d3f8527452fed48f52cde4737b66f62a72bac3d5e", "T3T1_en_test_msg_backup_device.py::test_interrupt_backup_fails": "ae147498028d68aa71c7337544e4a5049c4c943897f905c6fe29e88e5c3ab056", "T3T1_en_test_msg_backup_device.py::test_no_backup_fails": "fada9d38ec099b3c6a4fd8bf994bb1f3431e40085128b4e0cd9deb8344dec53e", "T3T1_en_test_msg_backup_device.py::test_no_backup_show_entropy_fails": "c47cfc38ee8a29b79808336a6f99b037f4a760b907bb9c680a554f92a194d262", @@ -8130,6 +8136,9 @@ "TR_en_test_autolock.py::test_dryrun_enter_word_slowly": "f35d159c13b36c428e68969bbeb87fb4bdbfa6c21eb98985b5832849938d6934", "TR_en_test_autolock.py::test_dryrun_locks_at_number_of_words": "df7a17ab9cd2c617ce22a615e1da9bedee41f978459cbe103f2eecb8dfe8db12", "TR_en_test_autolock.py::test_dryrun_locks_at_word_entry": "53b39b3ff0548a91e87ac3391462850e4f6060fa1a19ae873eca6fc5cce8dbb2", +"TR_en_test_backup_slip39_custom.py::test_backup_slip39_custom[1of1]": "9e620acfff4ef1442996b81c1fd9c73d20490c2deecc6bc05aa0b665d9cd217c", +"TR_en_test_backup_slip39_custom.py::test_backup_slip39_custom[2of3]": "aa0d1382ef0f2e16e24d0afbd25a105f572cd4f17d91dd44ca6a3f46a60bf6c4", +"TR_en_test_backup_slip39_custom.py::test_backup_slip39_custom[5of5]": "5586a21ef466d85077021c289690f22afe787445523b29900f49ec03468598d8", "TR_en_test_lock.py::test_hold_to_lock": "83e2d055215b03150069d9fcb3aee6dc3f78a0d2bc43d8133425bf6b000c191d", "TR_en_test_passphrase_tr.py::test_cancel": "4a79e82b3ddf23c087e70767f2c4d4720f980370e22be209189d4ed42a453ee8", "TR_en_test_passphrase_tr.py::test_passphrase_delete": "28468562292a54f2d8cc954129c6b1859c267bc5a9b94f9b406352e71a4c8036", @@ -15206,6 +15215,9 @@ "TT_en_test_autolock.py::test_dryrun_enter_word_slowly": "140ff1c01d0d27ade29e88af481a9a24385fbe01058bdbf35f2fa20c19e0c386", "TT_en_test_autolock.py::test_dryrun_locks_at_number_of_words": "f9a5c8f92ca3b0b9545a9a5b3cf8df4de9943fbe45de113aa6f970d60b3b9b49", "TT_en_test_autolock.py::test_dryrun_locks_at_word_entry": "2ea54adc6df443f758a6395b6b443fbfe5931cbd62a321504de9ae453aff8ca8", +"TT_en_test_backup_slip39_custom.py::test_backup_slip39_custom[1of1]": "6c33232fdff24175a489c83507bcde9bd859cc2d7f1ca687e154befd0cd2b883", +"TT_en_test_backup_slip39_custom.py::test_backup_slip39_custom[2of3]": "20bdb47f4150af61b2a64927f7df2f49fedf9d02442d103c2833bddcc57f9f93", +"TT_en_test_backup_slip39_custom.py::test_backup_slip39_custom[5of5]": "97fb33be7f98498b0acfcade1e5333475c9e6dce131ca7bdde3a54cf72c595fe", "TT_en_test_lock.py::test_hold_to_lock": "a5739f92ae28fc57769e444408ce5b58223d0d33b368022ef78ea68e0f8c9b80", "TT_en_test_passphrase_tt.py::test_cycle_through_last_character": "2a8d54c8014cc0c1bf46c0e4b58d6a002009b62aa8b92db663f88af0ad2f5e19", "TT_en_test_passphrase_tt.py::test_passphrase_click_same_button_many_times": "6a579067b4395a260d173e78643b67ac701304ea833a112cb2da1bce94cbb102", @@ -19593,11 +19605,14 @@ "TT_en_test_msg_applysettings.py::test_experimental_features": "523f74db7f660c261507dfdd92285981869af72c9ba391c4dfedb3f06ccf40ad", "TT_en_test_msg_applysettings.py::test_label_too_long": "80a6e289138a604cf351a29511cf6f85e2243591317894703152787e1351a1a3", "TT_en_test_msg_applysettings.py::test_safety_checks": "b129e0ba2e3c7ac836b3bb4b0bc8ded1c922eba054b42b434d10499fc780ea2b", -"TT_en_test_msg_backup_device.py::test_backup_bip39": "3b90a1c3383b3afa2c321109e1088b2e33d976a9f75db3b013456d52c85ae5d2", -"TT_en_test_msg_backup_device.py::test_backup_slip39_advanced[click_info]": "2738a2e8fb890f7f2f5857cd9af0d3f6eac9fbf71dc299c829b1ba6a1209b712", -"TT_en_test_msg_backup_device.py::test_backup_slip39_advanced[no_click_info]": "503c667f02e099102be7c9bf14e771b5e27f41eabebdf7269389c9d9b33016ae", -"TT_en_test_msg_backup_device.py::test_backup_slip39_basic[click_info]": "65b98eb420a3b4a5128dc686f491a55b460205db7e1e1bb2828a3d6998af5466", -"TT_en_test_msg_backup_device.py::test_backup_slip39_basic[no_click_info]": "a3c26090183f9b150cd0caccfa2a4ba9833de718868777cc1e6f255ddda8a94f", +"TT_en_test_msg_backup_device.py::test_backup_bip39": "09c32059eea15ce17dd07bc34f09ca335e8a6cee573fef0095cf2105932a078a", +"TT_en_test_msg_backup_device.py::test_backup_slip39_advanced[click_info]": "5007dfedc1b4420b096f76d33acc9bcf77ec6083d54ca59571f5d225dfb2a33b", +"TT_en_test_msg_backup_device.py::test_backup_slip39_advanced[no_click_info]": "4f13e0550f8e06481fd4f59d722d3893ea904f85fb20ec8018344347492aff1c", +"TT_en_test_msg_backup_device.py::test_backup_slip39_basic[click_info]": "b432bf3671b15fdcfb92113fcef0e95671c3d7beaae70b25a87d090d30f9a079", +"TT_en_test_msg_backup_device.py::test_backup_slip39_basic[no_click_info]": "4a6f3fdd5d7ae36e6b5ad84faa5385565fd13f4ce3f7d915fa16c2184f99ebbd", +"TT_en_test_msg_backup_device.py::test_backup_slip39_custom[2_of_2]": "53788010ec0f9c81ea07828ae32425c3d38a1d35789aa91d3163e117fa2cbe99", +"TT_en_test_msg_backup_device.py::test_backup_slip39_custom[1_of_1]": "e17b49d05f231444684b0ebad638456b4b6d7b0ae195180778c3f687b7d0ca7d", +"TT_en_test_msg_backup_device.py::test_backup_slip39_custom[3_of_5]": "ebc007c9801856ad27dd6332b7dc5413189191268e10fcf5604203b75d8c52ed", "TT_en_test_msg_backup_device.py::test_interrupt_backup_fails": "ae147498028d68aa71c7337544e4a5049c4c943897f905c6fe29e88e5c3ab056", "TT_en_test_msg_backup_device.py::test_no_backup_fails": "fada9d38ec099b3c6a4fd8bf994bb1f3431e40085128b4e0cd9deb8344dec53e", "TT_en_test_msg_backup_device.py::test_no_backup_show_entropy_fails": "001377ce61dcd189e6a9d17e20dcd71130e951dc3314b40ff26f816bd9355bdd",