You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
trezor-firmware/core/src/trezor/ui/passphrase.py

271 lines
8.3 KiB

from micropython import const
from trezor import io, loop, res, ui
from trezor.messages import PassphraseSourceType
from trezor.ui import display
from trezor.ui.button import Button, ButtonClear, ButtonConfirm
from trezor.ui.swipe import SWIPE_HORIZONTAL, SWIPE_LEFT, Swipe
if False:
from typing import List, Iterable, Optional
from trezor.ui.button import ButtonContent, ButtonStyleStateType
SPACE = res.load(ui.ICON_SPACE)
KEYBOARD_KEYS = (
("1", "2", "3", "4", "5", "6", "7", "8", "9", "0"),
(SPACE, "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz", "*#"),
(SPACE, "ABC", "DEF", "GHI", "JKL", "MNO", "PQRS", "TUV", "WXYZ", "*#"),
("_<>", ".:@", "/|\\", "!()", "+%&", "-[]", "?{}", ",'`", ';"~', "$^="),
)
def digit_area(i: int) -> ui.Area:
if i == 9: # 0-position
i = 10 # display it in the middle
return ui.grid(i + 3) # skip the first line
def render_scrollbar(page: int) -> None:
BBOX = const(240)
SIZE = const(8)
pages = len(KEYBOARD_KEYS)
padding = 12
if pages * padding > BBOX:
padding = BBOX // pages
x = (BBOX // 2) - (pages // 2) * padding
Y = const(44)
for i in range(0, pages):
if i == page:
fg = ui.FG
else:
fg = ui.DARK_GREY
ui.display.bar_radius(x + i * padding, Y, SIZE, SIZE, fg, ui.BG, SIZE // 2)
class KeyButton(Button):
def __init__(
self, area: ui.Area, content: ButtonContent, keyboard: "PassphraseKeyboard"
) -> None:
self.keyboard = keyboard
super().__init__(area, content)
def on_click(self) -> None:
self.keyboard.on_key_click(self)
def get_text_content(self) -> str:
if self.text:
return self.text
elif self.icon is SPACE:
return " "
else:
raise TypeError
def key_buttons(
keys: Iterable[ButtonContent], keyboard: "PassphraseKeyboard"
) -> List[KeyButton]:
return [KeyButton(digit_area(i), k, keyboard) for i, k in enumerate(keys)]
class Input(Button):
def __init__(self, area: ui.Area, text: str) -> None:
super().__init__(area, text)
self.pending = False
self.disable()
def edit(self, text: str, pending: bool) -> None:
self.text = text
self.pending = pending
self.repaint = True
def render_content(
self, s: ButtonStyleStateType, ax: int, ay: int, aw: int, ah: int
) -> None:
text_style = s.text_style
fg_color = s.fg_color
bg_color = s.bg_color
p = self.pending # should we draw the pending marker?
t = self.text # input content
tx = ax + 24 # x-offset of the content
ty = ay + ah // 2 + 8 # y-offset of the content
maxlen = const(14) # maximum text length
# input content
if len(t) > maxlen:
t = "<" + t[-maxlen:] # too long, align to the right
width = display.text_width(t, text_style)
display.text(tx, ty, t, text_style, fg_color, bg_color)
if p: # pending marker
pw = display.text_width(t[-1:], text_style)
px = tx + width - pw
display.bar(px, ty + 2, pw + 1, 3, fg_color)
else: # cursor
cx = tx + width + 1
display.bar(cx, ty - 18, 2, 22, fg_color)
def on_click(self) -> None:
pass
class Prompt(ui.Control):
def __init__(self, text: str) -> None:
self.text = text
self.repaint = True
def on_render(self) -> None:
if self.repaint:
display.bar(0, 0, ui.WIDTH, 48, ui.BG)
display.text_center(ui.WIDTH // 2, 32, self.text, ui.BOLD, ui.GREY, ui.BG)
self.repaint = False
CANCELLED = object()
class PassphraseKeyboard(ui.Layout):
def __init__(self, prompt: str, max_length: int, page: int = 1) -> None:
self.prompt = Prompt(prompt)
self.max_length = max_length
self.page = page
self.input = Input(ui.grid(0, n_x=1, n_y=6), "")
self.back = Button(ui.grid(12), res.load(ui.ICON_BACK), ButtonClear)
self.back.on_click = self.on_back_click # type: ignore
self.back.disable()
self.done = Button(ui.grid(14), res.load(ui.ICON_CONFIRM), ButtonConfirm)
self.done.on_click = self.on_confirm # type: ignore
self.keys = key_buttons(KEYBOARD_KEYS[self.page], self)
self.pending_button = None # type: Optional[KeyButton]
self.pending_index = 0
def dispatch(self, event: int, x: int, y: int) -> None:
if self.input.text:
self.input.dispatch(event, x, y)
else:
self.prompt.dispatch(event, x, y)
self.back.dispatch(event, x, y)
self.done.dispatch(event, x, y)
for btn in self.keys:
btn.dispatch(event, x, y)
if event == ui.RENDER:
render_scrollbar(self.page)
def on_back_click(self) -> None:
# Backspace was clicked. If we have any content in the input, let's delete
# the last character. Otherwise cancel.
text = self.input.text
if text:
self.edit(text[:-1])
else:
self.on_cancel()
def on_key_click(self, button: KeyButton) -> None:
# Key button was clicked. If this button is pending, let's cycle the
# pending character in input. If not, let's just append the first
# character.
button_text = button.get_text_content()
if self.pending_button is button:
index = (self.pending_index + 1) % len(button_text)
prefix = self.input.text[:-1]
else:
index = 0
prefix = self.input.text
if len(button_text) > 1:
self.edit(prefix + button_text[index], button, index)
else:
self.edit(prefix + button_text[index])
def on_timeout(self) -> None:
# Timeout occurred, let's just reset the pending marker.
self.edit(self.input.text)
def edit(self, text: str, button: KeyButton = None, index: int = 0) -> None:
if len(text) > self.max_length:
return
self.pending_button = button
self.pending_index = index
# modify the input state
pending = button is not None
self.input.edit(text, pending)
if text:
self.back.enable()
else:
self.back.disable()
self.prompt.repaint = True
async def handle_input(self) -> None:
touch = loop.wait(io.TOUCH)
timeout = loop.sleep(1000 * 1000 * 1)
spawn_touch = loop.spawn(touch)
spawn_timeout = loop.spawn(touch, timeout)
while True:
if self.pending_button is not None:
spawn = spawn_timeout
else:
spawn = spawn_touch
result = await spawn
if touch in spawn.finished:
event, x, y = result
self.dispatch(event, x, y)
else:
self.on_timeout()
async def handle_paging(self) -> None:
swipe = await Swipe(SWIPE_HORIZONTAL)
if swipe == SWIPE_LEFT:
self.page = (self.page + 1) % len(KEYBOARD_KEYS)
else:
self.page = (self.page - 1) % len(KEYBOARD_KEYS)
self.keys = key_buttons(KEYBOARD_KEYS[self.page], self)
self.back.repaint = True
self.done.repaint = True
self.input.repaint = True
self.prompt.repaint = True
def on_cancel(self) -> None:
raise ui.Result(CANCELLED)
def on_confirm(self) -> None:
raise ui.Result(self.input.text)
def create_tasks(self) -> Iterable[loop.Task]:
return self.handle_input(), self.handle_rendering(), self.handle_paging()
class PassphraseSource(ui.Layout):
def __init__(self, content: ui.Control) -> None:
self.content = content
self.device = Button(ui.grid(8, n_y=4, n_x=4, cells_x=4), "Device")
self.device.on_click = self.on_device # type: ignore
self.host = Button(ui.grid(12, n_y=4, n_x=4, cells_x=4), "Host")
self.host.on_click = self.on_host # type: ignore
def dispatch(self, event: int, x: int, y: int) -> None:
self.content.dispatch(event, x, y)
self.device.dispatch(event, x, y)
self.host.dispatch(event, x, y)
def on_device(self) -> None:
raise ui.Result(PassphraseSourceType.DEVICE)
def on_host(self) -> None:
raise ui.Result(PassphraseSourceType.HOST)