mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-11-22 07:28:10 +00:00
feat(core): implement pagination for sign/verify
This commit is contained in:
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…
Reference in New Issue
Block a user