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:
parent
bbef9c650b
commit
fd502f122f
@ -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)
|
||||||
|
@ -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),
|
||||||
|
)
|
||||||
|
@ -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,
|
||||||
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user