feat(core): add ability to request backups with any number of groups/shares.

Ioan Bizău 4 weeks ago
parent 91a783ee37
commit ba49b9e760

@ -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;
}
/**

@ -0,0 +1 @@
Added ability to request Shamir backups with any number of groups/shares.

@ -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)

@ -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)

@ -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.

@ -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"

@ -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

@ -0,0 +1 @@
Added ability to request Shamir backups with any number of groups/shares.

@ -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."""

@ -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

@ -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 = {

@ -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<u32>,
// @@protoc_insertion_point(field:hw.trezor.messages.management.BackupDevice.groups)
pub groups: ::std::vec::Vec<backup_device::Slip39Group>,
// 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>(
"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<Self>;
}
/// 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<u32>,
// @@protoc_insertion_point(field:hw.trezor.messages.management.BackupDevice.Slip39Group.member_count)
pub member_count: ::std::option::Option<u32>,
// 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 {
<Slip39Group as ::protobuf::Message>::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::<Slip39Group>(
"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<Self>;
}
}
// @@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());

@ -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()

@ -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 <https://www.gnu.org/licenses/lgpl-3.0.html>.
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

@ -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):

@ -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]]:

@ -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",

Loading…
Cancel
Save