From 2b5a0f1d537cc846b527e967c42ef2c827a87699 Mon Sep 17 00:00:00 2001 From: grdddj Date: Thu, 21 Oct 2021 10:38:05 +0200 Subject: [PATCH] changing file structure - layouts, helpers, sign_typed_data --- core/src/apps/ethereum/address.py | 54 -- core/src/apps/ethereum/get_address.py | 2 +- core/src/apps/ethereum/helpers.py | 125 ++++ core/src/apps/ethereum/layout.py | 132 +++- core/src/apps/ethereum/sign_message.py | 4 +- core/src/apps/ethereum/sign_tx.py | 7 +- core/src/apps/ethereum/sign_tx_eip1559.py | 8 +- core/src/apps/ethereum/sign_typed_data.py | 453 +++++++++++++- core/src/apps/ethereum/typed_data.py | 637 -------------------- core/src/apps/ethereum/verify_message.py | 2 +- core/tests/test_apps.ethereum.typed_data.py | 6 +- 11 files changed, 715 insertions(+), 715 deletions(-) delete mode 100644 core/src/apps/ethereum/address.py create mode 100644 core/src/apps/ethereum/helpers.py delete mode 100644 core/src/apps/ethereum/typed_data.py diff --git a/core/src/apps/ethereum/address.py b/core/src/apps/ethereum/address.py deleted file mode 100644 index d4fa1cde092..00000000000 --- a/core/src/apps/ethereum/address.py +++ /dev/null @@ -1,54 +0,0 @@ -from ubinascii import hexlify, unhexlify - -from trezor import wire - -if False: - from .networks import NetworkInfo - - -def address_from_bytes(address_bytes: bytes, network: NetworkInfo | None = None) -> str: - """ - Converts address in bytes to a checksummed string as defined - in https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md - """ - from trezor.crypto.hashlib import sha3_256 - - if network is not None and network.rskip60: - prefix = str(network.chain_id) + "0x" - else: - prefix = "" - - address_hex = hexlify(address_bytes).decode() - digest = sha3_256((prefix + address_hex).encode(), keccak=True).digest() - - def maybe_upper(i: int) -> str: - """Uppercase i-th letter only if the corresponding nibble has high bit set.""" - digest_byte = digest[i // 2] - hex_letter = address_hex[i] - if i % 2 == 0: - # even letter -> high nibble - bit = 0x80 - else: - # odd letter -> low nibble - bit = 0x08 - if digest_byte & bit: - return hex_letter.upper() - else: - return hex_letter - - return "0x" + "".join(maybe_upper(i) for i in range(len(address_hex))) - - -def bytes_from_address(address: str) -> bytes: - if len(address) == 40: - return unhexlify(address) - - elif len(address) == 42: - if address[0:2] not in ("0x", "0X"): - raise wire.ProcessError("Ethereum: invalid beginning of an address") - return unhexlify(address[2:]) - - elif len(address) == 0: - return bytes() - - raise wire.ProcessError("Ethereum: Invalid address length") diff --git a/core/src/apps/ethereum/get_address.py b/core/src/apps/ethereum/get_address.py index b12f60bd167..6275af7ea4b 100644 --- a/core/src/apps/ethereum/get_address.py +++ b/core/src/apps/ethereum/get_address.py @@ -6,7 +6,7 @@ from apps.common import paths from . import networks -from .address import address_from_bytes +from .helpers import address_from_bytes from .keychain import PATTERNS_ADDRESS, with_keychain_from_path if False: diff --git a/core/src/apps/ethereum/helpers.py b/core/src/apps/ethereum/helpers.py new file mode 100644 index 00000000000..61750a0bfdb --- /dev/null +++ b/core/src/apps/ethereum/helpers.py @@ -0,0 +1,125 @@ +from ubinascii import hexlify, unhexlify + +from trezor import wire +from trezor.enums import EthereumDataType +from trezor.messages import EthereumFieldType + +if False: + from .networks import NetworkInfo + + +def address_from_bytes(address_bytes: bytes, network: NetworkInfo | None = None) -> str: + """ + Converts address in bytes to a checksummed string as defined + in https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md + """ + from trezor.crypto.hashlib import sha3_256 + + if network is not None and network.rskip60: + prefix = str(network.chain_id) + "0x" + else: + prefix = "" + + address_hex = hexlify(address_bytes).decode() + digest = sha3_256((prefix + address_hex).encode(), keccak=True).digest() + + def maybe_upper(i: int) -> str: + """Uppercase i-th letter only if the corresponding nibble has high bit set.""" + digest_byte = digest[i // 2] + hex_letter = address_hex[i] + if i % 2 == 0: + # even letter -> high nibble + bit = 0x80 + else: + # odd letter -> low nibble + bit = 0x08 + if digest_byte & bit: + return hex_letter.upper() + else: + return hex_letter + + return "0x" + "".join(maybe_upper(i) for i in range(len(address_hex))) + + +def bytes_from_address(address: str) -> bytes: + if len(address) == 40: + return unhexlify(address) + + elif len(address) == 42: + if address[0:2] not in ("0x", "0X"): + raise wire.ProcessError("Ethereum: invalid beginning of an address") + return unhexlify(address[2:]) + + elif len(address) == 0: + return bytes() + + raise wire.ProcessError("Ethereum: Invalid address length") + + +def get_type_name(field: EthereumFieldType) -> str: + """Create a string from type definition (like uint256 or bytes16).""" + data_type = field.data_type + size = field.size + + TYPE_TRANSLATION_DICT = { + EthereumDataType.UINT: "uint", + EthereumDataType.INT: "int", + EthereumDataType.BYTES: "bytes", + EthereumDataType.STRING: "string", + EthereumDataType.BOOL: "bool", + EthereumDataType.ADDRESS: "address", + } + + if data_type == EthereumDataType.STRUCT: + assert field.struct_name is not None # validate_field_type + return field.struct_name + elif data_type == EthereumDataType.ARRAY: + assert field.entry_type is not None # validate_field_type + entry_type = field.entry_type + type_name = get_type_name(entry_type) + if size is None: + return f"{type_name}[]" + else: + return f"{type_name}[{size}]" + elif data_type in (EthereumDataType.UINT, EthereumDataType.INT): + assert size is not None # validate_field_type + return TYPE_TRANSLATION_DICT[data_type] + str(size * 8) + elif data_type == EthereumDataType.BYTES: + if size: + return TYPE_TRANSLATION_DICT[data_type] + str(size) + else: + return TYPE_TRANSLATION_DICT[data_type] + else: + # all remaining types can use the name directly + # if the data_type is left out, this will raise KeyError + return TYPE_TRANSLATION_DICT[data_type] + + +def decode_data(data: bytes, type_name: str) -> str: + if type_name.startswith("bytes"): + return hexlify(data).decode() + elif type_name == "string": + return data.decode() + elif type_name == "address": + return address_from_bytes(data) + elif type_name == "bool": + return "true" if data == b"\x01" else "false" + elif type_name.startswith("uint"): + return str(int.from_bytes(data, "big")) + elif type_name.startswith("int"): + # Micropython does not implement "signed" arg in int.from_bytes() + return str(from_bytes_bigendian_signed(data)) + + raise ValueError # Unsupported data type for direct field decoding + + +def from_bytes_bigendian_signed(b: bytes) -> int: + negative = b[0] & 0x80 + if negative: + neg_b = bytearray(b) + for i in range(len(neg_b)): + neg_b[i] = ~neg_b[i] & 0xFF + result = int.from_bytes(neg_b, "big") + return -result - 1 + else: + return int.from_bytes(b, "big") diff --git a/core/src/apps/ethereum/layout.py b/core/src/apps/ethereum/layout.py index 354bf1ac71f..65c88b4e73a 100644 --- a/core/src/apps/ethereum/layout.py +++ b/core/src/apps/ethereum/layout.py @@ -1,21 +1,26 @@ from ubinascii import hexlify from trezor import ui -from trezor.enums import ButtonRequestType +from trezor.enums import ButtonRequestType, EthereumDataType +from trezor.messages import EthereumFieldType, EthereumStructMember from trezor.strings import format_amount +from trezor.ui.components.tt.text import Text from trezor.ui.layouts import ( confirm_address, confirm_amount, confirm_blob, confirm_output, + confirm_text, ) from trezor.ui.layouts.tt.altcoin import confirm_total_ethereum +from apps.common.confirm import confirm + from . import networks, tokens -from .address import address_from_bytes +from .helpers import address_from_bytes, decode_data, get_type_name if False: - from typing import Awaitable + from typing import Awaitable, Iterable, Optional from trezor.wire import Context @@ -105,6 +110,120 @@ def require_confirm_data(ctx: Context, data: bytes, data_total: int) -> Awaitabl ) +async def confirm_hash(ctx: Context, primary_type: str, typed_data_hash: bytes) -> None: + data = "0x" + hexlify(typed_data_hash).decode() + await confirm_blob( + ctx, + "confirm_resulting_hash", + title="Sign typed data?", + description=f"Hashed {primary_type}:", + data=data, + hold=True, + icon=ui.ICON_CONFIG, + icon_color=ui.GREEN, + ) + + +async def should_show_domain(ctx: Context, name: bytes, version: bytes) -> bool: + page = Text("Typed Data", ui.ICON_SEND, icon_color=ui.GREEN) + + domain_name = decode_data(name, "string") + domain_version = decode_data(version, "string") + + page.bold(f"Name: {domain_name}") + page.normal(f"Version: {domain_version}") + page.br() + page.mono("View EIP712Domain?") + + return await confirm(ctx, page, ButtonRequestType.Other) + + +async def should_show_struct( + ctx: Context, + primary_type: str, + parent_objects: Iterable[str], + data_members: list[EthereumStructMember], +) -> bool: + title = f"{'.'.join(parent_objects)} - {primary_type}" + page = Text(title, ui.ICON_SEND, icon_color=ui.GREEN) + + # We have limited screen space, so showing only a preview when having lot of fields + MAX_FIELDS_TO_SHOW = 3 + fields_amount = len(data_members) + if fields_amount > MAX_FIELDS_TO_SHOW: + for field in data_members[:MAX_FIELDS_TO_SHOW]: + page.bold(limit_str(field.name)) + page.mono(f"...and {fields_amount - MAX_FIELDS_TO_SHOW} more.") + else: + for field in data_members: + page.bold(limit_str(field.name)) + + page.mono("View full struct?") + + return await confirm(ctx, page, ButtonRequestType.Other) + + +async def should_show_array( + ctx: Context, + name: str, + parent_objects: Iterable[str], + data_type: str, + size: int, +) -> bool: + title = f"{'.'.join(parent_objects)} - {name}" + page = Text(title, ui.ICON_SEND, icon_color=ui.GREEN) + + page.bold(limit_str(f"Type: {data_type}")) + page.bold(limit_str(f"Size: {size}")) + page.br() + page.mono("View full array?") + + return await confirm(ctx, page, ButtonRequestType.Other) + + +async def confirm_typed_value( + ctx: Context, + name: str, + value: bytes, + parent_objects: Iterable[str], + primary_type: str, + field: EthereumFieldType, + array_index: Optional[int] = None, +) -> None: + type_name = get_type_name(field) + if parent_objects: + title = f"{'.'.join(parent_objects)} - {primary_type}" + else: + title = primary_type + + if array_index is not None: + array_str = f"[{array_index}]" + else: + array_str = "" + + description = f"{name}{array_str} ({type_name})" + data = decode_data(value, type_name) + + if field.data_type in (EthereumDataType.ADDRESS, EthereumDataType.BYTES): + await confirm_blob( + ctx, + "show_data", + title=title, + data=data, + description=description, + br_code=ButtonRequestType.Other, + ) + else: + await confirm_text( + ctx, + "show_data", + title=title, + data=data, + description=description, + br_code=ButtonRequestType.Other, + ) + + def format_ethereum_amount( value: int, token: tokens.TokenInfo | None, chain_id: int ) -> str: @@ -121,3 +240,10 @@ def format_ethereum_amount( decimals = 0 return f"{format_amount(value, decimals)} {suffix}" + + +def limit_str(s: str, limit: int = 16) -> str: + if len(s) <= limit + 2: + return s + + return s[:limit] + ".." diff --git a/core/src/apps/ethereum/sign_message.py b/core/src/apps/ethereum/sign_message.py index a7afcbd65d6..921e7af6495 100644 --- a/core/src/apps/ethereum/sign_message.py +++ b/core/src/apps/ethereum/sign_message.py @@ -7,7 +7,7 @@ from apps.common import paths from apps.common.signverify import decode_message -from . import address +from .helpers import address_from_bytes from .keychain import PATTERNS_ADDRESS, with_keychain_from_path if False: @@ -42,6 +42,6 @@ async def sign_message( ) return EthereumMessageSignature( - address=address.address_from_bytes(node.ethereum_pubkeyhash()), + address=address_from_bytes(node.ethereum_pubkeyhash()), signature=signature[1:] + bytearray([signature[0]]), ) diff --git a/core/src/apps/ethereum/sign_tx.py b/core/src/apps/ethereum/sign_tx.py index 50f108d6877..45ab1e914d0 100644 --- a/core/src/apps/ethereum/sign_tx.py +++ b/core/src/apps/ethereum/sign_tx.py @@ -7,7 +7,8 @@ from apps.common import paths -from . import address, tokens +from . import tokens +from .helpers import bytes_from_address from .keychain import with_keychain_from_chain_id from .layout import ( require_confirm_data, @@ -96,7 +97,7 @@ async def handle_erc20( ctx: wire.Context, msg: EthereumSignTxAny ) -> Tuple[tokens.TokenInfo | None, bytes, bytes, int]: token = None - address_bytes = recipient = address.bytes_from_address(msg.to) + address_bytes = recipient = bytes_from_address(msg.to) value = int.from_bytes(msg.value, "big") if ( len(msg.to) in (40, 42) @@ -125,7 +126,7 @@ def get_total_length(msg: EthereumSignTx, data_total: int) -> int: msg.nonce, msg.gas_price, msg.gas_limit, - address.bytes_from_address(msg.to), + bytes_from_address(msg.to), msg.value, msg.chain_id, 0, diff --git a/core/src/apps/ethereum/sign_tx_eip1559.py b/core/src/apps/ethereum/sign_tx_eip1559.py index 9d06f1ee708..489b12cc3f3 100644 --- a/core/src/apps/ethereum/sign_tx_eip1559.py +++ b/core/src/apps/ethereum/sign_tx_eip1559.py @@ -7,7 +7,7 @@ from apps.common import paths -from . import address +from .helpers import bytes_from_address from .keychain import with_keychain_from_chain_id from .layout import ( require_confirm_data, @@ -27,7 +27,7 @@ def access_list_item_length(item: EthereumAccessList) -> int: - address_length = rlp.length(address.bytes_from_address(item.address)) + address_length = rlp.length(bytes_from_address(item.address)) keys_length = rlp.length(item.storage_keys) return ( rlp.header_length(address_length + keys_length) + address_length + keys_length @@ -43,7 +43,7 @@ def write_access_list(w: HashWriter, access_list: list[EthereumAccessList]) -> N payload_length = sum(access_list_item_length(i) for i in access_list) rlp.write_header(w, payload_length, rlp.LIST_HEADER_BYTE) for item in access_list: - address_bytes = address.bytes_from_address(item.address) + address_bytes = bytes_from_address(item.address) address_length = rlp.length(address_bytes) keys_length = rlp.length(item.storage_keys) rlp.write_header(w, address_length + keys_length, rlp.LIST_HEADER_BYTE) @@ -125,7 +125,7 @@ def get_total_length(msg: EthereumSignTxEIP1559, data_total: int) -> int: fields: Tuple[rlp.RLPItem, ...] = ( msg.nonce, msg.gas_limit, - address.bytes_from_address(msg.to), + bytes_from_address(msg.to), msg.value, msg.chain_id, msg.max_gas_fee, diff --git a/core/src/apps/ethereum/sign_typed_data.py b/core/src/apps/ethereum/sign_typed_data.py index b81cb63772d..f4d7365a030 100644 --- a/core/src/apps/ethereum/sign_typed_data.py +++ b/core/src/apps/ethereum/sign_typed_data.py @@ -1,14 +1,26 @@ +from trezor import wire from trezor.crypto.curve import secp256k1 -from trezor.messages import EthereumSignTypedData, EthereumTypedDataSignature +from trezor.crypto.hashlib import sha3_256 +from trezor.enums import EthereumDataType +from trezor.messages import ( + EthereumFieldType, + EthereumSignTypedData, + EthereumTypedDataSignature, + EthereumTypedDataStructAck, + EthereumTypedDataStructRequest, + EthereumTypedDataValueAck, + EthereumTypedDataValueRequest, +) +from trezor.utils import HashWriter from apps.common import paths -from . import address +from .helpers import address_from_bytes, get_type_name from .keychain import PATTERNS_ADDRESS, with_keychain_from_path -from .typed_data import ( - TypedDataEnvelope, +from .layout import ( confirm_hash, - keccak256, + confirm_typed_value, + should_show_array, should_show_domain, should_show_struct, ) @@ -18,6 +30,12 @@ from trezor.wire import Context +# Maximum data size we support +MAX_VALUE_BYTE_SIZE = 1024 + +# TODO: get better layouts + + @with_keychain_from_path(*PATTERNS_ADDRESS) async def sign_typed_data( ctx: Context, msg: EthereumSignTypedData, keychain: Keychain @@ -34,7 +52,7 @@ async def sign_typed_data( ) return EthereumTypedDataSignature( - address=address.address_from_bytes(node.ethereum_pubkeyhash()), + address=address_from_bytes(node.ethereum_pubkeyhash()), signature=signature[1:] + signature[0:1], ) @@ -55,7 +73,8 @@ async def generate_typed_data_hash( ) await typed_data_envelope.collect_types() - show_domain = await should_show_domain(ctx, typed_data_envelope) + name, version = await get_name_and_version_for_domain(ctx, typed_data_envelope) + show_domain = await should_show_domain(ctx, name, version) domain_separator = await typed_data_envelope.hash_struct( primary_type="EIP712Domain", member_path=[0], @@ -64,7 +83,7 @@ async def generate_typed_data_hash( ) show_message = await should_show_struct( - ctx, primary_type, ["data"], typed_data_envelope + ctx, primary_type, ["data"], typed_data_envelope.types[primary_type].members ) message_hash = await typed_data_envelope.hash_struct( primary_type=primary_type, @@ -76,3 +95,421 @@ async def generate_typed_data_hash( await confirm_hash(ctx, primary_type, message_hash) return keccak256(b"\x19" + b"\x01" + domain_separator + message_hash) + + +def get_hash_writer() -> HashWriter: + return HashWriter(sha3_256(keccak=True)) + + +def keccak256(message: bytes) -> bytes: + h = get_hash_writer() + h.extend(message) + return h.get_digest() + + +class TypedDataEnvelope: + """Encapsulates the type information for the message being hashed and signed.""" + + def __init__( + self, + ctx: Context, + primary_type: str, + metamask_v4_compat: bool, + ) -> None: + self.ctx = ctx + self.primary_type = primary_type + self.metamask_v4_compat = metamask_v4_compat + self.types: dict[str, EthereumTypedDataStructAck] = {} + + async def collect_types(self) -> None: + """Aggregate type collection process for both domain and message data.""" + await self._collect_types("EIP712Domain") + await self._collect_types(self.primary_type) + + async def _collect_types(self, type_name: str) -> None: + """Recursively collect types from the client.""" + req = EthereumTypedDataStructRequest(name=type_name) + current_type = await self.ctx.call(req, EthereumTypedDataStructAck) + self.types[type_name] = current_type + for member in current_type.members: + validate_field_type(member.type) + if ( + member.type.data_type == EthereumDataType.STRUCT + and member.type.struct_name not in self.types + ): + assert member.type.struct_name is not None # validate_field_type + await self._collect_types(member.type.struct_name) + + async def hash_struct( + self, + primary_type: str, + member_path: list, + show_data: bool, + parent_objects: list, + ) -> bytes: + """Generate a hash representation of the whole struct.""" + w = get_hash_writer() + self.hash_type(w, primary_type) + await self.get_and_encode_data( + w=w, + primary_type=primary_type, + member_path=member_path, + show_data=show_data, + parent_objects=parent_objects, + ) + return w.get_digest() + + def hash_type(self, w: HashWriter, primary_type: str) -> None: + """Create a representation of a type.""" + result = keccak256(self.encode_type(primary_type)) + w.extend(result) + + def encode_type(self, primary_type: str) -> bytes: + """ + SPEC: + The type of a struct is encoded as name ‖ "(" ‖ member₁ ‖ "," ‖ member₂ ‖ "," ‖ … ‖ memberₙ ")" + where each member is written as type ‖ " " ‖ name + If the struct type references other struct types (and these in turn reference even more struct types), + then the set of referenced struct types is collected, sorted by name and appended to the encoding. + """ + result: list[str] = [] + + deps: set[str] = set() + self.find_typed_dependencies(primary_type, deps) + deps.remove(primary_type) + primary_first_sorted_deps = [primary_type] + sorted(deps) + + for type_name in primary_first_sorted_deps: + members = self.types[type_name].members + fields = ",".join(f"{get_type_name(m.type)} {m.name}" for m in members) + result.append(f"{type_name}({fields})") + + return "".join(result).encode() + + def find_typed_dependencies( + self, + primary_type: str, + results: set, + ) -> None: + """Find all types within a type definition object.""" + # We already have this type or it is not even a defined type + if (primary_type in results) or (primary_type not in self.types): + return + + results.add(primary_type) + + # Recursively adding all the children struct types, + # also looking into (even nested) arrays for them + for member in self.types[primary_type].members: + member_type = member.type + while member_type.data_type == EthereumDataType.ARRAY: + assert member_type.entry_type is not None # validate_field_type + member_type = member_type.entry_type + if member_type.data_type == EthereumDataType.STRUCT: + assert member_type.struct_name is not None # validate_field_type + self.find_typed_dependencies(member_type.struct_name, results) + + async def get_and_encode_data( + self, + w: HashWriter, + primary_type: str, + member_path: list, + show_data: bool, + parent_objects: list, + ) -> None: + """ + Gradually fetch data from client and encode the whole struct. + + SPEC: + The encoding of a struct instance is enc(value₁) ‖ enc(value₂) ‖ … ‖ enc(valueₙ), + i.e. the concatenation of the encoded member values in the order that they appear in the type. + Each encoded member value is exactly 32-byte long. + """ + type_members = self.types[primary_type].members + for member_index, member in enumerate(type_members): + member_value_path = member_path + [member_index] + data_type = member.type.data_type + field_name = member.name + + # Arrays and structs need special recursive handling + if data_type == EthereumDataType.STRUCT: + assert member.type.struct_name is not None # validate_field_type + struct_name = member.type.struct_name + current_parent_objects = parent_objects + [field_name] + + if show_data: + show_struct = await should_show_struct( + ctx=self.ctx, + primary_type=struct_name, + parent_objects=current_parent_objects, + data_members=self.types[struct_name].members, + ) + else: + show_struct = False + + res = await self.hash_struct( + primary_type=struct_name, + member_path=member_value_path, + show_data=show_struct, + parent_objects=current_parent_objects, + ) + w.extend(res) + elif data_type == EthereumDataType.ARRAY: + # Getting the length of the array first, if not fixed + if member.type.size is None: + array_size = await get_array_size(self.ctx, member_value_path) + else: + array_size = member.type.size + + assert member.type.entry_type is not None # validate_field_type + entry_type = member.type.entry_type + current_parent_objects = parent_objects + [field_name] + + if show_data: + show_array = await should_show_array( + ctx=self.ctx, + name=member.name, + parent_objects=current_parent_objects, + data_type=get_type_name(entry_type), + size=array_size, + ) + else: + show_array = False + + arr_w = get_hash_writer() + for i in range(array_size): + el_member_path = member_value_path + [i] + # TODO: we do not support arrays of arrays, check if we should + if entry_type.data_type == EthereumDataType.STRUCT: + assert entry_type.struct_name is not None # validate_field_type + struct_name = entry_type.struct_name + # Metamask V4 implementation has a bug, that causes the + # behavior of structs in array be different from SPEC + # Explanation at https://github.com/MetaMask/eth-sig-util/pull/107 + # encode_data() is the way to process structs in arrays, but + # Metamask V4 is using hash_struct() even in this case + if self.metamask_v4_compat: + res = await self.hash_struct( + primary_type=struct_name, + member_path=el_member_path, + show_data=show_array, + parent_objects=current_parent_objects, + ) + arr_w.extend(res) + else: + await self.get_and_encode_data( + w=arr_w, + primary_type=struct_name, + member_path=el_member_path, + show_data=show_array, + parent_objects=current_parent_objects, + ) + else: + value = await get_value(self.ctx, entry_type, el_member_path) + encode_field(arr_w, entry_type, value) + if show_array: + await confirm_typed_value( + ctx=self.ctx, + name=field_name, + value=value, + parent_objects=parent_objects, + primary_type=primary_type, + field=entry_type, + array_index=i, + ) + w.extend(arr_w.get_digest()) + else: + value = await get_value(self.ctx, member.type, member_value_path) + encode_field(w, member.type, value) + if show_data: + await confirm_typed_value( + ctx=self.ctx, + name=field_name, + value=value, + parent_objects=parent_objects, + primary_type=primary_type, + field=member.type, + ) + + +def encode_field( + w: HashWriter, + field: EthereumFieldType, + value: bytes, +) -> None: + """ + SPEC: + Atomic types: + - Boolean false and true are encoded as uint256 values 0 and 1 respectively + - Addresses are encoded as uint160 + - Integer values are sign-extended to 256-bit and encoded in big endian order + - Bytes1 to bytes31 are arrays with a beginning (index 0) + and an end (index length - 1), they are zero-padded at the end to bytes32 and encoded + in beginning to end order + Dynamic types: + - Bytes and string are encoded as a keccak256 hash of their contents + Reference types: + - Array values are encoded as the keccak256 hash of the concatenated + encodeData of their contents + - Struct values are encoded recursively as hashStruct(value) + """ + data_type = field.data_type + + if data_type == EthereumDataType.BYTES: + if field.size is None: + w.extend(keccak256(value)) + else: + write_rightpad32(w, value) + elif data_type == EthereumDataType.STRING: + w.extend(keccak256(value)) + elif data_type == EthereumDataType.INT: + write_leftpad32(w, value, signed=True) + elif data_type in [ + EthereumDataType.UINT, + EthereumDataType.BOOL, + EthereumDataType.ADDRESS, + ]: + write_leftpad32(w, value) + else: + raise ValueError # Unsupported data type for field encoding + + +def write_leftpad32(w: HashWriter, value: bytes, signed: bool = False) -> None: + assert len(value) <= 32 + + # Values need to be sign-extended, so accounting for negative ints + if signed and value[0] & 0x80: + pad_value = 0xFF + else: + pad_value = 0x00 + + for _ in range(32 - len(value)): + w.append(pad_value) + w.extend(value) + + +def write_rightpad32(w: HashWriter, value: bytes) -> None: + assert len(value) <= 32 + + w.extend(value) + for _ in range(32 - len(value)): + w.append(0x00) + + +def validate_value(field: EthereumFieldType, value: bytes) -> None: + """ + Make sure the byte data we receive are not corrupted or incorrect. + + Raise wire.DataError if encountering a problem, so clients are notified. + """ + # Checking if the size corresponds to what is defined in types, + # and also setting our maximum supported size in bytes + if field.size is not None: + if len(value) != field.size: + raise wire.DataError("Invalid length") + else: + if len(value) > MAX_VALUE_BYTE_SIZE: + raise wire.DataError(f"Invalid length, bigger than {MAX_VALUE_BYTE_SIZE}") + + # Specific tests for some data types + if field.data_type == EthereumDataType.BOOL: + if value not in (b"\x00", b"\x01"): + raise wire.DataError("Invalid boolean value") + elif field.data_type == EthereumDataType.ADDRESS: + if len(value) != 20: + raise wire.DataError("Invalid address") + elif field.data_type == EthereumDataType.STRING: + try: + value.decode() + except UnicodeError: + raise wire.DataError("Invalid UTF-8") + + +def validate_field_type(field: EthereumFieldType) -> None: + """ + Make sure the field type is consistent with our expectation. + + Raise wire.DataError if encountering a problem, so clients are notified. + """ + data_type = field.data_type + + # entry_type is only for arrays + if data_type == EthereumDataType.ARRAY: + if field.entry_type is None: + raise wire.DataError("Missing entry_type in array") + # We also need to validate it recursively + validate_field_type(field.entry_type) + else: + if field.entry_type is not None: + raise wire.DataError("Unexpected entry_type in nonarray") + + # struct_name is only for structs + if data_type == EthereumDataType.STRUCT: + if field.struct_name is None: + raise wire.DataError("Missing struct_name in struct") + else: + if field.struct_name is not None: + raise wire.DataError("Unexpected struct_name in nonstruct") + + # size is special for each type + if data_type == EthereumDataType.STRUCT: + if field.size is None: + raise wire.DataError("Missing size in struct") + elif data_type == EthereumDataType.BYTES: + if field.size is not None and not 1 <= field.size <= 32: + raise wire.DataError("Invalid size in bytes") + elif data_type in [ + EthereumDataType.UINT, + EthereumDataType.INT, + ]: + if field.size is None or not 1 <= field.size <= 32: + raise wire.DataError("Invalid size in int/uint") + elif data_type in [ + EthereumDataType.STRING, + EthereumDataType.BOOL, + EthereumDataType.ADDRESS, + ]: + if field.size is not None: + raise wire.DataError("Unexpected size in str/bool/addr") + + +async def get_array_size(ctx: Context, member_path: list) -> int: + """Get the length of an array at specific `member_path` from the client.""" + # Field type for getting the array length from client, so we can check the return value + ARRAY_LENGTH_TYPE = EthereumFieldType(data_type=EthereumDataType.UINT, size=2) + length_value = await get_value(ctx, ARRAY_LENGTH_TYPE, member_path) + return int.from_bytes(length_value, "big") + + +async def get_value( + ctx: Context, + field: EthereumFieldType, + member_value_path: list, +) -> bytes: + """Get a single value from the client and perform its validation.""" + req = EthereumTypedDataValueRequest( + member_path=member_value_path, + ) + res = await ctx.call(req, EthereumTypedDataValueAck) + value = res.value + + validate_value(field=field, value=value) + + return value + + +async def get_name_and_version_for_domain( + ctx: Context, typed_data_envelope: TypedDataEnvelope +) -> tuple: + domain_name = b"unknown" + domain_version = b"unknown" + + domain_members = typed_data_envelope.types["EIP712Domain"].members + for member_index, member in enumerate(domain_members): + member_value_path = [0] + [member_index] + if member.name == "name": + domain_name = await get_value(ctx, member.type, member_value_path) + elif member.name == "version": + domain_version = await get_value(ctx, member.type, member_value_path) + + return domain_name, domain_version diff --git a/core/src/apps/ethereum/typed_data.py b/core/src/apps/ethereum/typed_data.py deleted file mode 100644 index 5e9f380630b..00000000000 --- a/core/src/apps/ethereum/typed_data.py +++ /dev/null @@ -1,637 +0,0 @@ -from ubinascii import hexlify - -from trezor import ui, wire -from trezor.crypto.hashlib import sha3_256 -from trezor.enums import ButtonRequestType, EthereumDataType -from trezor.messages import ( - EthereumFieldType, - EthereumTypedDataStructAck, - EthereumTypedDataStructRequest, - EthereumTypedDataValueAck, - EthereumTypedDataValueRequest, -) -from trezor.ui.components.tt.text import Text -from trezor.ui.layouts import confirm_blob, confirm_text -from trezor.utils import HashWriter - -from apps.common.confirm import confirm - -from .address import address_from_bytes - -if False: - from typing import Iterable, Optional - from trezor.wire import Context - - -# TODO: get better layouts - -# Maximum data size we support -MAX_VALUE_BYTE_SIZE = 1024 -# Field type for getting the array length from client, so we can check the return value -ARRAY_LENGTH_TYPE = EthereumFieldType(data_type=EthereumDataType.UINT, size=2) - - -def get_hash_writer() -> HashWriter: - return HashWriter(sha3_256(keccak=True)) - - -def keccak256(message: bytes) -> bytes: - h = get_hash_writer() - h.extend(message) - return h.get_digest() - - -class TypedDataEnvelope: - """Encapsulates the type information for the message being hashed and signed.""" - - def __init__( - self, - ctx: Context, - primary_type: str, - metamask_v4_compat: bool, - ) -> None: - self.ctx = ctx - self.primary_type = primary_type - self.metamask_v4_compat = metamask_v4_compat - self.types: dict[str, EthereumTypedDataStructAck] = {} - - async def collect_types(self) -> None: - await self._collect_types("EIP712Domain") - await self._collect_types(self.primary_type) - - async def _collect_types(self, type_name: str) -> None: - """ - Recursively collects types from the client - """ - req = EthereumTypedDataStructRequest(name=type_name) - current_type = await self.ctx.call(req, EthereumTypedDataStructAck) - self.types[type_name] = current_type - for member in current_type.members: - validate_field_type(member.type) - if ( - member.type.data_type == EthereumDataType.STRUCT - and member.type.struct_name not in self.types - ): - assert member.type.struct_name is not None # validate_field_type - await self._collect_types(member.type.struct_name) - - async def hash_struct( - self, - primary_type: str, - member_path: list, - show_data: bool, - parent_objects: list, - ) -> bytes: - """Generate a hash representation of the whole struct.""" - w = get_hash_writer() - self.hash_type(w, primary_type) - await self.get_and_encode_data( - w=w, - primary_type=primary_type, - member_path=member_path, - show_data=show_data, - parent_objects=parent_objects, - ) - return w.get_digest() - - def hash_type(self, w: HashWriter, primary_type: str) -> None: - """Create a representation of a type.""" - result = keccak256(self.encode_type(primary_type)) - w.extend(result) - - def encode_type(self, primary_type: str) -> bytes: - """ - SPEC: - The type of a struct is encoded as name ‖ "(" ‖ member₁ ‖ "," ‖ member₂ ‖ "," ‖ … ‖ memberₙ ")" - where each member is written as type ‖ " " ‖ name - If the struct type references other struct types (and these in turn reference even more struct types), - then the set of referenced struct types is collected, sorted by name and appended to the encoding. - """ - result: list[str] = [] - - deps: set[str] = set() - self.find_typed_dependencies(primary_type, deps) - deps.remove(primary_type) - primary_first_sorted_deps = [primary_type] + sorted(deps) - - for type_name in primary_first_sorted_deps: - members = self.types[type_name].members - fields = ",".join(f"{get_type_name(m.type)} {m.name}" for m in members) - result.append(f"{type_name}({fields})") - - return "".join(result).encode() - - def find_typed_dependencies( - self, - primary_type: str, - results: set, - ) -> None: - """Find all types within a type definition object.""" - # We already have this type or it is not even a defined type - if (primary_type in results) or (primary_type not in self.types): - return - - results.add(primary_type) - - # Recursively adding all the children struct types, - # also looking into (even nested) arrays for them - for member in self.types[primary_type].members: - member_type = member.type - while member_type.data_type == EthereumDataType.ARRAY: - assert member_type.entry_type is not None # validate_field_type - member_type = member_type.entry_type - if member_type.data_type == EthereumDataType.STRUCT: - assert member_type.struct_name is not None # validate_field_type - self.find_typed_dependencies(member_type.struct_name, results) - - async def get_and_encode_data( - self, - w: HashWriter, - primary_type: str, - member_path: list, - show_data: bool, - parent_objects: list, - ) -> None: - """ - Gradually fetch data from client and encode the whole struct. - - SPEC: - The encoding of a struct instance is enc(value₁) ‖ enc(value₂) ‖ … ‖ enc(valueₙ), - i.e. the concatenation of the encoded member values in the order that they appear in the type. - Each encoded member value is exactly 32-byte long. - """ - type_members = self.types[primary_type].members - for member_index, member in enumerate(type_members): - member_value_path = member_path + [member_index] - data_type = member.type.data_type - field_name = member.name - - # Arrays and structs need special recursive handling - if data_type == EthereumDataType.STRUCT: - assert member.type.struct_name is not None # validate_field_type - struct_name = member.type.struct_name - current_parent_objects = parent_objects + [field_name] - - if show_data: - show_struct = await should_show_struct( - ctx=self.ctx, - primary_type=struct_name, - parent_objects=current_parent_objects, - typed_data_envelope=self, - ) - else: - show_struct = False - - res = await self.hash_struct( - primary_type=struct_name, - member_path=member_value_path, - show_data=show_struct, - parent_objects=current_parent_objects, - ) - w.extend(res) - elif data_type == EthereumDataType.ARRAY: - # Getting the length of the array first, if not fixed - if member.type.size is None: - array_size = await get_array_size(self.ctx, member_value_path) - else: - array_size = member.type.size - - assert member.type.entry_type is not None # validate_field_type - entry_type = member.type.entry_type - current_parent_objects = parent_objects + [field_name] - - if show_data: - show_array = await should_show_array( - ctx=self.ctx, - name=member.name, - parent_objects=current_parent_objects, - data_type=get_type_name(entry_type), - size=array_size, - ) - else: - show_array = False - - arr_w = get_hash_writer() - for i in range(array_size): - el_member_path = member_value_path + [i] - # TODO: we do not support arrays of arrays, check if we should - if entry_type.data_type == EthereumDataType.STRUCT: - assert entry_type.struct_name is not None # validate_field_type - struct_name = entry_type.struct_name - # Metamask V4 implementation has a bug, that causes the - # behavior of structs in array be different from SPEC - # Explanation at https://github.com/MetaMask/eth-sig-util/pull/107 - # encode_data() is the way to process structs in arrays, but - # Metamask V4 is using hash_struct() even in this case - if self.metamask_v4_compat: - res = await self.hash_struct( - primary_type=struct_name, - member_path=el_member_path, - show_data=show_array, - parent_objects=current_parent_objects, - ) - arr_w.extend(res) - else: - await self.get_and_encode_data( - w=arr_w, - primary_type=struct_name, - member_path=el_member_path, - show_data=show_array, - parent_objects=current_parent_objects, - ) - else: - value = await get_value(self.ctx, entry_type, el_member_path) - encode_field(arr_w, entry_type, value) - if show_array: - await confirm_typed_value( - ctx=self.ctx, - name=field_name, - value=value, - parent_objects=parent_objects, - primary_type=primary_type, - field=entry_type, - array_index=i, - ) - w.extend(arr_w.get_digest()) - else: - value = await get_value(self.ctx, member.type, member_value_path) - encode_field(w, member.type, value) - if show_data: - await confirm_typed_value( - ctx=self.ctx, - name=field_name, - value=value, - parent_objects=parent_objects, - primary_type=primary_type, - field=member.type, - ) - - -def encode_field( - w: HashWriter, - field: EthereumFieldType, - value: bytes, -) -> None: - """ - SPEC: - Atomic types: - - Boolean false and true are encoded as uint256 values 0 and 1 respectively - - Addresses are encoded as uint160 - - Integer values are sign-extended to 256-bit and encoded in big endian order - - Bytes1 to bytes31 are arrays with a beginning (index 0) - and an end (index length - 1), they are zero-padded at the end to bytes32 and encoded - in beginning to end order - Dynamic types: - - Bytes and string are encoded as a keccak256 hash of their contents - Reference types: - - Array values are encoded as the keccak256 hash of the concatenated - encodeData of their contents - - Struct values are encoded recursively as hashStruct(value) - """ - data_type = field.data_type - - if data_type == EthereumDataType.BYTES: - if field.size is None: - w.extend(keccak256(value)) - else: - write_rightpad32(w, value) - elif data_type == EthereumDataType.STRING: - w.extend(keccak256(value)) - elif data_type == EthereumDataType.INT: - write_leftpad32(w, value, signed=True) - elif data_type in [ - EthereumDataType.UINT, - EthereumDataType.BOOL, - EthereumDataType.ADDRESS, - ]: - write_leftpad32(w, value) - else: - raise ValueError # Unsupported data type for field encoding - - -def write_leftpad32(w: HashWriter, value: bytes, signed: bool = False) -> None: - assert len(value) <= 32 - - # Values need to be sign-extended, so accounting for negative ints - if signed and value[0] & 0x80: - pad_value = 0xFF - else: - pad_value = 0x00 - - for _ in range(32 - len(value)): - w.append(pad_value) - w.extend(value) - - -def write_rightpad32(w: HashWriter, value: bytes) -> None: - assert len(value) <= 32 - - w.extend(value) - for _ in range(32 - len(value)): - w.append(0x00) - - -def validate_value(field: EthereumFieldType, value: bytes) -> None: - """ - Make sure the byte data we receive are not corrupted or incorrect. - - Raise wire.DataError if encountering a problem, so clients are notified. - """ - # Checking if the size corresponds to what is defined in types, - # and also setting our maximum supported size in bytes - if field.size is not None: - if len(value) != field.size: - raise wire.DataError("Invalid length") - else: - if len(value) > MAX_VALUE_BYTE_SIZE: - raise wire.DataError(f"Invalid length, bigger than {MAX_VALUE_BYTE_SIZE}") - - # Specific tests for some data types - if field.data_type == EthereumDataType.BOOL: - if value not in (b"\x00", b"\x01"): - raise wire.DataError("Invalid boolean value") - elif field.data_type == EthereumDataType.ADDRESS: - if len(value) != 20: - raise wire.DataError("Invalid address") - elif field.data_type == EthereumDataType.STRING: - try: - value.decode() - except UnicodeError: - raise wire.DataError("Invalid UTF-8") - - -def validate_field_type(field: EthereumFieldType) -> None: - """ - Make sure the field type is consistent with our expectation. - - Raise wire.DataError if encountering a problem, so clients are notified. - """ - data_type = field.data_type - - # entry_type is only for arrays - if data_type == EthereumDataType.ARRAY: - if field.entry_type is None: - raise wire.DataError("Missing entry_type in array") - # We also need to validate it recursively - validate_field_type(field.entry_type) - else: - if field.entry_type is not None: - raise wire.DataError("Unexpected entry_type in nonarray") - - # struct_name is only for structs - if data_type == EthereumDataType.STRUCT: - if field.struct_name is None: - raise wire.DataError("Missing struct_name in struct") - else: - if field.struct_name is not None: - raise wire.DataError("Unexpected struct_name in nonstruct") - - # size is special for each type - if data_type == EthereumDataType.STRUCT: - if field.size is None: - raise wire.DataError("Missing size in struct") - elif data_type == EthereumDataType.BYTES: - if field.size is not None and not 1 <= field.size <= 32: - raise wire.DataError("Invalid size in bytes") - elif data_type in [ - EthereumDataType.UINT, - EthereumDataType.INT, - ]: - if field.size is None or not 1 <= field.size <= 32: - raise wire.DataError("Invalid size in int/uint") - elif data_type in [ - EthereumDataType.STRING, - EthereumDataType.BOOL, - EthereumDataType.ADDRESS, - ]: - if field.size is not None: - raise wire.DataError("Unexpected size in str/bool/addr") - - -def get_type_name(field: EthereumFieldType) -> str: - """Create a string from type definition (like uint256 or bytes16).""" - data_type = field.data_type - size = field.size - - TYPE_TRANSLATION_DICT = { - EthereumDataType.UINT: "uint", - EthereumDataType.INT: "int", - EthereumDataType.BYTES: "bytes", - EthereumDataType.STRING: "string", - EthereumDataType.BOOL: "bool", - EthereumDataType.ADDRESS: "address", - } - - if data_type == EthereumDataType.STRUCT: - assert field.struct_name is not None # validate_field_type - return field.struct_name - elif data_type == EthereumDataType.ARRAY: - assert field.entry_type is not None # validate_field_type - entry_type = field.entry_type - type_name = get_type_name(entry_type) - if size is None: - return f"{type_name}[]" - else: - return f"{type_name}[{size}]" - elif data_type in (EthereumDataType.UINT, EthereumDataType.INT): - assert size is not None # validate_field_type - return TYPE_TRANSLATION_DICT[data_type] + str(size * 8) - elif data_type == EthereumDataType.BYTES: - if size: - return TYPE_TRANSLATION_DICT[data_type] + str(size) - else: - return TYPE_TRANSLATION_DICT[data_type] - else: - # all remaining types can use the name directly - # if the data_type is left out, this will raise KeyError - return TYPE_TRANSLATION_DICT[data_type] - - -def decode_data(data: bytes, type_name: str) -> str: - if type_name.startswith("bytes"): - return hexlify(data).decode() - elif type_name == "string": - return data.decode() - elif type_name == "address": - return address_from_bytes(data) - elif type_name == "bool": - return "true" if data == b"\x01" else "false" - elif type_name.startswith("uint"): - return str(int.from_bytes(data, "big")) - elif type_name.startswith("int"): - # Micropython does not implement "signed" arg in int.from_bytes() - return str(from_bytes_bigendian_signed(data)) - - raise ValueError # Unsupported data type for direct field decoding - - -def from_bytes_bigendian_signed(b: bytes) -> int: - negative = b[0] & 0x80 - if negative: - neg_b = bytearray(b) - for i in range(len(neg_b)): - neg_b[i] = ~neg_b[i] & 0xFF - result = int.from_bytes(neg_b, "big") - return -result - 1 - else: - return int.from_bytes(b, "big") - - -async def get_array_size(ctx: Context, member_path: list) -> int: - """Get the length of an array at specific `member_path` from the client.""" - length_value = await get_value(ctx, ARRAY_LENGTH_TYPE, member_path) - return int.from_bytes(length_value, "big") - - -async def get_value( - ctx: Context, - field: EthereumFieldType, - member_value_path: list, -) -> bytes: - """Get a single value from the client and perform its validation.""" - req = EthereumTypedDataValueRequest( - member_path=member_value_path, - ) - res = await ctx.call(req, EthereumTypedDataValueAck) - value = res.value - - validate_value(field=field, value=value) - - return value - - -async def confirm_typed_value( - ctx: Context, - name: str, - value: bytes, - parent_objects: Iterable[str], - primary_type: str, - field: EthereumFieldType, - array_index: Optional[int] = None, -) -> None: - type_name = get_type_name(field) - if parent_objects: - title = f"{'.'.join(parent_objects)} - {primary_type}" - else: - title = primary_type - - if array_index is not None: - array_str = f"[{array_index}]" - else: - array_str = "" - - description = f"{name}{array_str} ({type_name})" - data = decode_data(value, type_name) - - if field.data_type in (EthereumDataType.ADDRESS, EthereumDataType.BYTES): - await confirm_blob( - ctx, - "show_data", - title=title, - data=data, - description=description, - br_code=ButtonRequestType.Other, - ) - else: - await confirm_text( - ctx, - "show_data", - title=title, - data=data, - description=description, - br_code=ButtonRequestType.Other, - ) - - -async def should_show_struct( - ctx: Context, - primary_type: str, - parent_objects: Iterable[str], - typed_data_envelope: TypedDataEnvelope, -) -> bool: - data_members = typed_data_envelope.types[primary_type].members - - title = f"{'.'.join(parent_objects)} - {primary_type}" - page = Text(title, ui.ICON_SEND, icon_color=ui.GREEN) - - # We have limited screen space, so showing only a preview when having lot of fields - MAX_FIELDS_TO_SHOW = 3 - fields_amount = len(data_members) - if fields_amount > MAX_FIELDS_TO_SHOW: - for field in data_members[:MAX_FIELDS_TO_SHOW]: - page.bold(limit_str(field.name)) - page.mono(f"...and {fields_amount - MAX_FIELDS_TO_SHOW} more.") - else: - for field in data_members: - page.bold(limit_str(field.name)) - - page.mono("View full struct?") - - return await confirm(ctx, page, ButtonRequestType.Other) - - -async def should_show_array( - ctx: Context, - name: str, - parent_objects: Iterable[str], - data_type: str, - size: int, -) -> bool: - title = f"{'.'.join(parent_objects)} - {name}" - page = Text(title, ui.ICON_SEND, icon_color=ui.GREEN) - - page.bold(limit_str(f"Type: {data_type}")) - page.bold(limit_str(f"Size: {size}")) - page.br() - page.mono("View full array?") - - return await confirm(ctx, page, ButtonRequestType.Other) - - -async def should_show_domain( - ctx: Context, typed_data_envelope: TypedDataEnvelope -) -> bool: - # Getting the name and version - name = b"unknown" - version = b"unknown" - - domain_members = typed_data_envelope.types["EIP712Domain"].members - for member_index, member in enumerate(domain_members): - member_value_path = [0] + [member_index] - if member.name == "name": - name = await get_value(ctx, member.type, member_value_path) - elif member.name == "version": - version = await get_value(ctx, member.type, member_value_path) - - page = Text("Typed Data", ui.ICON_SEND, icon_color=ui.GREEN) - - domain_name = decode_data(name, "string") - domain_version = decode_data(version, "string") - - page.bold(f"Name: {domain_name}") - page.normal(f"Version: {domain_version}") - page.br() - page.mono("View EIP712Domain?") - - return await confirm(ctx, page, ButtonRequestType.Other) - - -async def confirm_hash(ctx: Context, primary_type: str, typed_data_hash: bytes) -> None: - data = "0x" + hexlify(typed_data_hash).decode() - await confirm_blob( - ctx, - "confirm_resulting_hash", - title="Sign typed data?", - description=f"Hashed {primary_type}:", - data=data, - hold=True, - icon=ui.ICON_CONFIG, - icon_color=ui.GREEN, - ) - - -def limit_str(s: str, limit: int = 16) -> str: - if len(s) <= limit + 2: - return s - - return s[:limit] + ".." diff --git a/core/src/apps/ethereum/verify_message.py b/core/src/apps/ethereum/verify_message.py index be06f9bd3e3..26b9130f953 100644 --- a/core/src/apps/ethereum/verify_message.py +++ b/core/src/apps/ethereum/verify_message.py @@ -6,7 +6,7 @@ from apps.common.signverify import decode_message -from .address import address_from_bytes, bytes_from_address +from .helpers import address_from_bytes, bytes_from_address from .sign_message import message_digest if False: diff --git a/core/tests/test_apps.ethereum.typed_data.py b/core/tests/test_apps.ethereum.typed_data.py index 200e8f2572c..2675a803969 100644 --- a/core/tests/test_apps.ethereum.typed_data.py +++ b/core/tests/test_apps.ethereum.typed_data.py @@ -9,14 +9,16 @@ if not utils.BITCOIN_ONLY: - from apps.ethereum.typed_data import ( + from apps.ethereum.sign_typed_data import ( encode_field, validate_value, validate_field_type, keccak256, + TypedDataEnvelope, + ) + from apps.ethereum.helpers import ( get_type_name, decode_data, - TypedDataEnvelope, )