diff --git a/common/protob/messages-management.proto b/common/protob/messages-management.proto index b0e623310..4c5c8f600 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 000000000..2d5938422 --- /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 b4c37afc5..c4bb30c70 100644 --- a/core/src/apps/management/backup_device.py +++ b/core/src/apps/management/backup_device.py @@ -18,14 +18,19 @@ async def backup_device(msg: BackupDevice) -> Success: 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 storage_device.set_unfinished_backup(True) storage_device.set_backed_up() - await backup_seed(mnemonic_type, mnemonic_secret) + await backup_seed( + backup_type, + mnemonic_secret, + msg.group_threshold, + [(g.member_threshold, g.member_count) for g in msg.groups], + ) 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 9f24d167a..2e1380332 100644 --- a/core/src/apps/management/reset_device/__init__.py +++ b/core/src/apps/management/reset_device/__init__.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Collection import storage import storage.device as storage_device @@ -111,31 +111,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) - - 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 + share_threshold = await layout.slip39_prompt_threshold(share_count) - # 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( + encrypted_master_secret, group_threshold, [(share_threshold, share_count)] + ) async def _backup_slip39_advanced(encrypted_master_secret: bytes) -> None: @@ -155,6 +143,14 @@ 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(encrypted_master_secret, group_threshold, groups) + + +async def _backup_slip39( + encrypted_master_secret: bytes, + group_threshold: int, + groups: Collection[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 +166,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 +184,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)") @@ -213,8 +213,20 @@ def _compute_secret_from_entropy( return secret -async def backup_seed(backup_type: BackupType, mnemonic_secret: bytes) -> None: - if backup_type == BAK_T_SLIP39_BASIC: +async def backup_seed( + backup_type: BackupType, + mnemonic_secret: bytes, + group_threshold: int | None = None, + groups: Collection[tuple[int, int]] = (), +) -> None: + # Either both should be defined or both should be missing: group_threshold, groups + assert (group_threshold is None) == (len(groups) == 0) + + assert backup_type != BAK_T_BIP39 or group_threshold is None + + if group_threshold is not None: + await _backup_slip39(mnemonic_secret, group_threshold, groups) + elif backup_type == BAK_T_SLIP39_BASIC: await _backup_slip39_basic(mnemonic_secret) elif backup_type == BAK_T_SLIP39_ADVANCED: await _backup_slip39_advanced(mnemonic_secret) diff --git a/core/src/trezor/crypto/slip39.py b/core/src/trezor/crypto/slip39.py index 304f4bdee..b2ee98597 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 a4d6b4ee2..0335bb53d 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 bd28e3f44..cb1000258 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 000000000..2d5938422 --- /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 e8d365da1..c09d49453 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() @@ -295,7 +302,7 @@ def unlock_bootloader(client: "TrezorClient") -> str: @cli.command() -@click.argument("enable", type=ChoiceType({"on": True, "off": False}), required=False) +@click.argument("enable", type=ChoiceType({"on": True, "off": False})) @click.option( "-e", "--expiry", @@ -322,7 +329,7 @@ def set_busy( @cli.command() -@click.argument("hex_challenge", required=False) +@click.argument("hex_challenge") @with_client def authenticate(client: "TrezorClient", hex_challenge: Optional[str]) -> None: """Get information to verify the authenticity of the device.""" diff --git a/python/src/trezorlib/device.py b/python/src/trezorlib/device.py index dbea87e80..bdebdc06b 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 d52619a5e..b5f52bce1 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 3fb802326..799111f5a 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 72260ca7e..3650b8809 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_single.py b/tests/click_tests/test_backup_slip39_single.py new file mode 100644 index 000000000..692472f0f --- /dev/null +++ b/tests/click_tests/test_backup_slip39_single.py @@ -0,0 +1,112 @@ +# 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.mark.setup_client(uninitialized=True) +@WITH_MOCK_URANDOM +def test_backup_slip39_single( + 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) + + 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, 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/device_tests/test_msg_backup_device.py b/tests/device_tests/test_msg_backup_device.py index ee4939659..7ad447af2 100644 --- a/tests/device_tests/test_msg_backup_device.py +++ b/tests/device_tests/test_msg_backup_device.py @@ -31,6 +31,7 @@ from ..input_flows import ( InputFlowBip39Backup, InputFlowSlip39AdvancedBackup, InputFlowSlip39BasicBackup, + InputFlowSlip39SingleBackup, ) @@ -111,6 +112,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_ADVANCED_20) +@pytest.mark.parametrize( + "click_info", [True, False], ids=["click_info", "no_click_info"] +) +def test_backup_slip39_single(client: Client, click_info: bool): + if click_info and client.model is models.T2B1: + pytest.skip("click_info not implemented on T2B1") + + assert client.features.needs_backup is True + + with client: + IF = InputFlowSlip39SingleBackup(client, click_info) + client.set_input_flow(IF.get()) + device.backup(client, group_threshold=1, groups=[(1, 1)]) + + 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 client.features.backup_type is messages.BackupType.Slip39_Advanced + + expected_ms = shamir.combine_mnemonics(MNEMONIC_SLIP39_ADVANCED_20) + actual_ms = shamir.combine_mnemonics( + IF.mnemonics[:3] + IF.mnemonics[5:8] + IF.mnemonics[10:13] + ) + assert expected_ms == actual_ms + + # 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 44a2e010a..0709b59a8 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 InputFlowSlip39SingleBackup(InputFlowBase): + def __init__(self, client: Client, click_info: bool): + super().__init__(client) + self.mnemonics: list[str] = [] + self.click_info = click_info + + 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, 1) + + 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, 1) + + 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, 1) + + 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 1cc6ba14f..d0369da5f 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -835,6 +835,7 @@ "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_single.py::test_backup_slip39_single[1of1]": "bc2a685f1529e18d4f634bcf7064169ad18c014a353c2da1502280c4767e53e6", "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", @@ -5101,6 +5102,8 @@ "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_slip39_single[click_info]": "26bab23e807c8109c4a23306eaa9009937a15835709a1ff6218fabb4a9332295", +"T3T1_en_test_msg_backup_device.py::test_backup_slip39_single[no_click_info]": "26bab23e807c8109c4a23306eaa9009937a15835709a1ff6218fabb4a9332295", "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 +8133,7 @@ "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_single.py::test_backup_slip39_single[1of1]": "5fde88a78ba7be0eeb901f49a737c52dc414e548d37b9e29550be4ca75d52a29", "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 +15210,7 @@ "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_single.py::test_backup_slip39_single[1of1]": "e0b796328e714068825876cd73188a12da38eba978388f32edcbfa973bc73a49", "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", @@ -19598,6 +19603,8 @@ "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_slip39_single[click_info]": "8ab2a9575d326c5578aa2b4ceca01e85d7e4c2b2454bdaef2378c5ac4d171df0", +"TT_en_test_msg_backup_device.py::test_backup_slip39_single[no_click_info]": "8ab2a9575d326c5578aa2b4ceca01e85d7e4c2b2454bdaef2378c5ac4d171df0", "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",