fix(common): require fields on SLIP-24 Text Details memo
What changed, and why it matters
This commit tightens the rules for a Trezor message type called TextDetailsMemo, which is part of the SLIP-24 payment-request memo format. Previously the title and text fields were optional and defaulted to empty strings; now both are required. The change is defensive: it prevents a device from accepting a payment-request memo that omits the title or body text. Without this, a malicious or buggy host could send a payment request with an empty/incomplete memo and still have it treated as a valid TextDetailsMemo, potentially misleading the user about what they are approving.
Treat as a low-severity hardening fix. Verify that downstream/host software already sends both title and text, because making fields required can break compatibility with older hosts that omitted them. No urgent user action is indicated.
Security signals we found
protobuf field requirement change from optional to required
adds missing-field validation in generated Rust is_initialized
affects SLIP-24 PaymentRequest memo parsing
prevents acceptance of incomplete TextDetailsMemo messages
Evidence from the diff
The protobuf definition for PaymentRequest.TextDetailsMemo is changed from optional string fields with empty defaults to required string fields. Generated bindings in core/src/trezor/messages.py, python/src/trezorlib/messages.py, and rust/trezor-client/src/protos/generated/messages_common.rs are updated accordingly. The Rust generated code adds is_initialized checks that reject messages missing title or text. This is a validation-hardening change in the SLIP-24 memo parsing path.
Changed components
common/protob/messages-common.protocore/src/trezor/messages.pypython/src/trezorlib/messages.pyrust/trezor-client/src/protos/generated/messages_common.rsSLIP-24 PaymentRequest TextDetailsMemo handlingInspect captured patch +28 / −22
diff --git a/common/protob/messages-common.proto b/common/protob/messages-common.proto
index 33e7a377e..8086c1e20 100644
--- a/common/protob/messages-common.proto
+++ b/common/protob/messages-common.proto
@@ -200,8 +200,8 @@ message PaymentRequest {
}
message TextDetailsMemo {
- optional string title = 1 [default=""]; // plain-text heading
- optional string text = 2 [default=""]; // plain-text note containing additional details about the payment
+ required string title = 1; // plain-text heading
+ required string text = 2; // plain-text note containing additional details about the payment
}
message RefundMemo {
diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py
index 7fcbb5f5b..bb18fd260 100644
--- a/core/src/trezor/messages.py
+++ b/core/src/trezor/messages.py
@@ -311,8 +311,8 @@ if TYPE_CHECKING:
def __init__(
self,
*,
- title: "str | None" = None,
- text: "str | None" = None,
+ title: "str",
+ text: "str",
) -> None:
pass
diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py
index 68741ebf0..7e1a838ab 100644
--- a/python/src/trezorlib/messages.py
+++ b/python/src/trezorlib/messages.py
@@ -961,15 +961,15 @@ class TextMemo(protobuf.MessageType):
class TextDetailsMemo(protobuf.MessageType):
MESSAGE_WIRE_TYPE = None
FIELDS = {
- 1: protobuf.Field("title", "string", repeated=False, required=False, default=''),
- 2: protobuf.Field("text", "string", repeated=False, required=False, default=''),
+ 1: protobuf.Field("title", "string", repeated=False, required=True),
+ 2: protobuf.Field("text", "string", repeated=False, required=True),
}
def __init__(
self,
*,
- title: Optional["str"] = '',
- text: Optional["str"] = '',
+ title: "str",
+ text: "str",
) -> None:
self.title = title
self.text = text
diff --git a/rust/trezor-client/src/protos/generated/messages_common.rs b/rust/trezor-client/src/protos/generated/messages_common.rs
index 50db3b97b..2fd0ad56d 100644
--- a/rust/trezor-client/src/protos/generated/messages_common.rs
+++ b/rust/trezor-client/src/protos/generated/messages_common.rs
@@ -3221,7 +3221,7 @@ pub mod payment_request {
::std::default::Default::default()
}
- // optional string title = 1;
+ // required string title = 1;
pub fn title(&self) -> &str {
match self.title.as_ref() {
@@ -3257,7 +3257,7 @@ pub mod payment_request {
self.title.take().unwrap_or_else(|| ::std::string::String::new())
}
- // optional string text = 2;
+ // required string text = 2;
pub fn text(&self) -> &str {
match self.text.as_ref() {
@@ -3318,6 +3318,12 @@ pub mod payment_request {
const NAME: &'static str = "TextDetailsMemo";
fn is_initialized(&self) -> bool {
+ if self.title.is_none() {
+ return false;
+ }
+ if self.text.is_none() {
+ return false;
+ }
true
}
@@ -4037,7 +4043,7 @@ static file_descriptor_proto_data: &'static [u8] = b"\
gerprint\x18\x02\x20\x02(\rR\x0bfingerprint\x12\x1b\n\tchild_num\x18\x03\
\x20\x02(\rR\x08childNum\x12\x1d\n\nchain_code\x18\x04\x20\x02(\x0cR\tch\
ainCode\x12\x1f\n\x0bprivate_key\x18\x05\x20\x01(\x0cR\nprivateKey\x12\
- \x1d\n\npublic_key\x18\x06\x20\x02(\x0cR\tpublicKey\"\xb8\x07\n\x0ePayme\
+ \x1d\n\npublic_key\x18\x06\x20\x02(\x0cR\tpublicKey\"\xb4\x07\n\x0ePayme\
ntRequest\x12\x14\n\x05nonce\x18\x01\x20\x01(\x0cR\x05nonce\x12%\n\x0ere\
cipient_name\x18\x02\x20\x02(\tR\rrecipientName\x12R\n\x05memos\x18\x03\
\x20\x03(\x0b2<.hw.trezor.messages.common.PaymentRequest.PaymentRequestM\
@@ -4050,17 +4056,17 @@ static file_descriptor_proto_data: &'static [u8] = b"\
or.messages.common.PaymentRequest.CoinPurchaseMemoR\x10coinPurchaseMemo\
\x12e\n\x11text_details_memo\x18\x04\x20\x01(\x0b29.hw.trezor.messages.c\
ommon.PaymentRequest.TextDetailsMemoR\x0ftextDetailsMemo\x1a\x1e\n\x08Te\
- xtMemo\x12\x12\n\x04text\x18\x01\x20\x02(\tR\x04text\x1a?\n\x0fTextDetai\
- lsMemo\x12\x16\n\x05title\x18\x01\x20\x01(\t:\0R\x05title\x12\x14\n\x04t\
- ext\x18\x02\x20\x01(\t:\0R\x04text\x1aU\n\nRefundMemo\x12\x18\n\x07addre\
- ss\x18\x01\x20\x02(\tR\x07address\x12\x1b\n\taddress_n\x18\x02\x20\x03(\
- \rR\x08addressN\x12\x10\n\x03mac\x18\x03\x20\x02(\x0cR\x03mac\x1a\x90\
- \x01\n\x10CoinPurchaseMemo\x12\x1b\n\tcoin_type\x18\x01\x20\x02(\rR\x08c\
- oinType\x12\x16\n\x06amount\x18\x02\x20\x02(\tR\x06amount\x12\x18\n\x07a\
- ddress\x18\x03\x20\x02(\tR\x07address\x12\x1b\n\taddress_n\x18\x04\x20\
- \x03(\rR\x08addressN\x12\x10\n\x03mac\x18\x05\x20\x02(\x0cR\x03mac:\x04\
- \x88\xb2\x19\x01B>\n#com.satoshilabs.trezor.lib.protobufB\x13TrezorMessa\
- geCommon\x80\xa6\x1d\x01\
+ xtMemo\x12\x12\n\x04text\x18\x01\x20\x02(\tR\x04text\x1a;\n\x0fTextDetai\
+ lsMemo\x12\x14\n\x05title\x18\x01\x20\x02(\tR\x05title\x12\x12\n\x04text\
+ \x18\x02\x20\x02(\tR\x04text\x1aU\n\nRefundMemo\x12\x18\n\x07address\x18\
+ \x01\x20\x02(\tR\x07address\x12\x1b\n\taddress_n\x18\x02\x20\x03(\rR\x08\
+ addressN\x12\x10\n\x03mac\x18\x03\x20\x02(\x0cR\x03mac\x1a\x90\x01\n\x10\
+ CoinPurchaseMemo\x12\x1b\n\tcoin_type\x18\x01\x20\x02(\rR\x08coinType\
+ \x12\x16\n\x06amount\x18\x02\x20\x02(\tR\x06amount\x12\x18\n\x07address\
+ \x18\x03\x20\x02(\tR\x07address\x12\x1b\n\taddress_n\x18\x04\x20\x03(\rR\
+ \x08addressN\x12\x10\n\x03mac\x18\x05\x20\x02(\x0cR\x03mac:\x04\x88\xb2\
+ \x19\x01B>\n#com.satoshilabs.trezor.lib.protobufB\x13TrezorMessageCommon\
+ \x80\xa6\x1d\x01\
";
/// `FileDescriptorProto` object which was a source for this generated file
Why this scored 27/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.