test: allow setting `unfinished_backup` flag in storage
What changed, and why it matters
This commit adds a new test-only debug option called `unfinished_backup` to the Trezor hardware wallet's device-loading tools. It lets developers set an internal storage flag that marks a backup as incomplete. The change only affects debug/test code paths and does not alter normal user-facing wallet behavior. There is no indication in the commit that this fixes or introduces a security vulnerability.
Treat as routine test/debug infrastructure. Review whether the `LoadDevice` debug message is appropriately gated from production builds and whether setting `unfinished_backup` via debug load can influence any runtime security decisions (e.g., backup reminders, seed safety checks). No immediate security action is indicated by the diff alone.
Security signals we found
Adds a debug-only storage flag to the LoadDevice message surface
Wires new protobuf field through core and legacy firmware debug handlers
No changelog entry; explicitly described as test infrastructure
No validation, bounds, or authorization changes observed
No references to CVEs, advisories, or security issues in commit materials
Evidence from the diff
The change extends the LoadDevice management message (protobuf field 11) with an optional unfinished_backup boolean and wires it through generated message bindings (Python, Rust) and firmware handlers (core apps/debug/load_device.py, legacy config.c). The flag is passed into device storage via existing set_unfinished_backup / config_setUnfinishedBackup APIs. It is also exposed in the test helper load_device() and SetupParams. The commit is tagged [no changelog] and is framed as a test/debug capability.
Changed components
common/protob/messages-management.protocore/src/apps/debug/load_device.pycore/src/trezor/messages.pylegacy/firmware/config.cpython/src/trezorlib/debuglink.pypython/src/trezorlib/messages.pyrust/trezor-client/src/protos/generated/messages_management.rstests/conftest.pyInspect captured patch +74 / −20
diff --git a/common/protob/messages-management.proto b/common/protob/messages-management.proto
index 5f35d2f8..f140075a 100644
--- a/common/protob/messages-management.proto
+++ b/common/protob/messages-management.proto
@@ -420,6 +420,7 @@ message LoadDevice {
optional uint32 u2f_counter = 8; // U2F counter
optional bool needs_backup = 9; // set "needs backup" flag
optional bool no_backup = 10; // indicate that no backup is going to be made
+ optional bool unfinished_backup = 11; // indicate that backup process has failed
}
/**
diff --git a/core/src/apps/debug/load_device.py b/core/src/apps/debug/load_device.py
index 3cc6ff94..49ede360 100644
--- a/core/src/apps/debug/load_device.py
+++ b/core/src/apps/debug/load_device.py
@@ -67,6 +67,8 @@ async def load_device(msg: LoadDevice) -> Success:
no_backup=msg.no_backup is True,
allow_derivation_fail=msg.skip_checksum is True,
)
+ if msg.unfinished_backup is not None:
+ storage_device.set_unfinished_backup(bool(msg.unfinished_backup))
storage_device.set_passphrase_enabled(bool(msg.passphrase_protection))
storage_device.set_label(msg.label or "")
if msg.pin:
diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py
index 49ea6765..7be4c438 100644
--- a/core/src/trezor/messages.py
+++ b/core/src/trezor/messages.py
@@ -2456,6 +2456,7 @@ if TYPE_CHECKING:
u2f_counter: "int | None"
needs_backup: "bool | None"
no_backup: "bool | None"
+ unfinished_backup: "bool | None"
def __init__(
self,
@@ -2468,6 +2469,7 @@ if TYPE_CHECKING:
u2f_counter: "int | None" = None,
needs_backup: "bool | None" = None,
no_backup: "bool | None" = None,
+ unfinished_backup: "bool | None" = None,
) -> None:
pass
diff --git a/legacy/firmware/config.c b/legacy/firmware/config.c
index 22463708..ebe344b1 100644
--- a/legacy/firmware/config.c
+++ b/legacy/firmware/config.c
@@ -529,6 +529,10 @@ void config_loadDevice(const LoadDevice *msg) {
if (msg->has_no_backup && msg->no_backup) {
config_setNoBackup();
}
+
+ if (msg->has_unfinished_backup) {
+ config_setUnfinishedBackup(msg->unfinished_backup);
+ }
}
#endif
diff --git a/python/src/trezorlib/debuglink.py b/python/src/trezorlib/debuglink.py
index a675a4f3..8f9b6fe6 100644
--- a/python/src/trezorlib/debuglink.py
+++ b/python/src/trezorlib/debuglink.py
@@ -1753,6 +1753,7 @@ def load_device(
skip_checksum: bool = False,
needs_backup: bool = False,
no_backup: bool = False,
+ unfinished_backup: bool | None = None,
) -> None:
if isinstance(mnemonic, str):
mnemonic = [mnemonic]
@@ -1773,6 +1774,7 @@ def load_device(
skip_checksum=skip_checksum,
needs_backup=needs_backup,
no_backup=no_backup,
+ unfinished_backup=unfinished_backup,
),
expect=messages.Success,
)
diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py
index 0942073f..ef67bd87 100644
--- a/python/src/trezorlib/messages.py
+++ b/python/src/trezorlib/messages.py
@@ -3716,6 +3716,7 @@ class LoadDevice(protobuf.MessageType):
8: protobuf.Field("u2f_counter", "uint32", repeated=False, required=False, default=None),
9: protobuf.Field("needs_backup", "bool", repeated=False, required=False, default=None),
10: protobuf.Field("no_backup", "bool", repeated=False, required=False, default=None),
+ 11: protobuf.Field("unfinished_backup", "bool", repeated=False, required=False, default=None),
}
def __init__(
@@ -3730,6 +3731,7 @@ class LoadDevice(protobuf.MessageType):
u2f_counter: Optional["int"] = None,
needs_backup: Optional["bool"] = None,
no_backup: Optional["bool"] = None,
+ unfinished_backup: Optional["bool"] = None,
) -> None:
self.mnemonics: Sequence["str"] = mnemonics if mnemonics is not None else []
self.pin = pin
@@ -3740,6 +3742,7 @@ class LoadDevice(protobuf.MessageType):
self.u2f_counter = u2f_counter
self.needs_backup = needs_backup
self.no_backup = no_backup
+ self.unfinished_backup = unfinished_backup
class ResetDevice(protobuf.MessageType):
diff --git a/rust/trezor-client/src/protos/generated/messages_management.rs b/rust/trezor-client/src/protos/generated/messages_management.rs
index 0ca2cd54..dd7dbdf2 100644
--- a/rust/trezor-client/src/protos/generated/messages_management.rs
+++ b/rust/trezor-client/src/protos/generated/messages_management.rs
@@ -6856,6 +6856,8 @@ pub struct LoadDevice {
pub needs_backup: ::std::option::Option<bool>,
// @@protoc_insertion_point(field:hw.trezor.messages.management.LoadDevice.no_backup)
pub no_backup: ::std::option::Option<bool>,
+ // @@protoc_insertion_point(field:hw.trezor.messages.management.LoadDevice.unfinished_backup)
+ pub unfinished_backup: ::std::option::Option<bool>,
// special fields
// @@protoc_insertion_point(special_field:hw.trezor.messages.management.LoadDevice.special_fields)
pub special_fields: ::protobuf::SpecialFields,
@@ -7075,8 +7077,27 @@ impl LoadDevice {
self.no_backup = ::std::option::Option::Some(v);
}
+ // optional bool unfinished_backup = 11;
+
+ pub fn unfinished_backup(&self) -> bool {
+ self.unfinished_backup.unwrap_or(false)
+ }
+
+ pub fn clear_unfinished_backup(&mut self) {
+ self.unfinished_backup = ::std::option::Option::None;
+ }
+
+ pub fn has_unfinished_backup(&self) -> bool {
+ self.unfinished_backup.is_some()
+ }
+
+ // Param is passed by value, moved
+ pub fn set_unfinished_backup(&mut self, v: bool) {
+ self.unfinished_backup = ::std::option::Option::Some(v);
+ }
+
fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData {
- let mut fields = ::std::vec::Vec::with_capacity(9);
+ let mut fields = ::std::vec::Vec::with_capacity(10);
let mut oneofs = ::std::vec::Vec::with_capacity(0);
fields.push(::protobuf::reflect::rt::v2::make_vec_simpler_accessor::<_, _>(
"mnemonics",
@@ -7123,6 +7144,11 @@ impl LoadDevice {
|m: &LoadDevice| { &m.no_backup },
|m: &mut LoadDevice| { &mut m.no_backup },
));
+ fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
+ "unfinished_backup",
+ |m: &LoadDevice| { &m.unfinished_backup },
+ |m: &mut LoadDevice| { &mut m.unfinished_backup },
+ ));
::protobuf::reflect::GeneratedMessageDescriptorData::new_2::<LoadDevice>(
"LoadDevice",
fields,
@@ -7168,6 +7194,9 @@ impl ::protobuf::Message for LoadDevice {
80 => {
self.no_backup = ::std::option::Option::Some(is.read_bool()?);
},
+ 88 => {
+ self.unfinished_backup = ::std::option::Option::Some(is.read_bool()?);
+ },
tag => {
::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?;
},
@@ -7207,6 +7236,9 @@ impl ::protobuf::Message for LoadDevice {
if let Some(v) = self.no_backup {
my_size += 1 + 1;
}
+ if let Some(v) = self.unfinished_backup {
+ my_size += 1 + 1;
+ }
my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields());
self.special_fields.cached_size().set(my_size as u32);
my_size
@@ -7240,6 +7272,9 @@ impl ::protobuf::Message for LoadDevice {
if let Some(v) = self.no_backup {
os.write_bool(10, v)?;
}
+ if let Some(v) = self.unfinished_backup {
+ os.write_bool(11, v)?;
+ }
os.write_unknown_fields(self.special_fields.unknown_fields())?;
::std::result::Result::Ok(())
}
@@ -7266,6 +7301,7 @@ impl ::protobuf::Message for LoadDevice {
self.u2f_counter = ::std::option::Option::None;
self.needs_backup = ::std::option::Option::None;
self.no_backup = ::std::option::Option::None;
+ self.unfinished_backup = ::std::option::Option::None;
self.special_fields.clear();
}
@@ -7280,6 +7316,7 @@ impl ::protobuf::Message for LoadDevice {
u2f_counter: ::std::option::Option::None,
needs_backup: ::std::option::Option::None,
no_backup: ::std::option::Option::None,
+ unfinished_backup: ::std::option::Option::None,
special_fields: ::protobuf::SpecialFields::new(),
};
&instance
@@ -12369,7 +12406,7 @@ static file_descriptor_proto_data: &'static [u8] = b"\
R\x12optigaCertificates\x12)\n\x10optiga_signature\x18\x02\x20\x02(\x0cR\
\x0foptigaSignature\x12/\n\x13tropic_certificates\x18\x03\x20\x03(\x0cR\
\x12tropicCertificates\x12)\n\x10tropic_signature\x18\x04\x20\x01(\x0cR\
- \x0ftropicSignature\"\x0c\n\nWipeDevice\"\xad\x02\n\nLoadDevice\x12\x1c\
+ \x0ftropicSignature\"\x0c\n\nWipeDevice\"\xda\x02\n\nLoadDevice\x12\x1c\
\n\tmnemonics\x18\x01\x20\x03(\tR\tmnemonics\x12\x10\n\x03pin\x18\x03\
\x20\x01(\tR\x03pin\x123\n\x15passphrase_protection\x18\x04\x20\x01(\x08\
R\x14passphraseProtection\x12\x1e\n\x08language\x18\x05\x20\x01(\tR\x08l\
@@ -12377,24 +12414,25 @@ static file_descriptor_proto_data: &'static [u8] = b"\
#\n\rskip_checksum\x18\x07\x20\x01(\x08R\x0cskipChecksum\x12\x1f\n\x0bu2\
f_counter\x18\x08\x20\x01(\rR\nu2fCounter\x12!\n\x0cneeds_backup\x18\t\
\x20\x01(\x08R\x0bneedsBackup\x12\x1b\n\tno_backup\x18\n\x20\x01(\x08R\
- \x08noBackup\"\x9d\x03\n\x0bResetDevice\x12\x1f\n\x08strength\x18\x02\
- \x20\x01(\r:\x03256R\x08strength\x123\n\x15passphrase_protection\x18\x03\
- \x20\x01(\x08R\x14passphraseProtection\x12%\n\x0epin_protection\x18\x04\
- \x20\x01(\x08R\rpinProtection\x12\x1e\n\x08language\x18\x05\x20\x01(\tR\
- \x08languageB\x02\x18\x01\x12\x14\n\x05label\x18\x06\x20\x01(\tR\x05labe\
- l\x12\x1f\n\x0bu2f_counter\x18\x07\x20\x01(\rR\nu2fCounter\x12\x1f\n\x0b\
- skip_backup\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.messages.management.BackupType:\x05Bip39R\nbackupType\x12#\n\ren\
- tropy_check\x18\x0b\x20\x01(\x08R\x0centropyCheckJ\x04\x08\x01\x10\x02\"\
- \xe5\x01\n\x0cBackupDevice\x12'\n\x0fgroup_threshold\x18\x01\x20\x01(\rR\
- \x0egroupThreshold\x12O\n\x06groups\x18\x02\x20\x03(\x0b27.hw.trezor.mes\
- sages.management.BackupDevice.Slip39GroupR\x06groups\x1a[\n\x0bSlip39Gro\
- up\x12)\n\x10member_threshold\x18\x01\x20\x02(\rR\x0fmemberThreshold\x12\
- !\n\x0cmember_count\x18\x02\x20\x02(\rR\x0bmemberCount\"b\n\x0eEntropyRe\
- quest\x12-\n\x12entropy_commitment\x18\x01\x20\x01(\x0cR\x11entropyCommi\
- tment\x12!\n\x0cprev_entropy\x18\x02\x20\x01(\x0cR\x0bprevEntropy\"&\n\n\
- EntropyAck\x12\x18\n\x07entropy\x18\x01\x20\x02(\x0cR\x07entropy\"\x13\n\
+ \x08noBackup\x12+\n\x11unfinished_backup\x18\x0b\x20\x01(\x08R\x10unfini\
+ shedBackup\"\x9d\x03\n\x0bResetDevice\x12\x1f\n\x08strength\x18\x02\x20\
+ \x01(\r:\x03256R\x08strength\x123\n\x15passphrase_protection\x18\x03\x20\
+ \x01(\x08R\x14passphraseProtection\x12%\n\x0epin_protection\x18\x04\x20\
+ \x01(\x08R\rpinProtection\x12\x1e\n\x08language\x18\x05\x20\x01(\tR\x08l\
+ anguageB\x02\x18\x01\x12\x14\n\x05label\x18\x06\x20\x01(\tR\x05label\x12\
+ \x1f\n\x0bu2f_counter\x18\x07\x20\x01(\rR\nu2fCounter\x12\x1f\n\x0bskip_\
+ backup\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.tre\
+ zor.messages.management.BackupType:\x05Bip39R\nbackupType\x12#\n\rentrop\
+ y_check\x18\x0b\x20\x01(\x08R\x0centropyCheckJ\x04\x08\x01\x10\x02\"\xe5\
+ \x01\n\x0cBackupDevice\x12'\n\x0fgroup_threshold\x18\x01\x20\x01(\rR\x0e\
+ groupThreshold\x12O\n\x06groups\x18\x02\x20\x03(\x0b27.hw.trezor.message\
+ s.management.BackupDevice.Slip39GroupR\x06groups\x1a[\n\x0bSlip39Group\
+ \x12)\n\x10member_threshold\x18\x01\x20\x02(\rR\x0fmemberThreshold\x12!\
+ \n\x0cmember_count\x18\x02\x20\x02(\rR\x0bmemberCount\"b\n\x0eEntropyReq\
+ uest\x12-\n\x12entropy_commitment\x18\x01\x20\x01(\x0cR\x11entropyCommit\
+ ment\x12!\n\x0cprev_entropy\x18\x02\x20\x01(\x0cR\x0bprevEntropy\"&\n\nE\
+ ntropyAck\x12\x18\n\x07entropy\x18\x01\x20\x02(\x0cR\x07entropy\"\x13\n\
\x11EntropyCheckReady\"5\n\x14EntropyCheckContinue\x12\x1d\n\x06finish\
\x18\x01\x20\x01(\x08:\x05falseR\x06finish\"\x8d\x04\n\x0eRecoveryDevice\
\x12\x1d\n\nword_count\x18\x01\x20\x01(\rR\twordCount\x123\n\x15passphra\
diff --git a/tests/conftest.py b/tests/conftest.py
index ae4cc2ef..e8a453ef 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -256,6 +256,7 @@ class SetupParams:
passphrase: bool | str = False
needs_backup: bool = False
no_backup: bool = False
+ unfinished_backup: bool | None = None
experimental: bool = False
label: str = "test"
@@ -290,6 +291,7 @@ class SetupParams:
label=self.label,
needs_backup=self.needs_backup,
no_backup=self.no_backup,
+ unfinished_backup=self.unfinished_backup,
)
if self.experimental:
apply_settings(session, experimental_features=True)
Why this scored 21/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.