eth: implement streaming for typed data messages
What changed, and why it matters
This commit extends an existing Ethereum data-streaming feature so it also works for EIP-712 typed messages with large data fields (e.g., a 50 KB 'bytes' field). Previously, only large Ethereum transaction data could be streamed in chunks; now the same chunking is applied to typed-message values. The change also improves on-screen display of long values by warning the user when a value is too large to show in full and by showing an ellipsis. It is a feature addition with defensive input checks, not a fix for a known vulnerability.
No immediate action required; this is a feature commit. Reviewers and users should ensure they upgrade both firmware and the Python library together, because the new `data_length` field is only meaningful on firmware that supports it. Wallets/integrators using the Python API will automatically benefit from streaming for large typed-message data.
Security signals we found
New protocol field `data_length` gated by size threshold
Streaming restricted to dynamic `bytes` type; other types rejected with `Error::InvalidInput`
Maximum streaming length capped at 1 MB (`MAX_TYPED_MSG_STREAMING_DATA_LENGTH`)
Mutually exclusive `value` and `data_length` enforced
User-facing warning when a value cannot be fully displayed
Extensive unit tests and generated cross-implementation test vectors added
Evidence from the diff
The patch adds a data_length field to ETHTypedMessageValueRequest in the protobuf definition and generated bindings. When the host reports a typed-message value larger than 6,144 bytes, the Python API now sends value=b"" with data_length set and then streams the actual bytes using the existing DataRequestChunk/DataResponseChunk protocol. The Rust firmware side (sign_typed_msg.rs) accepts streaming only for dynamic bytes fields (DataType::Bytes with size == 0), rejects streaming for other types and fixed-size bytes, enforces a 1 MB cap, and rejects the case where both value and data_length are set. It hashes the streamed chunks with Keccak-256 and shows a truncated preview plus a warning screen when the display body exceeds MAX_DISPLAY_SIZE (640 chars).
Changed components
Ethereum EIP-712 typed message signing flowProtobuf message `ETHTypedMessageValueRequest`Python BitBox02 API (`bitbox02.py`)Rust firmware `hww/api/ethereum/sign_typed_msg.rs`Rust generated protobuf code `shiftcrypto.bitbox02.rs`UI label component (`src/ui/components/label.h`)Test harness `hal/testing/ui.rs`Inspect captured patch +1155 / −72
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c2e5979..efdcd85 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,7 +8,7 @@ customers cannot upgrade their bootloader, its changes are recorded separately.
### [Unreleased]
- Improve experience of moving back and forth when entering password characters
-- Ethereum: add data streaming support for transactions with large (>6144 bytes) data
+- Ethereum: add data streaming support for transactions and EIP-712 typed messages with large data
- Ethereum: allow EIP-712 typed message signing without anti-klepto host nonce commitment
- Improve address display by grouping addresses into groups of 4 characters
diff --git a/messages/eth.proto b/messages/eth.proto
index ebff5c1..02c5c00 100644
--- a/messages/eth.proto
+++ b/messages/eth.proto
@@ -144,6 +144,9 @@ message ETHTypedMessageValueResponse {
message ETHTypedMessageValueRequest {
bytes value = 1;
+ // If non-zero, value should be empty and data will be streamed via
+ // DataRequestChunk/DataResponseChunk.
+ uint32 data_length = 2;
}
message ETHRequest {
diff --git a/py/bitbox02/bitbox02/bitbox02/bitbox02.py b/py/bitbox02/bitbox02/bitbox02/bitbox02.py
index 8c4e6a7..3122865 100644
--- a/py/bitbox02/bitbox02/bitbox02/bitbox02.py
+++ b/py/bitbox02/bitbox02/bitbox02/bitbox02.py
@@ -50,6 +50,8 @@ except ModuleNotFoundError:
HARDENED = 0x80000000
+STREAMING_THRESHOLD = 6144
+
Backup = Tuple[str, str, datetime]
@@ -928,9 +930,6 @@ class BitBox02(BitBoxCommonAPI):
return signature
- # Streaming threshold: use chunking for data larger than this
- streaming_threshold = 6144
-
if is_eip1559:
self._require_atleast(semver.VersionInfo(9, 16, 0))
(
@@ -952,7 +951,7 @@ class BitBox02(BitBoxCommonAPI):
f"chainID argument ({chain_id}) does not match chainID encoded in transaction ({decoded_chain_id_int})"
)
- require_streaming = len(data) > streaming_threshold
+ require_streaming = len(data) > STREAMING_THRESHOLD
if require_streaming:
self._require_atleast(semver.VersionInfo(9, 26, 0))
@@ -977,7 +976,7 @@ class BitBox02(BitBoxCommonAPI):
nonce, gas_price, gas_limit, recipient, value, data, _, _, _ = rlp.decode(transaction)
- require_streaming = len(data) > streaming_threshold
+ require_streaming = len(data) > STREAMING_THRESHOLD
if require_streaming:
self._require_atleast(semver.VersionInfo(9, 26, 0))
@@ -1229,15 +1228,25 @@ class BitBox02(BitBoxCommonAPI):
response = self._eth_msg_query(request)
while response.WhichOneof("response") == "typed_msg_value":
- response = self._eth_msg_query(
- eth.ETHRequest(
- typed_msg_value=eth.ETHTypedMessageValueRequest(
- value=get_value(
- response.typed_msg_value.root_object, response.typed_msg_value.path
+ value = get_value(response.typed_msg_value.root_object, response.typed_msg_value.path)
+ if len(value) > STREAMING_THRESHOLD:
+ response = self._eth_msg_query(
+ eth.ETHRequest(
+ typed_msg_value=eth.ETHTypedMessageValueRequest(
+ value=b"",
+ data_length=len(value),
),
- ),
+ )
+ )
+ response = self._handle_eth_chunking(response, value)
+ else:
+ response = self._eth_msg_query(
+ eth.ETHRequest(
+ typed_msg_value=eth.ETHTypedMessageValueRequest(
+ value=value,
+ ),
+ )
)
- )
if use_antiklepto:
assert response.WhichOneof("response") == "antiklepto_signer_commitment"
diff --git a/py/bitbox02/bitbox02/communication/generated/eth_pb2.py b/py/bitbox02/bitbox02/communication/generated/eth_pb2.py
index 3870633..d4d4aaf 100644
--- a/py/bitbox02/bitbox02/communication/generated/eth_pb2.py
+++ b/py/bitbox02/bitbox02/communication/generated/eth_pb2.py
@@ -15,17 +15,17 @@ from . import common_pb2 as common__pb2
from . import antiklepto_pb2 as antiklepto__pb2
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\teth.proto\x12\x14shiftcrypto.bitbox02\x1a\x0c\x63ommon.proto\x1a\x10\x61ntiklepto.proto\"\xf4\x01\n\rETHPubRequest\x12\x0f\n\x07keypath\x18\x01 \x03(\r\x12+\n\x04\x63oin\x18\x02 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.ETHCoin\x12\x43\n\x0boutput_type\x18\x03 \x01(\x0e\x32..shiftcrypto.bitbox02.ETHPubRequest.OutputType\x12\x0f\n\x07\x64isplay\x18\x04 \x01(\x08\x12\x18\n\x10\x63ontract_address\x18\x05 \x01(\x0c\x12\x10\n\x08\x63hain_id\x18\x06 \x01(\x04\"#\n\nOutputType\x12\x0b\n\x07\x41\x44\x44RESS\x10\x00\x12\x08\n\x04XPUB\x10\x01\"\xea\x02\n\x0e\x45THSignRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.ETHCoin\x12\x0f\n\x07keypath\x18\x02 \x03(\r\x12\r\n\x05nonce\x18\x03 \x01(\x0c\x12\x11\n\tgas_price\x18\x04 \x01(\x0c\x12\x11\n\tgas_limit\x18\x05 \x01(\x0c\x12\x11\n\trecipient\x18\x06 \x01(\x0c\x12\r\n\x05value\x18\x07 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x08 \x01(\x0c\x12R\n\x15host_nonce_commitment\x18\t \x01(\x0b\x32\x33.shiftcrypto.bitbox02.AntiKleptoHostNonceCommitment\x12\x10\n\x08\x63hain_id\x18\n \x01(\x04\x12:\n\x0c\x61\x64\x64ress_case\x18\x0b \x01(\x0e\x32$.shiftcrypto.bitbox02.ETHAddressCase\x12\x13\n\x0b\x64\x61ta_length\x18\x0c \x01(\r\"\xec\x02\n\x15\x45THSignEIP1559Request\x12\x10\n\x08\x63hain_id\x18\x01 \x01(\x04\x12\x0f\n\x07keypath\x18\x02 \x03(\r\x12\r\n\x05nonce\x18\x03 \x01(\x0c\x12 \n\x18max_priority_fee_per_gas\x18\x04 \x01(\x0c\x12\x17\n\x0fmax_fee_per_gas\x18\x05 \x01(\x0c\x12\x11\n\tgas_limit\x18\x06 \x01(\x0c\x12\x11\n\trecipient\x18\x07 \x01(\x0c\x12\r\n\x05value\x18\x08 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\t \x01(\x0c\x12R\n\x15host_nonce_commitment\x18\n \x01(\x0b\x32\x33.shiftcrypto.bitbox02.AntiKleptoHostNonceCommitment\x12:\n\x0c\x61\x64\x64ress_case\x18\x0b \x01(\x0e\x32$.shiftcrypto.bitbox02.ETHAddressCase\x12\x13\n\x0b\x64\x61ta_length\x18\x0c \x01(\r\"A\n\x1f\x45THSignDataRequestChunkResponse\x12\x0e\n\x06offset\x18\x01 \x01(\r\x12\x0e\n\x06length\x18\x02 \x01(\r\"0\n\x1f\x45THSignDataResponseChunkRequest\x12\r\n\x05\x63hunk\x18\x01 \x01(\x0c\"\xc8\x01\n\x15\x45THSignMessageRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.ETHCoin\x12\x0f\n\x07keypath\x18\x02 \x03(\r\x12\x0b\n\x03msg\x18\x03 \x01(\x0c\x12R\n\x15host_nonce_commitment\x18\x04 \x01(\x0b\x32\x33.shiftcrypto.bitbox02.AntiKleptoHostNonceCommitment\x12\x10\n\x08\x63hain_id\x18\x05 \x01(\x04\"$\n\x0f\x45THSignResponse\x12\x11\n\tsignature\x18\x01 \x01(\x0c\"\xfb\x05\n\x1a\x45THSignTypedMessageRequest\x12\x10\n\x08\x63hain_id\x18\x01 \x01(\x04\x12\x0f\n\x07keypath\x18\x02 \x03(\r\x12J\n\x05types\x18\x03 \x03(\x0b\x32;.shiftcrypto.bitbox02.ETHSignTypedMessageRequest.StructType\x12\x14\n\x0cprimary_type\x18\x04 \x01(\t\x12R\n\x15host_nonce_commitment\x18\x05 \x01(\x0b\x32\x33.shiftcrypto.bitbox02.AntiKleptoHostNonceCommitment\x1a\xc9\x01\n\nMemberType\x12G\n\x04type\x18\x01 \x01(\x0e\x32\x39.shiftcrypto.bitbox02.ETHSignTypedMessageRequest.DataType\x12\x0c\n\x04size\x18\x02 \x01(\r\x12\x13\n\x0bstruct_name\x18\x03 \x01(\t\x12O\n\narray_type\x18\x04 \x01(\x0b\x32;.shiftcrypto.bitbox02.ETHSignTypedMessageRequest.MemberType\x1a\x61\n\x06Member\x12\x0c\n\x04name\x18\x01 \x01(\t\x12I\n\x04type\x18\x02 \x01(\x0b\x32;.shiftcrypto.bitbox02.ETHSignTypedMessageRequest.MemberType\x1a\x64\n\nStructType\x12\x0c\n\x04name\x18\x01 \x01(\t\x12H\n\x07members\x18\x02 \x03(\x0b\x32\x37.shiftcrypto.bitbox02.ETHSignTypedMessageRequest.Member\"o\n\x08\x44\x61taType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\t\n\x05\x42YTES\x10\x01\x12\x08\n\x04UINT\x10\x02\x12\x07\n\x03INT\x10\x03\x12\x08\n\x04\x42OOL\x10\x04\x12\x0b\n\x07\x41\x44\x44RESS\x10\x05\x12\n\n\x06STRING\x10\x06\x12\t\n\x05\x41RRAY\x10\x07\x12\n\n\x06STRUCT\x10\x08\"\xb4\x01\n\x1c\x45THTypedMessageValueResponse\x12R\n\x0broot_object\x18\x01 \x01(\x0e\x32=.shiftcrypto.bitbox02.ETHTypedMessageValueResponse.RootObject\x12\x0c\n\x04path\x18\x02 \x03(\r\"2\n\nRootObject\x12\x0b\n\x07UNKNOWN\x10\x00\x12\n\n\x06\x44OMAIN\x10\x01\x12\x0b\n\x07MESSAGE\x10\x02\",\n\x1b\x45THTypedMessageValueRequest\x12\r\n\x05value\x18\x01 \x01(\x0c\"\xc9\x04\n\nETHRequest\x12\x32\n\x03pub\x18\x01 \x01(\x0b\x32#.shiftcrypto.bitbox02.ETHPubRequestH\x00\x12\x34\n\x04sign\x18\x02 \x01(\x0b\x32$.shiftcrypto.bitbox02.ETHSignRequestH\x00\x12?\n\x08sign_msg\x18\x03 \x01(\x0b\x32+.shiftcrypto.bitbox02.ETHSignMessageRequestH\x00\x12P\n\x14\x61ntiklepto_signature\x18\x04 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.AntiKleptoSignatureRequestH\x00\x12J\n\x0esign_typed_msg\x18\x05 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.ETHSignTypedMessageRequestH\x00\x12L\n\x0ftyped_msg_value\x18\x06 \x01(\x0b\x32\x31.shiftcrypto.bitbox02.ETHTypedMessageValueRequestH\x00\x12\x43\n\x0csign_eip1559\x18\x07 \x01(\x0b\x32+.shiftcrypto.bitbox02.ETHSignEIP1559RequestH\x00\x12T\n\x13\x64\x61ta_response_chunk\x18\x08 \x01(\x0b\x32\x35.shiftcrypto.bitbox02.ETHSignDataResponseChunkRequestH\x00\x42\t\n\x07request\"\x80\x03\n\x0b\x45THResponse\x12\x30\n\x03pub\x18\x01 \x01(\x0b\x32!.shiftcrypto.bitbox02.PubResponseH\x00\x12\x35\n\x04sign\x18\x02 \x01(\x0b\x32%.shiftcrypto.bitbox02.ETHSignResponseH\x00\x12X\n\x1c\x61ntiklepto_signer_commitment\x18\x03 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.AntiKleptoSignerCommitmentH\x00\x12M\n\x0ftyped_msg_value\x18\x04 \x01(\x0b\x32\x32.shiftcrypto.bitbox02.ETHTypedMessageValueResponseH\x00\x12S\n\x12\x64\x61ta_request_chunk\x18\x05 \x01(\x0b\x32\x35.shiftcrypto.bitbox02.ETHSignDataRequestChunkResponseH\x00\x42\n\n\x08response*2\n\x07\x45THCoin\x12\x07\n\x03\x45TH\x10\x00\x12\x0e\n\nRopstenETH\x10\x01\x12\x0e\n\nRinkebyETH\x10\x02*d\n\x0e\x45THAddressCase\x12\x1a\n\x16\x45TH_ADDRESS_CASE_MIXED\x10\x00\x12\x1a\n\x16\x45TH_ADDRESS_CASE_UPPER\x10\x01\x12\x1a\n\x16\x45TH_ADDRESS_CASE_LOWER\x10\x02\x62\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\teth.proto\x12\x14shiftcrypto.bitbox02\x1a\x0c\x63ommon.proto\x1a\x10\x61ntiklepto.proto\"\xf4\x01\n\rETHPubRequest\x12\x0f\n\x07keypath\x18\x01 \x03(\r\x12+\n\x04\x63oin\x18\x02 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.ETHCoin\x12\x43\n\x0boutput_type\x18\x03 \x01(\x0e\x32..shiftcrypto.bitbox02.ETHPubRequest.OutputType\x12\x0f\n\x07\x64isplay\x18\x04 \x01(\x08\x12\x18\n\x10\x63ontract_address\x18\x05 \x01(\x0c\x12\x10\n\x08\x63hain_id\x18\x06 \x01(\x04\"#\n\nOutputType\x12\x0b\n\x07\x41\x44\x44RESS\x10\x00\x12\x08\n\x04XPUB\x10\x01\"\xea\x02\n\x0e\x45THSignRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.ETHCoin\x12\x0f\n\x07keypath\x18\x02 \x03(\r\x12\r\n\x05nonce\x18\x03 \x01(\x0c\x12\x11\n\tgas_price\x18\x04 \x01(\x0c\x12\x11\n\tgas_limit\x18\x05 \x01(\x0c\x12\x11\n\trecipient\x18\x06 \x01(\x0c\x12\r\n\x05value\x18\x07 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x08 \x01(\x0c\x12R\n\x15host_nonce_commitment\x18\t \x01(\x0b\x32\x33.shiftcrypto.bitbox02.AntiKleptoHostNonceCommitment\x12\x10\n\x08\x63hain_id\x18\n \x01(\x04\x12:\n\x0c\x61\x64\x64ress_case\x18\x0b \x01(\x0e\x32$.shiftcrypto.bitbox02.ETHAddressCase\x12\x13\n\x0b\x64\x61ta_length\x18\x0c \x01(\r\"\xec\x02\n\x15\x45THSignEIP1559Request\x12\x10\n\x08\x63hain_id\x18\x01 \x01(\x04\x12\x0f\n\x07keypath\x18\x02 \x03(\r\x12\r\n\x05nonce\x18\x03 \x01(\x0c\x12 \n\x18max_priority_fee_per_gas\x18\x04 \x01(\x0c\x12\x17\n\x0fmax_fee_per_gas\x18\x05 \x01(\x0c\x12\x11\n\tgas_limit\x18\x06 \x01(\x0c\x12\x11\n\trecipient\x18\x07 \x01(\x0c\x12\r\n\x05value\x18\x08 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\t \x01(\x0c\x12R\n\x15host_nonce_commitment\x18\n \x01(\x0b\x32\x33.shiftcrypto.bitbox02.AntiKleptoHostNonceCommitment\x12:\n\x0c\x61\x64\x64ress_case\x18\x0b \x01(\x0e\x32$.shiftcrypto.bitbox02.ETHAddressCase\x12\x13\n\x0b\x64\x61ta_length\x18\x0c \x01(\r\"A\n\x1f\x45THSignDataRequestChunkResponse\x12\x0e\n\x06offset\x18\x01 \x01(\r\x12\x0e\n\x06length\x18\x02 \x01(\r\"0\n\x1f\x45THSignDataResponseChunkRequest\x12\r\n\x05\x63hunk\x18\x01 \x01(\x0c\"\xc8\x01\n\x15\x45THSignMessageRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.ETHCoin\x12\x0f\n\x07keypath\x18\x02 \x03(\r\x12\x0b\n\x03msg\x18\x03 \x01(\x0c\x12R\n\x15host_nonce_commitment\x18\x04 \x01(\x0b\x32\x33.shiftcrypto.bitbox02.AntiKleptoHostNonceCommitment\x12\x10\n\x08\x63hain_id\x18\x05 \x01(\x04\"$\n\x0f\x45THSignResponse\x12\x11\n\tsignature\x18\x01 \x01(\x0c\"\xfb\x05\n\x1a\x45THSignTypedMessageRequest\x12\x10\n\x08\x63hain_id\x18\x01 \x01(\x04\x12\x0f\n\x07keypath\x18\x02 \x03(\r\x12J\n\x05types\x18\x03 \x03(\x0b\x32;.shiftcrypto.bitbox02.ETHSignTypedMessageRequest.StructType\x12\x14\n\x0cprimary_type\x18\x04 \x01(\t\x12R\n\x15host_nonce_commitment\x18\x05 \x01(\x0b\x32\x33.shiftcrypto.bitbox02.AntiKleptoHostNonceCommitment\x1a\xc9\x01\n\nMemberType\x12G\n\x04type\x18\x01 \x01(\x0e\x32\x39.shiftcrypto.bitbox02.ETHSignTypedMessageRequest.DataType\x12\x0c\n\x04size\x18\x02 \x01(\r\x12\x13\n\x0bstruct_name\x18\x03 \x01(\t\x12O\n\narray_type\x18\x04 \x01(\x0b\x32;.shiftcrypto.bitbox02.ETHSignTypedMessageRequest.MemberType\x1a\x61\n\x06Member\x12\x0c\n\x04name\x18\x01 \x01(\t\x12I\n\x04type\x18\x02 \x01(\x0b\x32;.shiftcrypto.bitbox02.ETHSignTypedMessageRequest.MemberType\x1a\x64\n\nStructType\x12\x0c\n\x04name\x18\x01 \x01(\t\x12H\n\x07members\x18\x02 \x03(\x0b\x32\x37.shiftcrypto.bitbox02.ETHSignTypedMessageRequest.Member\"o\n\x08\x44\x61taType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\t\n\x05\x42YTES\x10\x01\x12\x08\n\x04UINT\x10\x02\x12\x07\n\x03INT\x10\x03\x12\x08\n\x04\x42OOL\x10\x04\x12\x0b\n\x07\x41\x44\x44RESS\x10\x05\x12\n\n\x06STRING\x10\x06\x12\t\n\x05\x41RRAY\x10\x07\x12\n\n\x06STRUCT\x10\x08\"\xb4\x01\n\x1c\x45THTypedMessageValueResponse\x12R\n\x0broot_object\x18\x01 \x01(\x0e\x32=.shiftcrypto.bitbox02.ETHTypedMessageValueResponse.RootObject\x12\x0c\n\x04path\x18\x02 \x03(\r\"2\n\nRootObject\x12\x0b\n\x07UNKNOWN\x10\x00\x12\n\n\x06\x44OMAIN\x10\x01\x12\x0b\n\x07MESSAGE\x10\x02\"A\n\x1b\x45THTypedMessageValueRequest\x12\r\n\x05value\x18\x01 \x01(\x0c\x12\x13\n\x0b\x64\x61ta_length\x18\x02 \x01(\r\"\xc9\x04\n\nETHRequest\x12\x32\n\x03pub\x18\x01 \x01(\x0b\x32#.shiftcrypto.bitbox02.ETHPubRequestH\x00\x12\x34\n\x04sign\x18\x02 \x01(\x0b\x32$.shiftcrypto.bitbox02.ETHSignRequestH\x00\x12?\n\x08sign_msg\x18\x03 \x01(\x0b\x32+.shiftcrypto.bitbox02.ETHSignMessageRequestH\x00\x12P\n\x14\x61ntiklepto_signature\x18\x04 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.AntiKleptoSignatureRequestH\x00\x12J\n\x0esign_typed_msg\x18\x05 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.ETHSignTypedMessageRequestH\x00\x12L\n\x0ftyped_msg_value\x18\x06 \x01(\x0b\x32\x31.shiftcrypto.bitbox02.ETHTypedMessageValueRequestH\x00\x12\x43\n\x0csign_eip1559\x18\x07 \x01(\x0b\x32+.shiftcrypto.bitbox02.ETHSignEIP1559RequestH\x00\x12T\n\x13\x64\x61ta_response_chunk\x18\x08 \x01(\x0b\x32\x35.shiftcrypto.bitbox02.ETHSignDataResponseChunkRequestH\x00\x42\t\n\x07request\"\x80\x03\n\x0b\x45THResponse\x12\x30\n\x03pub\x18\x01 \x01(\x0b\x32!.shiftcrypto.bitbox02.PubResponseH\x00\x12\x35\n\x04sign\x18\x02 \x01(\x0b\x32%.shiftcrypto.bitbox02.ETHSignResponseH\x00\x12X\n\x1c\x61ntiklepto_signer_commitment\x18\x03 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.AntiKleptoSignerCommitmentH\x00\x12M\n\x0ftyped_msg_value\x18\x04 \x01(\x0b\x32\x32.shiftcrypto.bitbox02.ETHTypedMessageValueResponseH\x00\x12S\n\x12\x64\x61ta_request_chunk\x18\x05 \x01(\x0b\x32\x35.shiftcrypto.bitbox02.ETHSignDataRequestChunkResponseH\x00\x42\n\n\x08response*2\n\x07\x45THCoin\x12\x07\n\x03\x45TH\x10\x00\x12\x0e\n\nRopstenETH\x10\x01\x12\x0e\n\nRinkebyETH\x10\x02*d\n\x0e\x45THAddressCase\x12\x1a\n\x16\x45TH_ADDRESS_CASE_MIXED\x10\x00\x12\x1a\n\x16\x45TH_ADDRESS_CASE_UPPER\x10\x01\x12\x1a\n\x16\x45TH_ADDRESS_CASE_LOWER\x10\x02\x62\x06proto3')
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'eth_pb2', globals())
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
- _ETHCOIN._serialized_start=3374
- _ETHCOIN._serialized_end=3424
- _ETHADDRESSCASE._serialized_start=3426
- _ETHADDRESSCASE._serialized_end=3526
+ _ETHCOIN._serialized_start=3395
+ _ETHCOIN._serialized_end=3445
+ _ETHADDRESSCASE._serialized_start=3447
+ _ETHADDRESSCASE._serialized_end=3547
_ETHPUBREQUEST._serialized_start=68
_ETHPUBREQUEST._serialized_end=312
_ETHPUBREQUEST_OUTPUTTYPE._serialized_start=277
@@ -57,9 +57,9 @@ if _descriptor._USE_C_DESCRIPTORS == False:
_ETHTYPEDMESSAGEVALUERESPONSE_ROOTOBJECT._serialized_start=2301
_ETHTYPEDMESSAGEVALUERESPONSE_ROOTOBJECT._serialized_end=2351
_ETHTYPEDMESSAGEVALUEREQUEST._serialized_start=2353
- _ETHTYPEDMESSAGEVALUEREQUEST._serialized_end=2397
- _ETHREQUEST._serialized_start=2400
- _ETHREQUEST._serialized_end=2985
- _ETHRESPONSE._serialized_start=2988
- _ETHRESPONSE._serialized_end=3372
+ _ETHTYPEDMESSAGEVALUEREQUEST._serialized_end=2418
+ _ETHREQUEST._serialized_start=2421
+ _ETHREQUEST._serialized_end=3006
+ _ETHRESPONSE._serialized_start=3009
+ _ETHRESPONSE._serialized_end=3393
# @@protoc_insertion_point(module_scope)
diff --git a/py/bitbox02/bitbox02/communication/generated/eth_pb2.pyi b/py/bitbox02/bitbox02/communication/generated/eth_pb2.pyi
index 0e83542..5b1bb65 100644
--- a/py/bitbox02/bitbox02/communication/generated/eth_pb2.pyi
+++ b/py/bitbox02/bitbox02/communication/generated/eth_pb2.pyi
@@ -467,13 +467,19 @@ class ETHTypedMessageValueRequest(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
VALUE_FIELD_NUMBER: builtins.int
+ DATA_LENGTH_FIELD_NUMBER: builtins.int
value: builtins.bytes
+ data_length: builtins.int
+ """If non-zero, value should be empty and data will be streamed via
+ DataRequestChunk/DataResponseChunk.
+ """
def __init__(
self,
*,
value: builtins.bytes = ...,
+ data_length: builtins.int = ...,
) -> None: ...
- def ClearField(self, field_name: typing.Literal["value", b"value"]) -> None: ...
+ def ClearField(self, field_name: typing.Literal["data_length", b"data_length", "value", b"value"]) -> None: ...
global___ETHTypedMessageValueRequest = ETHTypedMessageValueRequest
diff --git a/py/send_message.py b/py/send_message.py
index bbd8022..642ac9d 100755
--- a/py/send_message.py
+++ b/py/send_message.py
@@ -15,6 +15,7 @@ import binascii
import textwrap
import json
from pathlib import Path
+import os
import requests
import hid
@@ -1227,6 +1228,52 @@ class SendMessage:
print("Signature: 0x{}".format(binascii.hexlify(sig).decode("utf-8")))
+ def _sign_eth_typed_message_large_data(self) -> None:
+ large_data = "0x" + os.urandom(50000).hex()
+ msg = {
+ "types": {
+ "EIP712Domain": [
+ {"name": "chainId", "type": "uint256"},
+ {"name": "verifyingContract", "type": "address"},
+ ],
+ "SafeTx": [
+ {"name": "to", "type": "address"},
+ {"name": "value", "type": "uint256"},
+ {"name": "data", "type": "bytes"},
+ {"name": "operation", "type": "uint8"},
+ {"name": "safeTxGas", "type": "uint256"},
+ {"name": "baseGas", "type": "uint256"},
+ {"name": "gasPrice", "type": "uint256"},
+ {"name": "gasToken", "type": "address"},
+ {"name": "refundReceiver", "type": "address"},
+ {"name": "nonce", "type": "uint256"},
+ ],
+ },
+ "primaryType": "SafeTx",
+ "domain": {
+ "chainId": "1",
+ "verifyingContract": "0x0000000000000000000000000000000000000000",
+ },
+ "message": {
+ "to": "0x0000000000000000000000000000000000000000",
+ "value": "0",
+ "data": large_data,
+ "operation": "0",
+ "safeTxGas": "0",
+ "baseGas": "0",
+ "gasPrice": "0",
+ "gasToken": "0x0000000000000000000000000000000000000000",
+ "refundReceiver": "0x0000000000000000000000000000000000000000",
+ "nonce": "3",
+ },
+ }
+ print(f"Signing SafeTx with {len(large_data)//2 - 1} bytes of data (streaming)")
+ sig = self._device.eth_sign_typed_msg(
+ keypath=[44 + HARDENED, 60 + HARDENED, 0 + HARDENED, 0, 0],
+ msg=msg,
+ )
+ print("Signature: 0x{}".format(binascii.hexlify(sig).decode("utf-8")))
+
def _cardano(self) -> None:
def xpubs() -> None:
xpubs = self._device.cardano_xpubs(
@@ -1593,6 +1640,10 @@ class SendMessage:
("Sign Ethereum tx", self._sign_eth_tx),
("Sign Ethereum Message", self._sign_eth_message),
("Sign Ethereum Typed Message (EIP-712)", self._sign_eth_typed_message),
+ (
+ "Sign Ethereum Typed Message (50KB streaming) ",
+ self._sign_eth_typed_message_large_data,
+ ),
("Cardano", self._cardano),
("Show Electrum wallet encryption key", self._get_electrum_encryption_key),
("BIP85 - BIP39", self._bip85_bip39),
diff --git a/src/rust/bitbox02-rust/src/hal/testing/ui.rs b/src/rust/bitbox02-rust/src/hal/testing/ui.rs
index 96e9d2d..716bb92 100644
--- a/src/rust/bitbox02-rust/src/hal/testing/ui.rs
+++ b/src/rust/bitbox02-rust/src/hal/testing/ui.rs
@@ -56,6 +56,7 @@ type TrinaryChoiceCb<'a> =
pub struct TestingUi<'a> {
_abort_nth: Option<usize>,
pub screens: Vec<Screen>,
+ pub confirm_display_sizes: Vec<usize>,
_enter_string: Option<EnterStringCb<'a>>,
_menu: Option<MenuCb<'a>>,
_trinary_choice: Option<TrinaryChoiceCb<'a>>,
@@ -85,6 +86,7 @@ impl Ui for TestingUi<'_> {
}
async fn confirm(&mut self, params: &ConfirmParams<'_>) -> Result<(), UserAbort> {
+ self.confirm_display_sizes.push(params.display_size);
self.screens.push(Screen::Confirm {
title: params.title.into(),
body: params.body.into(),
@@ -238,6 +240,7 @@ impl<'a> TestingUi<'a> {
pub fn new() -> Self {
Self {
screens: vec![],
+ confirm_display_sizes: vec![],
_abort_nth: None,
_enter_string: None,
_menu: None,
diff --git a/src/rust/bitbox02-rust/src/hww/api/ethereum/sign_typed_msg.rs b/src/rust/bitbox02-rust/src/hww/api/ethereum/sign_typed_msg.rs
index 8b13c23..9a370f0 100644
--- a/src/rust/bitbox02-rust/src/hww/api/ethereum/sign_typed_msg.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/ethereum/sign_typed_msg.rs
@@ -9,6 +9,7 @@
use super::Error;
use super::pb;
+use super::sighash::DataProducer;
use crate::hal::Ui;
use crate::hal::ui::ConfirmParams;
@@ -30,6 +31,11 @@ use pb::eth_typed_message_value_response::RootObject;
const DOMAIN_TYPE_NAME: &str = "EIP712Domain";
+const MAX_TYPED_MSG_STREAMING_DATA_LENGTH: u32 = 1024 * 1024;
+
+// If changed, keep in sync with MAX_LABEL_SIZE.
+const MAX_DISPLAY_SIZE: usize = 640;
+
fn get_type<'a>(types: &'a [StructType], name: &str) -> Option<&'a StructType> {
types.iter().find(|t| t.name == name)
}
@@ -166,14 +172,17 @@ fn type_hash(types: &[StructType], name: &str) -> Result<Vec<u8>, Error> {
.to_vec())
}
-async fn get_value_from_host(root_object: RootObject, path: &[u32]) -> Result<Vec<u8>, Error> {
+async fn get_value_from_host(
+ root_object: RootObject,
+ path: &[u32],
+) -> Result<pb::EthTypedMessageValueRequest, Error> {
let request = super::next_request(Response::TypedMsgValue(pb::EthTypedMessageValueResponse {
root_object: root_object as _,
path: path.to_vec(),
}))
.await?;
match request {
- Request::TypedMsgValue(pb::EthTypedMessageValueRequest { value }) => Ok(value.clone()),
+ Request::TypedMsgValue(req) => Ok(req),
_ => Err(Error::InvalidInput),
}
}
@@ -262,6 +271,42 @@ fn confirm_title(root_object: RootObject) -> &'static str {
}
}
+fn format_display_line_prefix(display_path: &str, line_num: usize, num_lines: usize) -> String {
+ if num_lines > 1 {
+ format!("{display_path}, line {}/{}", line_num + 1, num_lines)
+ } else {
+ display_path.to_string()
+ }
+}
+
+fn format_display_line_body(
+ display_path: &str,
+ line_num: usize,
+ num_lines: usize,
+ line: &str,
+) -> String {
+ format!(
+ "{}: {}",
+ format_display_line_prefix(display_path, line_num, num_lines),
+ line
+ )
+}
+
+/// Returns how many bytes of a streamed `bytes` value to include in the preview body.
+///
+/// The preview is rendered as `<path>: 0x<hex>`. We first compute how many full bytes fit into the
+/// body without truncation. If the full value is longer than that, we include one additional byte
+/// so the body exceeds `MAX_DISPLAY_SIZE` and the UI appends `...`.
+fn streaming_display_byte_cap(display_path: &str, data_length: usize) -> usize {
+ let body_prefix = format!("{}: 0x", format_display_line_prefix(display_path, 0, 1));
+ let hex_chars_budget = MAX_DISPLAY_SIZE.saturating_sub(body_prefix.len());
+ let bytes_that_fit = hex_chars_budget / 2;
+ let needs_ellipsis = data_length > bytes_that_fit;
+ let preview_bytes = bytes_that_fit + usize::from(needs_ellipsis);
+
+ preview_bytes.min(data_length)
+}
+
#[allow(clippy::too_many_arguments)]
async fn encode_member<U: sha3::digest::Update>(
hal: &mut impl crate::hal::Hal,
@@ -298,10 +343,60 @@ async fn encode_member<U: sha3::digest::Update>(
.await?;
hasher.update(&encoded_value);
} else {
- let value = get_value_from_host(root_object, path).await?;
- let (value_encoded, value_formatted) = encode_value(member_type, value)?;
+ let req = get_value_from_host(root_object, path).await?;
+ let display_path = formatted_path.join(".");
+ let data_type = DataType::try_from(member_type.r#type)?;
+ let mut display_size = 0usize;
+
+ let value_formatted = if req.data_length > 0 {
+ // Streaming mode: only dynamic bytes may stream.
+ if data_type != DataType::Bytes || member_type.size != 0 {
+ return Err(Error::InvalidInput);
+ }
+ if !req.value.is_empty() {
+ return Err(Error::InvalidInput);
+ }
+ if req.data_length > MAX_TYPED_MSG_STREAMING_DATA_LENGTH {
+ return Err(Error::InvalidInput);
+ }
+
+ display_size = req.data_length as usize;
+ let display_cap = streaming_display_byte_cap(&display_path, display_size);
+ let mut producer = super::sighash::ChunkingProducer::from_host(req.data_length);
+ let mut keccak = sha3::Keccak256::new();
+ let mut display_buf: Vec<u8> = Vec::new();
+ while let Some(chunk) = producer.next().await? {
+ keccak.update(&chunk);
+ let n = (display_cap - display_buf.len()).min(chunk.len());
+ display_buf.extend_from_slice(&chunk[..n]);
+ }
+ hasher.update(&keccak.finalize());
+
+ format!("0x{}", hex::encode(&display_buf))
+ } else {
+ let value_len = req.value.len();
+ let (value_encoded, fmt) = encode_value(member_type, req.value)?;
+ hasher.update(&value_encoded);
+ if data_type == DataType::Bytes {
+ display_size = value_len;
+ }
+ fmt
+ };
+
+ // Display value, splitting multi-line strings into separate screens.
let lines: Vec<&str> = value_formatted.split('\n').collect();
for (i, &line) in lines.iter().enumerate() {
+ let body = format_display_line_body(&display_path, i, lines.len(), line);
+ if body.len() > MAX_DISPLAY_SIZE {
+ hal.ui()
+ .confirm(&ConfirmParams {
+ title: "Warning",
+ body: "The next value is\ntoo large to display\nin full",
+ accept_is_nextarrow: true,
+ ..Default::default()
+ })
+ .await?;
+ }
hal.ui()
.confirm(&ConfirmParams {
title: &format!(
@@ -309,23 +404,14 @@ async fn encode_member<U: sha3::digest::Update>(
confirm_title(root_object),
title_suffix.as_deref().unwrap_or("")
),
- body: &format!(
- "{}{}: {}",
- formatted_path.join("."),
- if lines.len() > 1 {
- format!(", line {}/{}", i + 1, lines.len())
- } else {
- "".into()
- },
- line
- ),
+ body: &body,
scrollable: true,
+ display_size,
accept_is_nextarrow: true,
..Default::default()
})
.await?;
}
- hasher.update(&value_encoded);
}
Ok(())
}
@@ -342,8 +428,11 @@ async fn hash_array(
let array_size = if member_type.size > 0 {
member_type.size
} else {
- let array_size_encoded = get_value_from_host(root_object, path).await?;
- u32::from_be_bytes(array_size_encoded.try_into().or(Err(Error::InvalidInput))?)
+ let req = get_value_from_host(root_object, path).await?;
+ if req.data_length > 0 {
+ return Err(Error::InvalidInput);
+ }
+ u32::from_be_bytes(req.value.try_into().or(Err(Error::InvalidInput))?)
};
let array_type = member_type.array_type.as_ref().ok_or(Error::InvalidInput)?;
@@ -456,8 +545,11 @@ async fn validate_chain_id(request: &pb::EthSignTypedMessageRequest) -> Result<(
return Ok(());
}
};
- let encoded_value = get_value_from_host(RootObject::Domain, &[chain_id_index as u32]).await?;
- let chain_id: u64 = BigUint::from_bytes_be(&encoded_value)
+ let req = get_value_from_host(RootObject::Domain, &[chain_id_index as u32]).await?;
+ if req.data_length > 0 {
+ return Err(Error::InvalidInput);
+ }
+ let chain_id: u64 = BigUint::from_bytes_be(&req.value)
.try_into()
.or(Err(Error::InvalidInput))?;
if chain_id != request.chain_id {
@@ -662,6 +754,52 @@ mod tests {
]
}
+ fn run_single_message_typed_msg(
+ member_type: MemberType,
+ message_obj: Object<'static>,
+ ) -> TestingHal<'static> {
+ let typed_msg = alloc::rc::Rc::new(core::cell::RefCell::new(TypedMessage::new(
+ vec![
+ StructType {
+ name: "EIP712Domain".into(),
+ members: vec![mk_member("name", mk_type(DataType::String))],
+ },
+ StructType {
+ name: "Msg".into(),
+ members: vec![mk_member("data", member_type)],
+ },
+ ],
+ "Msg",
+ Object::Struct(vec![Object::String("test")]),
+ Object::Struct(vec![message_obj]),
+ )));
+ {
+ let typed_msg = typed_msg.clone();
+ *crate::hww::MOCK_NEXT_REQUEST.0.borrow_mut() = Some(Box::new(move |response| {
+ let typed_msg = typed_msg.borrow();
+ Ok(typed_msg.handle_host_response(&response).unwrap())
+ }));
+ }
+ let typed_msg = typed_msg.borrow();
+ let mut mock_hal = TestingHal::new();
+ block_on(eip712_sighash(
+ &mut mock_hal,
+ &typed_msg.types,
+ typed_msg.primary_type,
+ ))
+ .unwrap();
+ mock_hal
+ }
+
+ fn run_single_string_message(message: String) -> TestingHal<'static> {
+ let message = Box::leak(message.into_boxed_str());
+ run_single_message_typed_msg(mk_type(DataType::String), Object::String(message))
+ }
+
+ fn run_single_streaming_bytes_message(data: Vec<u8>) -> TestingHal<'static> {
+ run_single_message_typed_msg(mk_type(DataType::Bytes), Object::StreamingBytes(data))
+ }
+
/// A utility structure to build domain/message objects for testing.
enum Object<'a> {
String(&'a str),
@@ -671,6 +809,7 @@ mod tests {
BigUint(BigUint),
List(Vec<Object<'a>>),
Struct(Vec<Object<'a>>),
+ StreamingBytes(Vec<u8>),
}
impl Object<'_> {
@@ -682,6 +821,7 @@ mod tests {
Object::BigInt(i) => i.to_signed_bytes_be(),
Object::BigUint(i) => i.to_bytes_be(),
Object::List(l) => (l.len() as u32).to_be_bytes().to_vec(),
+ Object::StreamingBytes(_) => panic!("streaming values use get_value_protobuf"),
_ => panic!("unexpected"),
}
}
@@ -697,26 +837,76 @@ mod tests {
}
}
+ fn get_object(&self, path: &[u32]) -> &Object<'_> {
+ if path.is_empty() {
+ return self;
+ }
+ match self {
+ Object::Struct(l) | Object::List(l) => l[path[0] as usize].get_object(&path[1..]),
+ _ => panic!("unexpected"),
+ }
+ }
+
fn get_value_protobuf(&self, path: &[u32]) -> pb::request::Request {
- pb::request::Request::Eth(pb::EthRequest {
- request: Some(Request::TypedMsgValue(pb::EthTypedMessageValueRequest {
- value: self.get_value(path),
- })),
- })
+ let obj = self.get_object(path);
+ match obj {
+ Object::StreamingBytes(data) => pb::request::Request::Eth(pb::EthRequest {
+ request: Some(Request::TypedMsgValue(pb::EthTypedMessageValueRequest {
+ value: vec![],
+ data_length: data.len() as u32,
+ })),
+ }),
+ _ => pb::request::Request::Eth(pb::EthRequest {
+ request: Some(Request::TypedMsgValue(pb::EthTypedMessageValueRequest {
+ value: self.get_value(path),
+ data_length: 0,
+ })),
+ }),
+ }
+ }
+
+ fn get_streaming_data(&self, path: &[u32]) -> Option<&[u8]> {
+ let obj = self.get_object(path);
+ match obj {
+ Object::StreamingBytes(data) => Some(data),
+ _ => None,
+ }
}
}
+ struct StreamingState {
+ data: Vec<u8>,
+ }
+
/// Typed message to be signed, as it would exist on the host.
struct TypedMessage<'a> {
types: Vec<StructType>,
primary_type: &'a str,
domain: Object<'a>,
message: Object<'a>,
+ streaming: core::cell::RefCell<Option<StreamingState>>,
+ }
+
+ impl<'a> TypedMessage<'a> {
+ fn new(
+ types: Vec<StructType>,
+ primary_type: &'a str,
+ domain: Object<'a>,
+ message: Object<'a>,
+ ) -> Self {
+ TypedMessage {
+ types,
+ primary_type,
+ domain,
+ message,
+ streaming: core::cell::RefCell::new(None),
+ }
+ }
}
impl TypedMessage<'_> {
/// The host is asked for a value at a member of an object. This handles this request and
- /// responds with value.
+ /// responds with value. Also handles DataRequestChunk for streaming.
fn handle_host_response(
&self,
response: &pb::response::Response,
@@ -728,11 +918,36 @@ mod tests {
root_object,
path,
})),
- }) => match RootObject::try_from(*root_object).unwrap() {
- RootObject::Domain => return Some(self.domain.get_value_protobuf(path)),
- RootObject::Message => return Some(self.message.get_value_protobuf(path)),
- _ => {}
- },
+ }) => {
+ let obj = match RootObject::try_from(*root_object).unwrap() {
+ RootObject::Domain => &self.domain,
+ RootObject::Message => &self.message,
+ _ => return None,
+ };
+ // If this value should stream, set up streaming state.
+ if let Some(data) = obj.get_streaming_data(path) {
+ *self.streaming.borrow_mut() = Some(StreamingState {
+ data: data.to_vec(),
+ });
+ }
+ return Some(obj.get_value_protobuf(path));
+ }
+ pb::response::Response::Eth(pb::EthResponse {
+ response: Some(Response::DataRequestChunk(req)),
+ }) => {
+ let mut streaming = self.streaming.borrow_mut();
+ let state = streaming
+ .as_mut()
+ .expect("chunk request without streaming state");
+ let offset = req.offset as usize;
+ let length = req.length as usize;
+ let chunk = state.data[offset..offset + length].to_vec();
+ return Some(pb::request::Request::Eth(pb::EthRequest {
+ request: Some(Request::DataResponseChunk(
+ pb::EthSignDataResponseChunkRequest { chunk },
+ )),
+ }));
+ }
_ => {}
}
None
@@ -931,6 +1146,112 @@ mod tests {
);
}
+ #[test]
+ fn test_streaming_display_byte_cap() {
+ let exact_fit = streaming_display_byte_cap("data", 316);
+ assert_eq!(exact_fit, 316);
+ let exact_fit_body = format_display_line_body(
+ "data",
+ 0,
+ 1,
+ &format!("0x{}", hex::encode(vec![0u8; exact_fit])),
+ );
+ assert_eq!(exact_fit_body.len(), MAX_DISPLAY_SIZE);
+
+ let truncated = streaming_display_byte_cap("data", 10_000);
+ assert_eq!(truncated, 317);
+ let truncated_body = format_display_line_body(
+ "data",
+ 0,
+ 1,
+ &format!("0x{}", hex::encode(vec![0u8; truncated])),
+ );
+ assert!(truncated_body.len() > MAX_DISPLAY_SIZE);
+ }
+
+ #[test]
+ fn test_multiline_warning_not_shown_when_each_line_fits() {
+ let line1 = "a".repeat(400);
+ let line2 = "b".repeat(300);
+ let mock_hal = run_single_string_message(format!("{line1}\n{line2}"));
+
+ assert_eq!(
+ mock_hal.ui.screens,
+ vec![
+ Screen::Confirm {
+ title: "Domain (1/1)".into(),
+ body: "name: test".into(),
+ longtouch: false,
+ },
+ Screen::Confirm {
+ title: "Message (1/1)".into(),
+ body: format!("data, line 1/2: {line1}"),
+ longtouch: false,
+ },
+ Screen::Confirm {
+ title: "Message (1/1)".into(),
+ body: format!("data, line 2/2: {line2}"),
+ longtouch: false,
+ },
+ ]
+ );
+ }
+
+ #[test]
+ fn test_multiline_warning_shown_only_for_overlong_line() {
+ let line2 = "b".repeat(MAX_DISPLAY_SIZE);
+ let mock_hal = run_single_string_message(format!("ok\n{line2}"));
+
+ assert_eq!(
+ mock_hal.ui.screens,
+ vec![
+ Screen::Confirm {
+ title: "Domain (1/1)".into(),
+ body: "name: test".into(),
+ longtouch: false,
+ },
+ Screen::Confirm {
+ title: "Message (1/1)".into(),
+ body: "data, line 1/2: ok".into(),
+ longtouch: false,
+ },
+ Screen::Confirm {
+ title: "Warning".into(),
+ body: "The next value is\ntoo large to display\nin full".into(),
+ longtouch: false,
+ },
+ Screen::Confirm {
+ title: "Message (1/1)".into(),
+ body: format!("data, line 2/2: {line2}"),
+ longtouch: false,
+ },
+ ]
+ );
+ }
+
+ #[test]
+ fn test_streaming_bytes_show_display_size_and_truncated_body() {
+ let data: Vec<u8> = (0u8..=255).cycle().take(10_000).collect();
+ let mock_hal = run_single_streaming_bytes_message(data);
+
+ assert_eq!(mock_hal.ui.confirm_display_sizes, vec![0, 0, 10_000]);
+ assert_eq!(
+ mock_hal.ui.screens[1],
+ Screen::Confirm {
+ title: "Warning".into(),
+ body: "The next value is\ntoo large to display\nin full".into(),
+ longtouch: false,
+ }
+ );
+ match &mock_hal.ui.screens[2] {
+ Screen::Confirm { title, body, .. } => {
+ assert_eq!(title, "Message (1/1)");
+ assert!(body.len() > MAX_DISPLAY_SIZE);
+ }
+ _ => panic!("unexpected screen"),
+ }
+ }
+
#[test]
fn test_encode_type() {
assert_eq!(
@@ -954,17 +1275,17 @@ mod tests {
/// Test computation of the domain separator, which is `hashStruct(domain)`.
#[test]
fn test_domain_separator() {
- let typed_msg = alloc::rc::Rc::new(core::cell::RefCell::new(TypedMessage {
- types: make_types(),
- primary_type: "Mail",
- domain: Object::Struct(vec![
+ let typed_msg = alloc::rc::Rc::new(core::cell::RefCell::new(TypedMessage::new(
+ make_types(),
+ "Mail",
+ Object::Struct(vec![
Object::String("Ether Mail"),
Object::String("1"),
Object::BigUint(BigUint::from(1u32)),
Object::String("0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"),
]),
- message: Object::Struct(vec![]),
- }));
+ Object::Struct(vec![]),
+ )));
{
let typed_msg = typed_msg.clone();
*crate::hww::MOCK_NEXT_REQUEST.0.borrow_mut() = Some(Box::new(move |response| {
@@ -1268,8 +1589,8 @@ mod tests {
let bytes32 = b"\xd0\xf0\x29\x88\xfd\x88\x15\x65\xe9\x27\xc7\x47\x3c\x28\x73\x22\xdb\x16\x69\x01\xba\xc0\x3b\xef\x55\xd7\xa5\x2a\x5c\x75\x0a\xb4";
let bigint256_positive = b"\x7f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff";
let bigint256_negative = b"\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
- let typed_msg = alloc::rc::Rc::new(core::cell::RefCell::new(TypedMessage {
- types: vec![
+ let typed_msg = alloc::rc::Rc::new(core::cell::RefCell::new(TypedMessage::new(
+ vec![
StructType {
name: "EIP712Domain".into(),
members: vec![
@@ -1334,8 +1655,8 @@ mod tests {
],
},
],
- primary_type: "AllTypes",
- domain: Object::Struct(vec![
+ "AllTypes",
+ Object::Struct(vec![
// name
Object::String("Ether Mail"),
// version
@@ -1345,7 +1666,7 @@ mod tests {
// verifyingContract
Object::String("0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"),
]),
- message: Object::Struct(vec![
+ Object::Struct(vec![
// str
Object::String("str"),
// emptyArray
@@ -1489,7 +1810,7 @@ mod tests {
]),
]),
]),
- }));
+ )));
{
let typed_msg = typed_msg.clone();
@@ -1555,8 +1876,8 @@ mod tests {
/// ```
#[test]
fn test_no_message() {
- let typed_msg = alloc::rc::Rc::new(core::cell::RefCell::new(TypedMessage {
- types: vec![StructType {
+ let typed_msg = alloc::rc::Rc::new(core::cell::RefCell::new(TypedMessage::new(
+ vec![StructType {
name: "EIP712Domain".into(),
members: vec![
mk_member("name", mk_type(DataType::String)),
@@ -1565,8 +1886,8 @@ mod tests {
mk_member("verifyingContract", mk_type(DataType::Address)),
],
}],
- primary_type: "EIP712Domain",
- domain: Object::Struct(vec![
+ "EIP712Domain",
+ Object::Struct(vec![
// name
Object::String("Ether Mail"),
// version
@@ -1576,8 +1897,8 @@ mod tests {
// verifyingContract
Object::String("0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"),
]),
- message: Object::Struct(vec![]),
- }));
+ Object::Struct(vec![]),
+ )));
{
let typed_msg = typed_msg.clone();
@@ -1598,4 +1919,436 @@ mod tests {
*b"\xaa\x83\xc7\x03\x05\xec\x6c\x13\x1e\x7a\x88\xf2\x58\xc4\x08\x13\x44\x7b\xec\x8b\x9b\xce\xf9\x4e\x54\x79\x60\x3d\x99\x59\xda\x07",
);
}
+
+ // Test vectors generated by testdata/gen_typed_msg_streaming_tests.js using
+ // @metamask/eth-sig-util v4.0.1.
+ #[derive(serde::Deserialize)]
+ struct StreamingTestCase {
+ description: String,
+ types: serde_json::Value,
+ primary_type: String,
+ domain: serde_json::Value,
+ field_type: String,
+ message_data: String,
+ expected_sighash: String,
+ expected_signature: Option<String>,
+ address: Option<String>,
+ expected_screens: Vec<(String, String)>,
+ }
+
+ fn decode_hex(s: &str) -> Vec<u8> {
+ hex::decode(s).unwrap()
+ }
+
+ fn load_streaming_test_cases() -> Vec<StreamingTestCase> {
+ let json_data = include_str!("testdata/typed_msg_streaming_tests.json");
+ serde_json::from_str(json_data).unwrap()
+ }
+
+ fn parse_json_types(json: &serde_json::Value) -> Vec<StructType> {
+ json.as_object()
+ .unwrap()
+ .iter()
+ .map(|(name, members_val)| StructType {
+ name: name.clone(),
+ members: members_val
+ .as_array()
+ .unwrap()
+ .iter()
+ .map(|m| {
+ let type_str = m["type"].as_str().unwrap();
+ let member_type = match type_str {
+ "string" => mk_type(DataType::String),
+ "bytes" => mk_type(DataType::Bytes),
+ _ => panic!("unhandled type in test data: {type_str}"),
+ };
+ mk_member(m["name"].as_str().unwrap(), member_type)
+ })
+ .collect(),
+ })
+ .collect()
+ }
+
+ fn build_domain_object(
+ domain_json: &serde_json::Value,
+ types: &[StructType],
+ ) -> Object<'static> {
+ let domain_type = get_type(types, "EIP712Domain").unwrap();
+ Object::Struct(
+ domain_type
+ .members
+ .iter()
+ .map(|member| {
+ let s: &'static str = Box::leak(
+ domain_json[&member.name]
+ .as_str()
+ .unwrap()
+ .to_string()
+ .into_boxed_str(),
+ );
+ Object::String(s)
+ })
+ .collect(),
+ )
+ }
+
+ /// Verify streaming and inline sighashes match expected values, verify display screens,
+ /// and optionally verify signatures. Test vectors generated by
+ /// testdata/gen_typed_msg_streaming_tests.js using @metamask/eth-sig-util v4.0.1.
+ #[test]
+ fn test_streaming_equivalence() {
+ let tests = load_streaming_test_cases();
+ for tc in &tests {
+ let expected: [u8; 32] = decode_hex(&tc.expected_sighash).try_into().unwrap();
+ let types = parse_json_types(&tc.types);
+ // Leak to get 'static refs needed by the mock closure -- test-only.
+ let primary_type: &'static str = Box::leak(tc.primary_type.clone().into_boxed_str());
+
+ // Inline sighash.
+ {
+ let inline_value = match tc.field_type.as_str() {
+ "bytes" => {
+ let data: Vec<u8> = decode_hex(&tc.message_data);
+ let data_static: &'static [u8] = Box::leak(data.into_boxed_slice());
+ Object::Bytes(data_static)
+ }
+ "string" => {
+ let data_static: &'static str =
+ Box::leak(tc.message_data.clone().into_boxed_str());
+ Object::String(data_static)
+ }
+ _ => panic!("unhandled field_type in test data: {}", tc.field_type),
+ };
+ let typed_msg = alloc::rc::Rc::new(core::cell::RefCell::new(TypedMessage::new(
+ types.clone(),
+ primary_type,
+ build_domain_object(&tc.domain, &types),
+ Object::Struct(vec![inline_value]),
+ )));
+ let typed_msg_clone = typed_msg.clone();
+ *crate::hww::MOCK_NEXT_REQUEST.0.borrow_mut() = Some(Box::new(move |response| {
+ let typed_msg = typed_msg_clone.borrow();
+ Ok(typed_msg.handle_host_response(&response).unwrap())
+ }));
+ let typed_msg = typed_msg.borrow();
+ let inline_sighash = block_on(eip712_sighash(
+ &mut TestingHal::new(),
+ &typed_msg.types,
+ typed_msg.primary_type,
+ ))
+ .unwrap();
+ assert_eq!(
+ inline_sighash, expected,
+ "inline sighash mismatch for: {}",
+ tc.description
+ );
+ }
+
+ // Streaming sighash + display verification (bytes only).
+ if tc.field_type == "bytes" {
+ let data: Vec<u8> = decode_hex(&tc.message_data);
+ let typed_msg = alloc::rc::Rc::new(core::cell::RefCell::new(TypedMessage::new(
+ types.clone(),
+ primary_type,
+ build_domain_object(&tc.domain, &types),
+ Object::Struct(vec![Object::StreamingBytes(data.clone())]),
+ )));
+ let typed_msg_clone = typed_msg.clone();
+ *crate::hww::MOCK_NEXT_REQUEST.0.borrow_mut() = Some(Box::new(move |response| {
+ let typed_msg = typed_msg_clone.borrow();
+ Ok(typed_msg.handle_host_response(&response).unwrap())
+ }));
+ let typed_msg = typed_msg.borrow();
+ let mut mock_hal = TestingHal::new();
+ let streaming_sighash = block_on(eip712_sighash(
+ &mut mock_hal,
+ &typed_msg.types,
+ typed_msg.primary_type,
+ ))
+ .unwrap();
+ assert_eq!(
+ streaming_sighash, expected,
+ "streaming sighash mismatch for: {}",
+ tc.description
+ );
+ let expected_screens: Vec<Screen> = tc
+ .expected_screens
+ .iter()
+ .map(|(title, body)| Screen::Confirm {
+ title: title.clone(),
+ body: body.clone(),
+ longtouch: false,
+ })
+ .collect();
+ assert_eq!(
+ mock_hal.ui.screens, expected_screens,
+ "screens mismatch for: {}",
+ tc.description
+ );
+ } else if tc.field_type == "string" {
+ let data_static: &'static str = Box::leak(tc.message_data.clone().into_boxed_str());
+ let typed_msg = alloc::rc::Rc::new(core::cell::RefCell::new(TypedMessage::new(
+ types.clone(),
+ primary_type,
+ build_domain_object(&tc.domain, &types),
+ Object::Struct(vec![Object::String(data_static)]),
+ )));
+ let typed_msg_clone = typed_msg.clone();
+ *crate::hww::MOCK_NEXT_REQUEST.0.borrow_mut() = Some(Box::new(move |response| {
+ let typed_msg = typed_msg_clone.borrow();
+ Ok(typed_msg.handle_host_response(&response).unwrap())
+ }));
+ let typed_msg = typed_msg.borrow();
+ let mut mock_hal = TestingHal::new();
+ let sighash = block_on(eip712_sighash(
+ &mut mock_hal,
+ &typed_msg.types,
+ typed_msg.primary_type,
+ ))
+ .unwrap();
+ assert_eq!(
+ sighash, expected,
+ "sighash mismatch for: {}",
+ tc.description
+ );
+ let expected_screens: Vec<Screen> = tc
+ .expected_screens
+ .iter()
+ .map(|(title, body)| Screen::Confirm {
+ title: title.clone(),
+ body: body.clone(),
+ longtouch: false,
+ })
+ .collect();
+ assert_eq!(
+ mock_hal.ui.screens, expected_screens,
+ "screens mismatch for: {}",
+ tc.description
+ );
+ }
+
+ // Verify signature if expected.
+ if let (Some(expected_sig_hex), Some(address)) = (&tc.expected_signature, &tc.address) {
+ assert_eq!(tc.field_type, "bytes");
+ mock_unlocked();
+ let data: Vec<u8> = decode_hex(&tc.message_data);
+ let expected_sig = decode_hex(expected_sig_hex);
+ let typed_msg = alloc::rc::Rc::new(core::cell::RefCell::new(TypedMessage::new(
+ types.clone(),
+ primary_type,
+ build_domain_object(&tc.domain, &types),
+ Object::Struct(vec![Object::StreamingBytes(data.clone())]),
+ )));
+ let typed_msg_clone = typed_msg.clone();
+ *crate::hww::MOCK_NEXT_REQUEST.0.borrow_mut() = Some(Box::new(move |response| {
+ let typed_msg = typed_msg_clone.borrow();
+ Ok(typed_msg.handle_host_response(&response).unwrap())
+ }));
+ let mut mock_hal = TestingHal::new();
+ assert_eq!(
+ block_on(process(
+ &mut mock_hal,
+ &pb::EthSignTypedMessageRequest {
+ chain_id: 1,
+ keypath: vec![44 + HARDENED, 60 + HARDENED, 0 + HARDENED, 0, 0],
+ types: types.clone(),
+ primary_type: tc.primary_type.clone(),
+ host_nonce_commitment: None,
+ }
+ )),
+ Ok(Response::Sign(pb::EthSignResponse {
+ signature: expected_sig,
+ })),
+ "signature mismatch for: {}",
+ tc.description
+ );
+ let mut expected_screens = vec![Screen::Confirm {
+ title: "Ethereum".into(),
+ body: address.clone(),
+ longtouch: false,
+ }];
+ expected_screens.extend(tc.expected_screens.iter().map(|(title, body)| {
+ Screen::Confirm {
+ title: title.clone(),
+ body: body.clone(),
+ longtouch: false,
+ }
+ }));
+ expected_screens.push(Screen::Confirm {
+ title: "".into(),
+ body: "Sign data?".into(),
+ longtouch: true,
+ });
+ assert_eq!(
+ mock_hal.ui.screens, expected_screens,
+ "signature flow screens mismatch for: {}",
+ tc.description
+ );
+ }
+ }
+ }
+
+ /// Streaming is rejected for non-streamable types (e.g. uint).
+ #[test]
+ fn test_streaming_rejected_for_uint() {
+ let typed_msg = alloc::rc::Rc::new(core::cell::RefCell::new(TypedMessage::new(
+ vec![
+ StructType {
+ name: "EIP712Domain".into(),
+ members: vec![mk_member("name", mk_type(DataType::String))],
+ },
+ StructType {
+ name: "Msg".into(),
+ members: vec![mk_member("data", mk_sized_type(DataType::Uint, 32))],
+ },
+ ],
+ "Msg",
+ Object::Struct(vec![Object::String("test")]),
+ Object::Struct(vec![Object::StreamingBytes(vec![0x01; 100])]),
+ )));
+ {
+ let typed_msg = typed_msg.clone();
+ *crate::hww::MOCK_NEXT_REQUEST.0.borrow_mut() = Some(Box::new(move |response| {
+ let typed_msg = typed_msg.borrow();
+ Ok(typed_msg.handle_host_response(&response).unwrap())
+ }));
+ }
+ let typed_msg = typed_msg.borrow();
+ let mut mock_hal = TestingHal::new();
+ let result = block_on(eip712_sighash(
+ &mut mock_hal,
+ &typed_msg.types,
+ typed_msg.primary_type,
+ ));
+ assert_eq!(result, Err(Error::InvalidInput));
+ }
+
+ /// Streaming is rejected for fixed-size bytes (e.g. bytes32).
+ #[test]
+ fn test_streaming_rejected_for_fixed_bytes() {
+ let typed_msg = alloc::rc::Rc::new(core::cell::RefCell::new(TypedMessage::new(
+ vec![
+ StructType {
+ name: "EIP712Domain".into(),
+ members: vec![mk_member("name", mk_type(DataType::String))],
+ },
+ StructType {
+ name: "Msg".into(),
+ members: vec![mk_member("data", mk_sized_type(DataType::Bytes, 32))],
+ },
+ ],
+ "Msg",
+ Object::Struct(vec![Object::String("test")]),
+ Object::Struct(vec![Object::StreamingBytes(vec![0x01; 32])]),
+ )));
+ {
+ let typed_msg = typed_msg.clone();
+ *crate::hww::MOCK_NEXT_REQUEST.0.borrow_mut() = Some(Box::new(move |response| {
+ let typed_msg = typed_msg.borrow();
+ Ok(typed_msg.handle_host_response(&response).unwrap())
+ }));
+ }
+ let typed_msg = typed_msg.borrow();
+ let mut mock_hal = TestingHal::new();
+ let result = block_on(eip712_sighash(
+ &mut mock_hal,
+ &typed_msg.types,
+ typed_msg.primary_type,
+ ));
+ assert_eq!(result, Err(Error::InvalidInput));
+ }
+
+ /// data_length exceeding the max is rejected.
+ #[test]
+ fn test_streaming_exceeding_max_rejected() {
+ *crate::hww::MOCK_NEXT_REQUEST.0.borrow_mut() =
+ Some(Box::new(|response| match &response {
+ pb::response::Response::Eth(pb::EthResponse {
+ response:
+ Some(Response::TypedMsgValue(pb::EthTypedMessageValueResponse {
+ root_object,
+ path: _,
+ })),
+ }) => {
+ if *root_object == RootObject::Domain as i32 {
+ return Ok(pb::request::Request::Eth(pb::EthRequest {
+ request: Some(Request::TypedMsgValue(
+ pb::EthTypedMessageValueRequest {
+ value: b"test".to_vec(),
+ data_length: 0,
+ },
+ )),
+ }));
+ }
+ Ok(pb::request::Request::Eth(pb::EthRequest {
+ request: Some(Request::TypedMsgValue(pb::EthTypedMessageValueRequest {
+ value: vec![],
+ data_length: MAX_TYPED_MSG_STREAMING_DATA_LENGTH + 1,
+ })),
+ }))
+ }
+ _ => panic!("unexpected"),
+ }));
+ let types = vec![
+ StructType {
+ name: "EIP712Domain".into(),
+ members: vec![mk_member("name", mk_type(DataType::String))],
+ },
+ StructType {
+ name: "Msg".into(),
+ members: vec![mk_member("data", mk_type(DataType::Bytes))],
+ },
+ ];
+ let mut mock_hal = TestingHal::new();
+ let result = block_on(eip712_sighash(&mut mock_hal, &types, "Msg"));
+ assert_eq!(result, Err(Error::InvalidInput));
+ }
+
+ /// Both value and data_length non-empty is rejected.
+ #[test]
+ fn test_streaming_value_and_data_length_both_set_rejected() {
+ *crate::hww::MOCK_NEXT_REQUEST.0.borrow_mut() = Some(Box::new(|response| {
+ match &response {
+ pb::response::Response::Eth(pb::EthResponse {
+ response:
+ Some(Response::TypedMsgValue(pb::EthTypedMessageValueResponse {
+ root_object,
+ ..
+ })),
+ }) => {
+ if *root_object == RootObject::Domain as i32 {
+ return Ok(pb::request::Request::Eth(pb::EthRequest {
+ request: Some(Request::TypedMsgValue(
+ pb::EthTypedMessageValueRequest {
+ value: b"test".to_vec(),
+ data_length: 0,
+ },
+ )),
+ }));
+ }
+ // message.data: both value and data_length set
+ Ok(pb::request::Request::Eth(pb::EthRequest {
+ request: Some(Request::TypedMsgValue(pb::EthTypedMessageValueRequest {
+ value: vec![1, 2, 3],
+ data_length: 100,
+ })),
+ }))
+ }
+ _ => panic!("unexpected"),
+ }
+ }));
+ let types = vec![
+ StructType {
+ name: "EIP712Domain".into(),
+ members: vec![mk_member("name", mk_type(DataType::String))],
+ },
+ StructType {
+ name: "Msg".into(),
+ members: vec![mk_member("data", mk_type(DataType::Bytes))],
+ },
+ ];
+ let mut mock_hal = TestingHal::new();
+ let result = block_on(eip712_sighash(&mut mock_hal, &types, "Msg"));
+ assert_eq!(result, Err(Error::InvalidInput));
+ }
}
diff --git a/src/rust/bitbox02-rust/src/hww/api/ethereum/testdata/gen_typed_msg_streaming_tests.js b/src/rust/bitbox02-rust/src/hww/api/ethereum/testdata/gen_typed_msg_streaming_tests.js
new file mode 100644
index 0000000..6bd550e
--- /dev/null
+++ b/src/rust/bitbox02-rust/src/hww/api/ethereum/testdata/gen_typed_msg_streaming_tests.js
@@ -0,0 +1,169 @@
+// Generates typed_msg_streaming_tests.json using @metamask/eth-sig-util v4.0.1 and ethers v5.
+//
+// Usage:
+// npm install @metamask/eth-sig-util@v4.0.1 ethers@5
+// node gen_typed_msg_streaming_tests.js > typed_msg_streaming_tests.json
+
+const util = require("@metamask/eth-sig-util");
+const { ethers } = require("ethers");
+
+const mnemonic =
+ "purity concert above invest pigeon category peace tuition hazard vivid latin since legal speak nation session onion library travel spell region blast estate stay";
+const wallet = ethers.Wallet.fromMnemonic(mnemonic, "m/44'/60'/0'/0/0");
+const privateKey = Buffer.from(wallet.privateKey.slice(2), "hex");
+
+// Mirrors firmware's address formatting: "0x" prefix separated, rest in groups of 4.
+function formatEthAddress(address) {
+ const rest = address.slice(2);
+ return "0x " + rest.match(/.{1,4}/g).join(" ");
+}
+
+function generateBytes(length, pattern) {
+ const buf = Buffer.alloc(length);
+ for (let i = 0; i < length; i++) {
+ buf[i] = pattern(i);
+ }
+ return "0x" + buf.toString("hex");
+}
+
+function formatDisplayLinePrefix(displayPath, lineNum, numLines) {
+ if (numLines > 1) {
+ return `${displayPath}, line ${lineNum + 1}/${numLines}`;
+ }
+ return displayPath;
+}
+
+function formatDisplayLineBody(displayPath, lineNum, numLines, line) {
+ return `${formatDisplayLinePrefix(displayPath, lineNum, numLines)}: ${line}`;
+}
+
+function streamingDisplayByteCap(displayPath, dataLength) {
+ const MAX_DISPLAY_SIZE = 640;
+ const bodyLenWithoutHex =
+ formatDisplayLinePrefix(displayPath, 0, 1).length + ": ".length + "0x".length;
+ const visibleBytes = Math.floor(
+ Math.max(0, MAX_DISPLAY_SIZE - bodyLenWithoutHex) / 2
+ );
+ return Math.min(dataLength, visibleBytes + Number(dataLength > visibleBytes));
+}
+
+const testCases = [
+ {
+ description: "large dynamic bytes",
+ types: {
+ EIP712Domain: [{ name: "name", type: "string" }],
+ Msg: [{ name: "data", type: "bytes" }],
+ },
+ primaryType: "Msg",
+ domain: { name: "test" },
+ message: {
+ data: generateBytes(10000, (i) => i % 256),
+ },
+ sign: true,
+ },
+ {
+ description: "multiline string with long second line",
+ types: {
+ EIP712Domain: [{ name: "name", type: "string" }],
+ Msg: [{ name: "data", type: "string" }],
+ },
+ primaryType: "Msg",
+ domain: { name: "test" },
+ message: {
+ data: `ok\n${"b".repeat(640)}`,
+ },
+ sign: false,
+ },
+];
+
+const output = testCases.map((tc) => {
+ const msgParams = {
+ types: tc.types,
+ primaryType: tc.primaryType,
+ domain: tc.domain,
+ message: tc.message,
+ };
+ const sighash = util.TypedDataUtils.eip712Hash(msgParams, "V4");
+
+ const fieldType = tc.types[tc.primaryType].find(
+ (m) => m.name === "data"
+ ).type;
+ const messageValue =
+ fieldType === "bytes" ? tc.message.data.slice(2) : tc.message.data;
+
+ // Compute expected screens shown by the firmware.
+ const expectedScreens = [];
+ const domainType = tc.types.EIP712Domain;
+ for (let i = 0; i < domainType.length; i++) {
+ const field = domainType[i];
+ expectedScreens.push([
+ `Domain (${i + 1}/${domainType.length})`,
+ `${field.name}: ${tc.domain[field.name]}`,
+ ]);
+ }
+ const MAX_DISPLAY_SIZE = 640;
+ const msgType = tc.types[tc.primaryType];
+ for (let i = 0; i < msgType.length; i++) {
+ const field = msgType[i];
+ const title = `Message (${i + 1}/${msgType.length})`;
+ let valueFormatted;
+ if (field.type === "bytes") {
+ const rawBytes = Buffer.from(tc.message[field.name].slice(2), "hex");
+ const displayCap = streamingDisplayByteCap(field.name, rawBytes.length);
+ const truncated = rawBytes.slice(0, displayCap);
+ valueFormatted = "0x" + truncated.toString("hex");
+ } else if (field.type === "string") {
+ valueFormatted = tc.message[field.name];
+ } else {
+ throw new Error(`Unhandled field type in test case: ${field.type}`);
+ }
+ const lines = valueFormatted.split("\n");
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
+ const body = formatDisplayLineBody(
+ field.name,
+ lineIdx,
+ lines.length,
+ lines[lineIdx]
+ );
+ if (body.length > MAX_DISPLAY_SIZE) {
+ expectedScreens.push([
+ "Warning",
+ "The next value is\ntoo large to display\nin full",
+ ]);
+ }
+ expectedScreens.push([title, body]);
+ }
+ }
+
+ const result = {
+ description: tc.description,
+ types: tc.types,
+ primary_type: tc.primaryType,
+ domain: tc.domain,
+ field_type: fieldType,
+ message_data: messageValue,
+ expected_sighash: sighash.toString("hex"),
+ expected_screens: expectedScreens,
+ };
+
+ if (tc.sign) {
+ const sig = util.signTypedData({
+ privateKey,
+ data: msgParams,
+ version: "V4",
+ });
+ // sig is hex string "0x" + r (32 bytes) + s (32 bytes) + v (1 byte), where v is 27 or 28.
+ const sigBytes = Buffer.from(sig.slice(2), "hex");
+ const r = sigBytes.slice(0, 32);
+ const s = sigBytes.slice(32, 64);
+ const v = sigBytes[64];
+ // Firmware format: r + s + recid, where recid = v - 27.
+ const firmwareSig = Buffer.concat([r, s, Buffer.from([v - 27])]);
+ result.expected_signature = firmwareSig.toString("hex");
+ result.address = formatEthAddress(wallet.address);
+ }
+
+ return result;
+});
+
+console.log(JSON.stringify(output, null, 2));
diff --git a/src/rust/bitbox02-rust/src/hww/api/ethereum/testdata/typed_msg_streaming_tests.json b/src/rust/bitbox02-rust/src/hww/api/ethereum/testdata/typed_msg_streaming_tests.json
new file mode 100644
index 0000000..cbc181f
--- /dev/null
+++ b/src/rust/bitbox02-rust/src/hww/api/ethereum/testdata/typed_msg_streaming_tests.json
@@ -0,0 +1,84 @@
+[
+ {
+ "description": "large dynamic bytes",
+ "types": {
+ "EIP712Domain": [
+ {
+ "name": "name",
+ "type": "string"
+ }
+ ],
+ "Msg": [
+ {
+ "name": "data",
+ "type": "bytes"
+ }
+ ]
+ },
+ "primary_type": "Msg",
+ "domain": {
+ "name": "test"
+ },
+ "field_type": "bytes",
+ "message_data": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f",
+ "expected_sighash": "8b791f4fc1a7fe8c2adc89877cee75b14aa23c896943c414b394a9e0cc3d0696",
+ "expected_screens": [
+ [
+ "Domain (1/1)",
+ "name: test"
+ ],
+ [
+ "Warning",
+ "The next value is\ntoo large to display\nin full"
+ ],
+ [
+ "Message (1/1)",
+ "data: 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c"
+ ]
+ ],
+ "expected_signature": "14afbad99bb3f39f3d2f5fdd66da325fdcde1a5109d6b2f04b1f9e2e6cf6752d418f45ba375d8b49727d6418ab3a1999273cb34f84e74c37a7cbe480f7bfc7f400",
+ "address": "0x 773A 77b9 D325 89be 03f9 132A F759 e294 f785 1be9"
+ },
+ {
+ "description": "multiline string with long second line",
+ "types": {
+ "EIP712Domain": [
+ {
+ "name": "name",
+ "type": "string"
+ }
+ ],
+ "Msg": [
+ {
+ "name": "data",
+ "type": "string"
+ }
+ ]
+ },
+ "primary_type": "Msg",
+ "domain": {
+ "name": "test"
+ },
+ "field_type": "string",
+ "message_data": "ok\nbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "expected_sighash": "7599e2ee5cceee1de599875d5d71af1c09db0b03552a9673b205bf2e49b02066",
+ "expected_screens": [
+ [
+ "Domain (1/1)",
+ "name: test"
+ ],
+ [
+ "Message (1/1)",
+ "data, line 1/2: ok"
+ ],
+ [
+ "Warning",
+ "The next value is\ntoo large to display\nin full"
+ ],
+ [
+ "Message (1/1)",
+ "data, line 2/2: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+ ]
+ ]
+ }
+]
diff --git a/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs b/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs
index e91f92d..be8a9f1 100644
--- a/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs
+++ b/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs
@@ -1787,6 +1787,10 @@ pub mod eth_typed_message_value_response {
pub struct EthTypedMessageValueRequest {
#[prost(bytes = "vec", tag = "1")]
pub value: ::prost::alloc::vec::Vec<u8>,
+ /// If non-zero, value should be empty and data will be streamed via
+ /// DataRequestChunk/DataResponseChunk.
+ #[prost(uint32, tag = "2")]
+ pub data_length: u32,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
diff --git a/src/ui/components/label.h b/src/ui/components/label.h
index 45629c4..12a3143 100644
--- a/src/ui/components/label.h
+++ b/src/ui/components/label.h
@@ -9,6 +9,7 @@
// Max size of text shown (excl. null terminator). The current size of 640 is chosen to be able to
// show up to 320 bytes of Ethereum tx data in hex format.
+// If changed, keep in sync with MAX_DISPLAY_SIZE.
#define MAX_LABEL_SIZE 640
/**
Why this scored 37/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.