diff --git a/src/apps/common/request_passphrase.py b/src/apps/common/request_passphrase.py index 9fc8622c0a..fcd61ee21b 100644 --- a/src/apps/common/request_passphrase.py +++ b/src/apps/common/request_passphrase.py @@ -1,57 +1,54 @@ -from trezor import ui, wire, res +from trezor import res, ui, wire -async def request_passphrase(ctx): +async def request_passphrase_on_display(ctx): + from trezor.messages.FailureType import ActionCancelled + from trezor.ui.confirm import ConfirmDialog, CONFIRMED + from trezor.ui.passphrase import PassphraseKeyboard + + ui.display.clear() + keyboard = PassphraseKeyboard('Enter passphrase') + + while True: + result = await keyboard + if result: + print(result) + else: + raise wire.FailureError(ActionCancelled, 'Passphrase cancelled') + + +async def request_passphrase_on_host(ctx): from trezor.messages.FailureType import ActionCancelled from trezor.messages.PassphraseRequest import PassphraseRequest from trezor.messages.wire_types import PassphraseAck, Cancel from trezor.ui.text import Text - from trezor.ui.confirm import ConfirmDialog, CONFIRMED + + text = Text( + 'Passphrase entry', ui.ICON_RESET, + 'Please, type passphrase', 'on connected host.') + ui.display.clear() + text.render() + ack = await ctx.call(PassphraseRequest(), PassphraseAck, Cancel) + if ack.MESSAGE_WIRE_TYPE == Cancel: + raise wire.FailureError(ActionCancelled, 'Passphrase cancelled') + return ack.passphrase + + +async def request_passphrase(ctx): + from trezor.ui.text import Text from trezor.ui.entry_select import EntrySelector - from trezor.ui.keyboard import PassphraseKeyboard ui.display.clear() - text = Text('Enter passphrase', ui.ICON_RESET, - 'Where to enter your', 'passphrase?') + text = Text( + 'Enter passphrase', ui.ICON_RESET, + 'Where to enter your', 'passphrase?') entry = EntrySelector(text) entry_type = await entry if entry_type == 1: - ui.display.clear() - text = Text('Passphrase entry', ui.ICON_RESET, - 'Please, type passphrase', 'on connected host.') - text.render() - ack = await ctx.call(PassphraseRequest(), PassphraseAck, Cancel) - if ack.MESSAGE_WIRE_TYPE == Cancel: - raise wire.FailureError(ActionCancelled, 'Passphrase cancelled') - return ack.passphrase + return await request_passphrase_on_host(ctx) else: - def onchange(): - c = dialog.cancel - if keyboard.passphrase: - c.content = res.load(ui.ICON_CLEAR) - else: - c.content = res.load(ui.ICON_LOCK) - c.taint() - c.render() - - ui.display.clear() - keyboard = PassphraseKeyboard('Enter passphrase') - keyboard.onchange = onchange - dialog = ConfirmDialog(keyboard) - dialog.cancel.area = ui.grid(12) - dialog.confirm.area = ui.grid(14) - keyboard.onchange() - - while True: - result = await dialog - if result == CONFIRMED: - return keyboard.passphrase - elif result != CONFIRMED and keyboard.passphrase: - keyboard.change('') - continue - else: - raise wire.FailureError(ActionCancelled, 'Passphrase cancelled') + return await request_passphrase_on_display(ctx) async def protect_by_passphrase(ctx): diff --git a/src/trezor/ui/entry_select.py b/src/trezor/ui/entry_select.py index f5f7c26a0b..1eee99e37c 100644 --- a/src/trezor/ui/entry_select.py +++ b/src/trezor/ui/entry_select.py @@ -12,12 +12,8 @@ class EntrySelector(Widget): def __init__(self, content): self.content = content - self.device = Button(ui.grid(8, n_y=4, n_x=4, cells_x=4), 'Device', - normal_style=ui.BTN_KEY, - active_style=ui.BTN_KEY_ACTIVE) - self.host = Button(ui.grid(12, n_y=4, n_x=4, cells_x=4), 'Host', - normal_style=ui.BTN_KEY, - active_style=ui.BTN_KEY_ACTIVE) + self.device = Button(ui.grid(8, n_y=4, n_x=4, cells_x=4), 'Device') + self.host = Button(ui.grid(12, n_y=4, n_x=4, cells_x=4), 'Host') def render(self): self.device.render() diff --git a/src/trezor/ui/keyboard.py b/src/trezor/ui/keyboard.py index fe79ce27c5..3f5e5c9f60 100644 --- a/src/trezor/ui/keyboard.py +++ b/src/trezor/ui/keyboard.py @@ -1,41 +1,14 @@ -from trezor import ui, res, loop, io +from trezor import io, loop, res, ui from trezor.crypto import bip39 from trezor.ui import display -from trezor.ui.button import Button, BTN_CLICKED, ICON -from .swipe import Swipe, SWIPE_LEFT, SWIPE_RIGHT, SWIPE_HORIZONTAL +from trezor.ui.button import BTN_CLICKED, ICON, Button -KEYBOARD = { - '0': ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'], - '1': ['_', 'abc', 'def', 'ghi', 'jkl', 'mno', 'pqrs', 'tuv', 'wxyz', '*#'], - '2': ['_', 'ABC', 'DEF', 'GHI', 'JKL', 'MNO', 'PQRS', 'TUV', 'WXYZ', '*#'], - '3': ['_', '.', '/', '!', '+', '-', '?', ',', ';', '$'] -} +MNEMONIC_KEYS = ('abc', 'def', 'ghi', 'jkl', 'mno', 'pqr', 'stu', 'vwx', 'yz') -def key_buttons(): - keys = ['abc', 'def', 'ghi', 'jkl', 'mno', 'pqr', 'stu', 'vwx', 'yz'] - return [ - Button(ui.grid(i + 3, n_y=4), k, style=ui.BTN_KEY) - for i, k in enumerate(keys) - ] -def render_scrollbar(page): - bbox = const(240) - size = const(8) - padding = 12 - page_count = len(KEYBOARD) +def key_buttons(keys): + return [Button(ui.grid(i + 3, n_y=4), k) for i, k in enumerate(keys)] - if page_count * padding > bbox: - padding = bbox // page_count - - x = (bbox // 2) - (page_count // 2) * padding - y = 44 - - for i in range(0, page_count): - if i != page: - ui.display.bar_radius(x + i * padding, y, size, - size, ui.DARK_GREY, ui.BG, size // 2) - ui.display.bar_radius(x + page * padding, y, size, - size, ui.FG, ui.BG, size // 2) def compute_mask(text: str) -> int: mask = 0 @@ -46,14 +19,6 @@ def compute_mask(text: str) -> int: mask |= 1 << shift return mask -def digit_area(i): - if i == 9: # 0-position - i = 10 # display it in the middle - return ui.grid(i + 3) # skip the first line - -def generate_keyboard(index): - digits = list(range(0, 10)) # 0-9 - return digits class Input(Button): def __init__(self, area: tuple, content: str='', word: str=''): @@ -109,41 +74,6 @@ class Input(Button): iy = ty - ICON display.icon(ix, iy, res.load(i), fg_color, bg_color) -class PassphraseKeyboard(ui.Widget): - def __init__(self, label): - self.label = label - self.passphrase = '' - self.index = 1 - self.keyboard_type = 1 - self.keys = KEYBOARD[str(self.keyboard_type)] - - self.key_buttons = [Button(digit_area(i), d) - for i, d in enumerate(self.keys)] - self.onchange = None - - def render(self): - # clear canvas under input line - display.bar(0, 0, 240, 45, ui.BG) - - # input line with a header - header = self.passphrase if self.passphrase else self.label - display.text_center(120, 32, header, ui.BOLD, ui.GREY, ui.BG) - render_scrollbar(self.keyboard_type) - # pin matrix buttons - for btn in self.key_buttons: - btn.render() - - def touch(self, event, pos): - for btn in self.key_buttons: - if btn.touch(event, pos) == BTN_CLICKED: - self.change(self.passphrase + btn.content) - break - - def change(self, passphrase): - self.passphrase = passphrase - if self.onchange: - self.onchange() - class MnemonicKeyboard(ui.Widget): def __init__(self, prompt: str=''): @@ -152,7 +82,7 @@ class MnemonicKeyboard(ui.Widget): self.back = Button(ui.grid(0, n_x=4, n_y=4), res.load(ui.ICON_BACK), style=ui.BTN_CLEAR) - self.keys = key_buttons() + self.keys = key_buttons(MNEMONIC_KEYS) self.pbutton = None # pending key button self.pindex = 0 # index of current pending char in pbutton diff --git a/src/trezor/ui/passphrase.py b/src/trezor/ui/passphrase.py new file mode 100644 index 0000000000..76e663c67e --- /dev/null +++ b/src/trezor/ui/passphrase.py @@ -0,0 +1,170 @@ +from micropython import const +from trezor import io, loop, ui, res +from trezor.ui import display +from trezor.ui.button import BTN_CLICKED, ICON, Button +from trezor.ui.swipe import SWIPE_HORIZONTAL, SWIPE_LEFT, SWIPE_RIGHT, Swipe + + +KEYBOARD_KEYS = ( + ('1', '2', '3', '4', '5', '6', '7', '8', '9', '0'), + ('_', 'abc', 'def', 'ghi', 'jkl', 'mno', 'pqrs', 'tuv', 'wxyz', '*#'), + ('_', 'ABC', 'DEF', 'GHI', 'JKL', 'MNO', 'PQRS', 'TUV', 'WXYZ', '*#'), + ('_', '.', '/', '!', '+', '-', '?', ',', ';', '$')) + + +def digit_area(i): + if i == 9: # 0-position + i = 10 # display it in the middle + return ui.grid(i + 3) # skip the first line + + +def key_buttons(keys): + return [Button(digit_area(i), str(k)) for i, k in enumerate(keys)] + + +def render_scrollbar(page): + bbox = const(240) + size = const(8) + padding = 12 + page_count = len(KEYBOARD_KEYS) + + if page_count * padding > bbox: + padding = bbox // page_count + + x = (bbox // 2) - (page_count // 2) * padding + y = 44 + + for i in range(0, page_count): + if i != page: + ui.display.bar_radius( + x + i * padding, y, size, size, ui.DARK_GREY, ui.BG, size // 2) + ui.display.bar_radius( + x + page * padding, y, size, size, ui.FG, ui.BG, size // 2) + + +class Input(Button): + def __init__(self, area: tuple, content: str=''): + super().__init__(area, content) + self.pending = False + self.disable() + + def edit(self, content: str, pending: bool): + self.content = content + self.pending = pending + self.taint() + + def render_content(self, s, ax, ay, aw, ah): + 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.content # input content + + tx = ax + 24 # x-offset of the content + ty = ay + ah // 2 + 8 # y-offset of the content + + # input content + display.text(tx, ty, t, text_style, fg_color, bg_color) + + if p: # pending marker + width = display.text_width(t, text_style) + pw = display.text_width(t[-1:], text_style) + px = tx + width - pw + display.bar(px, ty + 2, pw + 1, 3, fg_color) + + +class PassphraseKeyboard(ui.Widget): + def __init__(self, prompt, page=1): + self.prompt = prompt + 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), + style=ui.BTN_CLEAR) + self.keys = key_buttons(KEYBOARD_KEYS[self.page]) + self.pbutton = None # pending key button + self.pindex = 0 # index of current pending char in pbutton + self.onchange = None + + def render(self): + if self.input.content: + # content and backspace + self.input.render() + self.back.render() + else: + # prompt + display.bar(0, 0, 240, 48, ui.BG) + display.text_center(ui.SCREEN // 2, 32, self.prompt, ui.BOLD, ui.GREY, ui.BG) + + # key buttons + for btn in self.keys: + btn.render() + + render_scrollbar(self.page) + + def touch(self, event, pos): + content = self.input.content + + if self.back.touch(event, pos) == BTN_CLICKED: + # backspace, delete the last character of input + self.edit(content[:-1]) + return + + for btn in self.keys: + if btn.touch(event, pos) == BTN_CLICKED: + # key press, add new char to input or cycle the pending button + if self.pbutton is btn: + index = (self.pindex + 1) % len(btn.content) + content = content[:-1] + btn.content[index] + else: + index = 0 + content += btn.content[0] + self.edit(content, btn, index) + return + + def edit(self, content, button=None, index=0): + self.pbutton = button + self.pindex = index + self.input.edit(content, button is not None) + if self.onchange: + self.onchange() + + async def __iter__(self): + while True: + swipe = Swipe(directions=SWIPE_HORIZONTAL) + wait = loop.wait(swipe, self.show_page()) + result = await wait + if swipe in wait.finished: + if result == 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]) + else: + return result + + @ui.layout + async def show_page(self): + timeout = loop.sleep(1000 * 1000 * 1) + touch = loop.select(io.TOUCH) + wait_timeout = loop.wait(touch, timeout) + wait_touch = loop.wait(touch) + content = None + + self.back.taint() + self.input.taint() + + while content is None: + self.render() + if self.pbutton is not None: + wait = wait_timeout + else: + wait = wait_touch + result = await wait + if touch in wait.finished: + event, *pos = result + content = self.touch(event, pos) + else: + self.edit(self.input.content) + return content