1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-11-22 15:38:11 +00:00

feat(core): implement pagination for sign/verify

This commit is contained in:
matejcik 2020-12-11 12:02:28 +01:00 committed by matejcik
parent bbef9c650b
commit fd502f122f
3 changed files with 133 additions and 18 deletions

View File

@ -7,14 +7,14 @@ from trezor.ui.button import ButtonDefault
from trezor.ui.container import Container from trezor.ui.container import Container
from trezor.ui.qr import Qr from trezor.ui.qr import Qr
from trezor.ui.scroll import Paginated from trezor.ui.scroll import Paginated
from trezor.ui.text import Text from trezor.ui.text import TEXT_MAX_LINES, Span, Text
from trezor.utils import chunks from trezor.utils import chunks
from apps.common import HARDENED from apps.common import HARDENED
from apps.common.confirm import confirm, require_confirm from apps.common.confirm import confirm, require_confirm
if False: if False:
from typing import Iterable, Iterator, List from typing import Iterable, Iterator, List, Union
from trezor import wire from trezor import wire
@ -136,3 +136,48 @@ async def show_success(
await require_confirm( await require_confirm(
ctx, text, ButtonRequestType.Success, confirm=button, cancel=None ctx, text, ButtonRequestType.Success, confirm=button, cancel=None
) )
def paginate_text(
text: str,
header: str,
font: int = ui.NORMAL,
header_icon: str = ui.ICON_DEFAULT,
icon_color: int = ui.ORANGE_ICON,
break_words: bool = False,
) -> Union[Text, Paginated]:
span = Span(text, 0, font, break_words=break_words)
if span.count_lines() <= TEXT_MAX_LINES:
result = Text(
header,
header_icon=header_icon,
icon_color=icon_color,
new_lines=False,
)
result.content = [font, text]
return result
else:
pages: List[ui.Component] = []
span.reset(text, 0, font, break_words=break_words, line_width=204)
while span.has_more_content():
# advance to first line of the page
span.next_line()
page = Text(
header,
header_icon=header_icon,
icon_color=icon_color,
new_lines=False,
content_offset=0,
char_offset=span.start,
line_width=204,
render_page_overflow=False,
)
page.content = [font, text]
pages.append(page)
# roll over the remaining lines on the page
for _ in range(TEXT_MAX_LINES - 1):
span.next_line()
return Paginated(pages)

View File

@ -1,15 +1,14 @@
from ubinascii import hexlify from ubinascii import hexlify
from trezor import utils, wire from trezor import ui, utils, wire
from trezor.crypto.hashlib import blake256, sha256 from trezor.crypto.hashlib import blake256, sha256
from trezor.ui.text import Text from trezor.ui.text import Text
from apps.common.confirm import require_confirm from apps.common.confirm import require_confirm
from apps.common.layout import split_address from apps.common.layout import paginate_text, split_address
from apps.common.writers import write_bitcoin_varint from apps.common.writers import write_bitcoin_varint
if False: if False:
from typing import List
from apps.common.coininfo import CoinInfo from apps.common.coininfo import CoinInfo
@ -30,24 +29,18 @@ def message_digest(coin: CoinInfo, message: bytes) -> bytes:
return ret return ret
def split_message(message: bytes) -> List[str]: def decode_message(message: bytes) -> str:
try: try:
m = bytes(message).decode() return bytes(message).decode()
words = m.split(" ")
except UnicodeError: except UnicodeError:
m = "hex(%s)" % hexlify(message).decode() return "hex(%s)" % hexlify(message).decode()
words = [m]
return words
async def require_confirm_sign_message( async def require_confirm_sign_message(
ctx: wire.Context, coin: str, message: bytes ctx: wire.Context, coin: str, message: bytes
) -> None: ) -> None:
header = "Sign {} message".format(coin) header = "Sign {} message".format(coin)
message_lines = split_message(message) await require_confirm(ctx, paginate_text(decode_message(message), header))
text = Text(header, new_lines=False)
text.normal(*message_lines)
await require_confirm(ctx, text)
async def require_confirm_verify_message( async def require_confirm_verify_message(
@ -60,6 +53,7 @@ async def require_confirm_verify_message(
text.mono(*split_address(address)) text.mono(*split_address(address))
await require_confirm(ctx, text) await require_confirm(ctx, text)
text = Text(header, new_lines=False) await require_confirm(
text.mono(*split_message(message)) ctx,
await require_confirm(ctx, text) paginate_text(decode_message(message), header, font=ui.MONO),
)

View File

@ -206,3 +206,79 @@ def test_signmessage(client, coin_name, path, script_type, address, message, sig
) )
assert sig.address == address assert sig.address == address
assert sig.signature.hex() == signature assert sig.signature.hex() == signature
MESSAGE_LENGTHS = (
pytest.param("This is a very long message. " * 16, id="normal_text"),
pytest.param("ThisIsAMessageWithoutSpaces" * 16, id="no_spaces"),
pytest.param("ThisIsAMessageWithLongWords " * 16, id="long_words"),
pytest.param("This\nmessage\nhas\nnewlines\nafter\nevery\nword", id="newlines"),
pytest.param("Příšerně žluťoučký kůň úpěl ďábelské ódy. " * 16, id="utf_text"),
pytest.param("PříšerněŽluťoučkýKůňÚpělĎábelskéÓdy" * 16, id="utf_nospace"),
pytest.param("1\n2\n3\n4\n5\n6", id="single_line_over"),
)
@pytest.mark.skip_t1
@pytest.mark.parametrize("message", MESSAGE_LENGTHS)
def test_signmessage_pagination(client, message):
message_read = ""
message += "End."
def input_flow():
# collect screen contents into `message_read`.
# Join lines that are separated by a single "-" string, space-separate lines otherwise.
nonlocal message_read
yield
# start assuming there was a word break; this avoids prepending space at start
word_break = True
max_attempts = 100
while max_attempts:
layout = client.debug.wait_layout()
for line in layout.lines[1:]:
if line == "-":
# next line will be attached without space
word_break = True
elif word_break:
# attach without space, reset word_break
message_read += line
word_break = False
else:
# attach with space
message_read += " " + line
if not message_read.endswith("End."):
client.debug.swipe_up()
else:
client.debug.press_yes()
break
max_attempts -= 1
assert max_attempts > 0, "failed to scroll through message"
with client:
client.set_input_flow(input_flow)
client.debug.watch_layout(True)
btc.sign_message(
client,
coin_name="Bitcoin",
n=parse_path("m/44h/0h/0h/0/0"),
message=message,
)
assert message.replace("\n", " ") == message_read
@pytest.mark.skip_t1
def test_signmessage_pagination_trailing_newline(client):
# This can currently only be tested by a human via the UI test diff:
message = "THIS\nMUST\nNOT\nBE\nPAGINATED\n"
# The trailing newline must not cause a new paginated screen to appear.
# The UI must be a single dialog without pagination.
btc.sign_message(
client,
coin_name="Bitcoin",
n=parse_path("m/44h/0h/0h/0/0"),
message=message,
)