feat(core): implement pagination for sign/verify

pull/1403/head
matejcik 3 years ago committed by matejcik
parent bbef9c650b
commit fd502f122f

@ -7,14 +7,14 @@ from trezor.ui.button import ButtonDefault
from trezor.ui.container import Container
from trezor.ui.qr import Qr
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 apps.common import HARDENED
from apps.common.confirm import confirm, require_confirm
if False:
from typing import Iterable, Iterator, List
from typing import Iterable, Iterator, List, Union
from trezor import wire
@ -136,3 +136,48 @@ async def show_success(
await require_confirm(
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)

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

@ -206,3 +206,79 @@ def test_signmessage(client, coin_name, path, script_type, address, message, sig
)
assert sig.address == address
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,
)

Loading…
Cancel
Save