1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-01-17 19:00:58 +00:00

Merge pull request #93 from trezor/recovery

Seed recovery with on-screen keyboard
This commit is contained in:
Jan Pochyla 2018-01-16 16:10:06 +01:00 committed by GitHub
commit 1357ba7202
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 412 additions and 294 deletions

BIN
assets/5390-200.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets/left.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
assets/recovery-old.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
assets/send-old.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -18,7 +18,7 @@ STATIC mp_obj_t mod_trezorcrypto_bip39_find_word(mp_obj_t prefix)
mp_buffer_info_t pfx; mp_buffer_info_t pfx;
mp_get_buffer_raise(prefix, &pfx, MP_BUFFER_READ); mp_get_buffer_raise(prefix, &pfx, MP_BUFFER_READ);
if (pfx.len == 0) { if (pfx.len == 0) {
mp_raise_ValueError("Invalid word prefix"); return mp_const_none;
} }
for (const char * const *w = mnemonic_wordlist(); *w != 0; w++) { for (const char * const *w = mnemonic_wordlist(); *w != 0; w++) {
if (strncmp(*w, pfx.buf, pfx.len) == 0) { if (strncmp(*w, pfx.buf, pfx.len) == 0) {
@ -39,7 +39,7 @@ STATIC mp_obj_t mod_trezorcrypto_bip39_complete_word(mp_obj_t prefix)
mp_buffer_info_t pfx; mp_buffer_info_t pfx;
mp_get_buffer_raise(prefix, &pfx, MP_BUFFER_READ); mp_get_buffer_raise(prefix, &pfx, MP_BUFFER_READ);
if (pfx.len == 0) { if (pfx.len == 0) {
mp_raise_ValueError("Invalid word prefix"); return mp_obj_new_int(0xFFFFFFFF); // all letters
} }
uint32_t res = 0; uint32_t res = 0;
uint8_t bit; uint8_t bit;

View File

@ -106,7 +106,7 @@ int usb_hid_read(uint8_t iface_num, uint8_t *buf, uint32_t len) {
static const char *ping_req = "PINGPING"; static const char *ping_req = "PINGPING";
static const char *ping_resp = "PONGPONG"; static const char *ping_resp = "PONGPONG";
if (r == strlen(ping_req) && memcmp(ping_req, buf, strlen(ping_req)) == 0) { if (r == strlen(ping_req) && memcmp(ping_req, buf, strlen(ping_req)) == 0) {
usb_hid_write(0, (const uint8_t *)ping_resp, strlen(ping_resp)); ensure(usb_hid_write(0, (const uint8_t *)ping_resp, strlen(ping_resp)), "usb_hid_write");
return 0; return 0;
} }
return r; return r;

View File

@ -26,13 +26,12 @@ async def request_pin(code: int = None) -> str:
matrix = PinMatrix(label, with_zero=True) matrix = PinMatrix(label, with_zero=True)
matrix.onchange = onchange matrix.onchange = onchange
dialog = ConfirmDialog(matrix) dialog = ConfirmDialog(matrix)
dialog.cancel.area = (0, 240 - 48, 80, 48) dialog.cancel.area = ui.grid(12)
dialog.confirm.area = (240 - 80, 240 - 48, 80, 48) dialog.confirm.area = ui.grid(14)
matrix.onchange() matrix.onchange()
while True: while True:
result = await dialog result = await dialog
if result == CONFIRMED: if result == CONFIRMED:
return matrix.pin return matrix.pin
elif result != CONFIRMED and matrix.pin: elif result != CONFIRMED and matrix.pin:

View File

@ -0,0 +1,30 @@
from trezor import wire, ui, loop
from trezor.utils import unimport
# used to confirm/cancel the dialogs from outside of this module (i.e.
# through debug link)
if __debug__:
signal = loop.signal()
@ui.layout
@unimport
async def request_words(ctx, content, code=None, *args, **kwargs):
from trezor.ui.word_select import WordSelector
from trezor.messages.ButtonRequest import ButtonRequest
from trezor.messages.ButtonRequestType import Other
from trezor.messages.wire_types import ButtonAck
ui.display.clear()
dialog = WordSelector(content, *args, **kwargs)
dialog.render()
if code is None:
code = Other
await ctx.call(ButtonRequest(code=code), ButtonAck)
if __debug__:
waiter = loop.wait(signal, dialog)
else:
waiter = dialog
return await waiter

View File

@ -24,8 +24,8 @@ def dispatch_WipeDevice(*args, **kwargs):
@unimport @unimport
def dispatch_RecoveryDevice(*args, **kwargs): def dispatch_RecoveryDevice(*args, **kwargs):
from .recovery_device import layout_recovery_device from .recovery_device import recovery_device
return layout_recovery_device(*args, **kwargs) return recovery_device(*args, **kwargs)
@unimport @unimport

View File

@ -1,23 +1,55 @@
from trezor import ui, wire from trezor import ui, wire
from trezor.utils import unimport
def nth(n): async def recovery_device(ctx, msg):
if 4 <= n % 100 <= 20: '''
sfx = 'th' Recover BIP39 seed into empty device.
else:
sfx = {1: 'st', 2: 'nd', 3: 'rd'}.get(n % 10, 'th') 1. Ask for the number of words in recovered seed.
return str(n) + sfx 2. Let user type in the mnemonic words one by one.
3. Optionally check the seed validity.
4. Optionally ask for the PIN, with confirmation.
5. Save into storage.
'''
from trezor import config
from trezor.crypto import bip39
from trezor.messages.FailureType import UnexpectedMessage, ProcessError
from trezor.messages.Success import Success
from trezor.ui.text import Text
from apps.common import storage
from apps.common.request_pin import request_pin
from apps.common.request_words import request_words
if storage.is_initialized():
raise wire.FailureError(UnexpectedMessage, 'Already initialized')
wordcount = await request_words(ctx,
Text('Device recovery', ui.ICON_RECOVERY,
'Number of words?'))
mnemonic = await request_mnemonic(wordcount, 'Type %s. word')
if msg.enforce_wordlist and not bip39.check(mnemonic):
raise wire.FailureError(ProcessError, 'Mnemonic is not valid')
if msg.pin_protection:
curpin = ''
newpin = await request_pin(ctx)
config.change_pin(curpin, newpin)
storage.load_settings(label=msg.label,
use_passphrase=msg.passphrase_protection)
storage.load_mnemonic(mnemonic)
return Success()
@unimport async def request_mnemonic(count: int, prompt: str) -> str:
async def layout_recovery_device(ctx, msg): from trezor.ui.keyboard import MnemonicKeyboard
msg = 'Please enter ' + nth(msg.word_count) + ' word' words = []
board = MnemonicKeyboard()
for i in range(0, count):
board.prompt = prompt % (i + 1)
word = await board
words.append(word)
ui.display.clear() return ' '.join(words)
ui.header('Recovery device', ui.ICON_RECOVERY, ui.BG, ui.LIGHT_GREEN)
ui.display.text(10, 74, msg, ui.BOLD, ui.FG, ui.BG)
ui.display.text(10, 104, 'of your mnemonic.', ui.BOLD, ui.FG, ui.BG)
# TODO

View File

@ -11,6 +11,7 @@ if __debug__:
@unimport @unimport
async def layout_reset_device(ctx, msg): async def layout_reset_device(ctx, msg):
from trezor import config
from trezor.ui.text import Text from trezor.ui.text import Text
from trezor.crypto import hashlib, random, bip39 from trezor.crypto import hashlib, random, bip39
from trezor.messages.EntropyRequest import EntropyRequest from trezor.messages.EntropyRequest import EntropyRequest
@ -19,7 +20,7 @@ async def layout_reset_device(ctx, msg):
from trezor.messages import ButtonRequestType from trezor.messages import ButtonRequestType
from trezor.messages.wire_types import EntropyAck from trezor.messages.wire_types import EntropyAck
from apps.common.request_pin import request_pin_twice from apps.management.change_pin import request_pin_confirm
from apps.common.confirm import require_confirm from apps.common.confirm import require_confirm
from apps.common import storage from apps.common import storage
@ -42,9 +43,11 @@ async def layout_reset_device(ctx, msg):
await require_confirm(ctx, entropy_content, ButtonRequestType.ResetDevice) await require_confirm(ctx, entropy_content, ButtonRequestType.ResetDevice)
if msg.pin_protection: if msg.pin_protection:
pin = await request_pin_twice(ctx) curpin = ''
newpin = await request_pin_confirm(ctx)
else: else:
pin = None curpin = ''
newpin = ''
external_entropy_ack = await ctx.call(EntropyRequest(), EntropyAck) external_entropy_ack = await ctx.call(EntropyRequest(), EntropyAck)
ehash = hashlib.sha256() ehash = hashlib.sha256()
@ -55,11 +58,11 @@ async def layout_reset_device(ctx, msg):
await show_mnemonic_by_word(ctx, mnemonic) await show_mnemonic_by_word(ctx, mnemonic)
if curpin != newpin:
config.change_pin(curpin, newpin)
storage.load_settings(label=msg.label,
use_passphrase=msg.passphrase_protection)
storage.load_mnemonic(mnemonic) storage.load_mnemonic(mnemonic)
storage.load_settings(pin=pin,
passphrase_protection=msg.passphrase_protection,
language=msg.language,
label=msg.label)
return Success(message='Initialized') return Success(message='Initialized')

BIN
src/trezor/res/click.toig Normal file

Binary file not shown.

Binary file not shown.

BIN
src/trezor/res/cross2.toig Normal file

Binary file not shown.

BIN
src/trezor/res/left.toig Normal file

Binary file not shown.

Binary file not shown.

BIN
src/trezor/res/send2.toig Normal file

Binary file not shown.

View File

@ -106,11 +106,36 @@ def layout(f):
return inner return inner
def header(title: str, icon: bytes=ICON_RESET, fg: int=BG, bg: int=BG): def header(title: str,
display.bar(0, 0, 240, 32, bg) icon: bytes=ICON_RESET,
fg: int=BG,
bg: int=BG,
ifg: int=BG):
if icon is not None: if icon is not None:
display.icon(8, 4, res.load(icon), fg, bg) display.icon(14, 18, res.load(icon), ifg, bg)
display.text(8 + 24 + 2, 24, title, BOLD, fg, bg) display.text(44, 35, title, BOLD, fg, bg)
VIEWX = const(6)
VIEWY = const(9)
VIEW = const(228) # SCREEN - 2 * VIEWX
def grid(i: int,
n_x: int=3,
n_y: int=5,
start_x: int=VIEWX,
start_y: int=VIEWY,
end_x: int=(VIEWX + VIEW),
end_y: int=(VIEWY + VIEW),
cells_x: int=1,
cells_y: int=1,
spacing: int=0):
w = (end_x - start_x) // n_x
h = (end_y - start_y) // n_y
x = (i % n_x) * w
y = (i // n_x) * h
return (x + start_x, y + start_y, (w - spacing) * cells_x, (h - spacing) * cells_y)
class Widget: class Widget:

View File

@ -15,6 +15,9 @@ BTN_ACTIVE = const(2)
BTN_DIRTY = const(4) BTN_DIRTY = const(4)
BTN_DISABLED = const(8) BTN_DISABLED = const(8)
ICON = const(16) # icon size in pixels
BORDER = const(4) # border size in pixels
class Button(Widget): class Button(Widget):
@ -25,18 +28,20 @@ class Button(Widget):
absolute=False): absolute=False):
self.area = area self.area = area
self.content = content self.content = content
self.normal_style = normal_style or ui.BTN_DEFAULT self.normal_style = normal_style or ui.BTN_KEY
self.active_style = active_style or ui.BTN_DEFAULT_ACTIVE self.active_style = active_style or ui.BTN_KEY_ACTIVE
self.disabled_style = disabled_style or ui.BTN_DEFAULT_DISABLED self.disabled_style = disabled_style or ui.BTN_KEY_DISABLED
self.absolute = absolute self.absolute = absolute
self.state = BTN_DIRTY self.state = BTN_DIRTY
def enable(self): def enable(self):
self.state &= ~BTN_DISABLED if self.state & BTN_DISABLED:
self.state |= BTN_DIRTY self.state &= ~BTN_DISABLED
self.state |= BTN_DIRTY
def disable(self): def disable(self):
self.state |= BTN_DISABLED | BTN_DIRTY if not self.state & BTN_DISABLED:
self.state |= BTN_DISABLED | BTN_DIRTY
def taint(self): def taint(self):
self.state |= BTN_DIRTY self.state |= BTN_DIRTY
@ -52,46 +57,66 @@ class Button(Widget):
else: else:
s = self.normal_style s = self.normal_style
ax, ay, aw, ah = self.area ax, ay, aw, ah = self.area
tx = ax + aw // 2
ty = ay + ah // 2 + 8
display.bar_radius(ax, ay, aw, ah,
s['border-color'],
ui.BG,
s['radius'])
display.bar_radius(ax + 4, ay + 4, aw - 8, ah - 8,
s['bg-color'],
s['border-color'],
s['radius'])
if isinstance(self.content, str):
display.text_center(tx, ty, self.content,
s['text-style'],
s['fg-color'],
s['bg-color'])
else:
display.icon(tx - 15, ty - 20, self.content,
s['fg-color'],
s['bg-color'])
self.render_background(s, ax, ay, aw, ah)
self.render_content(s, ax, ay, aw, ah)
self.state = state self.state = state
def render_background(self, s, ax, ay, aw, ah):
radius = s['radius']
bg_color = s['bg-color']
border_color = s['border-color']
if border_color != bg_color:
# render border and background on top of it
display.bar_radius(ax, ay,
aw, ah,
border_color,
ui.BG,
radius)
display.bar_radius(ax + BORDER, ay + BORDER,
aw - BORDER*2, ah - BORDER*2,
bg_color,
border_color,
radius)
else:
# render only the background
display.bar_radius(ax, ay,
aw, ah,
bg_color,
ui.BG,
radius)
def render_content(self, s, ax, ay, aw, ah):
c = self.content
tx = ax + aw//2
ty = ay + ah//2 + 8
if isinstance(c, str):
display.text_center(
tx, ty, c, s['text-style'], s['fg-color'], s['bg-color'])
else:
display.icon(
tx - ICON//2, ty - ICON, c, s['fg-color'], s['bg-color'])
def touch(self, event, pos): def touch(self, event, pos):
if self.state & BTN_DISABLED: state = self.state
if state & BTN_DISABLED:
return return
if not self.absolute: if not self.absolute:
pos = rotate(pos) pos = rotate(pos)
if event == io.TOUCH_START: if event == io.TOUCH_START:
if contains(self.area, pos): if contains(self.area, pos):
self.state = BTN_STARTED | BTN_DIRTY | BTN_ACTIVE self.state = BTN_STARTED | BTN_DIRTY | BTN_ACTIVE
elif event == io.TOUCH_MOVE and self.state & BTN_STARTED:
elif event == io.TOUCH_MOVE and state & BTN_STARTED:
if contains(self.area, pos): if contains(self.area, pos):
if not self.state & BTN_ACTIVE: if not state & BTN_ACTIVE:
self.state = BTN_STARTED | BTN_DIRTY | BTN_ACTIVE self.state = BTN_STARTED | BTN_DIRTY | BTN_ACTIVE
else: else:
if self.state & BTN_ACTIVE: if state & BTN_ACTIVE:
self.state = BTN_STARTED | BTN_DIRTY self.state = BTN_STARTED | BTN_DIRTY
elif event == io.TOUCH_END and self.state & BTN_STARTED:
elif event == io.TOUCH_END and state & BTN_STARTED:
self.state = BTN_DIRTY self.state = BTN_DIRTY
if contains(self.area, pos): if contains(self.area, pos):
return BTN_CLICKED return BTN_CLICKED

View File

@ -16,17 +16,17 @@ class ConfirmDialog(Widget):
def __init__(self, content, confirm=DEFAULT_CONFIRM, cancel=DEFAULT_CANCEL): def __init__(self, content, confirm=DEFAULT_CONFIRM, cancel=DEFAULT_CANCEL):
self.content = content self.content = content
if cancel is not None: if cancel is not None:
self.confirm = Button((121, 240 - 48, 119, 48), confirm, self.confirm = Button(ui.grid(8, n_x=2), confirm,
normal_style=ui.BTN_CONFIRM, normal_style=ui.BTN_CONFIRM,
active_style=ui.BTN_CONFIRM_ACTIVE) active_style=ui.BTN_CONFIRM_ACTIVE)
self.cancel = Button((0, 240 - 48, 119, 48), cancel, self.cancel = Button(ui.grid(9, n_x=2), cancel,
normal_style=ui.BTN_CANCEL, normal_style=ui.BTN_CANCEL,
active_style=ui.BTN_CANCEL_ACTIVE) active_style=ui.BTN_CANCEL_ACTIVE)
else: else:
self.cancel = None self.confirm = Button(ui.grid(4, n_x=1), confirm,
self.confirm = Button((0, 240 - 48, 240, 48), confirm,
normal_style=ui.BTN_CONFIRM, normal_style=ui.BTN_CONFIRM,
active_style=ui.BTN_CONFIRM_ACTIVE) active_style=ui.BTN_CONFIRM_ACTIVE)
self.cancel = None
def render(self): def render(self):
self.confirm.render() self.confirm.render()
@ -52,7 +52,7 @@ class HoldToConfirmDialog(Widget):
def __init__(self, content, hold='Hold to confirm', *args, **kwargs): def __init__(self, content, hold='Hold to confirm', *args, **kwargs):
self.content = content self.content = content
self.button = Button((0, 240 - 48, 240, 48), hold, self.button = Button(ui.grid(4, n_x=1), hold,
normal_style=ui.BTN_CONFIRM, normal_style=ui.BTN_CONFIRM,
active_style=ui.BTN_CONFIRM_ACTIVE) active_style=ui.BTN_CONFIRM_ACTIVE)
self.loader = Loader(*args, **kwargs) self.loader = Loader(*args, **kwargs)
@ -66,6 +66,7 @@ class HoldToConfirmDialog(Widget):
button.touch(event, pos) button.touch(event, pos)
is_started = button.state & BTN_STARTED and button.state & BTN_ACTIVE is_started = button.state & BTN_STARTED and button.state & BTN_ACTIVE
if is_started and not was_started: if is_started and not was_started:
ui.display.clear()
self.loader.start() self.loader.start()
return _STARTED return _STARTED
if was_started and not is_started: if was_started and not is_started:

View File

@ -1,26 +1,18 @@
from trezor import ui, res, loop, io from trezor import ui, res, loop, io
from trezor.crypto import bip39 from trezor.crypto import bip39
from trezor.ui import display from trezor.ui import display
from trezor.ui.button import Button, BTN_CLICKED from trezor.ui.button import Button, BTN_CLICKED, ICON
def cell_area(i, n_x=3, n_y=3, start_x=0, start_y=40, end_x=240, end_y=240 - 48, spacing=0):
w = (end_x - start_x) // n_x
h = (end_y - start_y) // n_y
x = (i % n_x) * w
y = (i // n_x) * h
return (x + start_x, y + start_y, w - spacing, h - spacing)
def key_buttons(): def key_buttons():
keys = ['abc', 'def', 'ghi', 'jkl', 'mno', 'pqr', 'stu', 'vwx', 'yz'] keys = ['abc', 'def', 'ghi', 'jkl', 'mno', 'pqr', 'stu', 'vwx', 'yz']
# keys = [' ', 'abc', 'def', 'ghi', 'jkl', 'mno', 'pqrs', 'tuv', 'wxyz'] return [Button(ui.grid(i + 3, n_y=4), k,
return [Button(cell_area(i), k,
normal_style=ui.BTN_KEY, normal_style=ui.BTN_KEY,
active_style=ui.BTN_KEY_ACTIVE) for i, k in enumerate(keys)] active_style=ui.BTN_KEY_ACTIVE,
disabled_style=ui.BTN_KEY_DISABLED) for i, k in enumerate(keys)]
def compute_mask(text): def compute_mask(text: str) -> int:
mask = 0 mask = 0
for c in text: for c in text:
shift = ord(c) - 97 # ord('a') == 97 shift = ord(c) - 97 # ord('a') == 97
@ -30,199 +22,156 @@ def compute_mask(text):
return mask return mask
class KeyboardMultiTap(ui.Widget): class Input(Button):
def __init__(self, area: tuple, content: str='', word: str=''):
super().__init__(area, content)
self.word = word
self.icon = None
self.pending = False
def __init__(self, content=''): def edit(self, content: str, word: str, pending: bool):
self.content = content self.content = content
self.sugg_mask = 0xffffffff self.word = word
self.sugg_word = None self.pending = pending
self.pending_button = None self.taint()
self.pending_index = 0 if content == word: # confirm button
self.enable()
self.normal_style = ui.BTN_CONFIRM
self.active_style = ui.BTN_CONFIRM_ACTIVE
self.icon = ui.ICON_CONFIRM
elif word: # auto-complete button
self.enable()
self.normal_style = ui.BTN_KEY
self.active_style = ui.BTN_KEY_ACTIVE
self.icon = ui.ICON_CLICK
else: # disabled button
self.disable()
self.icon = None
self.key_buttons = key_buttons() def render_content(self, s, ax, ay, aw, ah):
self.sugg_button = Button((5, 5, 240 - 35, 30), '') text_style = s['text-style']
self.bs_button = Button((240 - 35, 5, 30, 30), fg_color = s['fg-color']
res.load('trezor/res/pin_close.toig'), bg_color = s['bg-color']
normal_style=ui.BTN_CLEAR,
active_style=ui.BTN_CLEAR_ACTIVE) p = self.pending # should we draw the pending marker?
t = self.content # input content
w = self.word[len(t):] # suggested word
i = self.icon # rendered icon
tx = ax + 24 # x-offset of the content
ty = ay + ah//2 + 8 # y-offset of the content
# input content and the suggested word
display.text(tx, ty, t, text_style, fg_color, bg_color)
width = display.text_width(t, text_style)
display.text(tx + width, ty, w, text_style, ui.GREY, 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)
if i: # icon
ix = ax + aw - ICON*2
iy = ty - ICON
display.icon(ix, iy, res.load(i), fg_color, bg_color)
class MnemonicKeyboard(ui.Widget):
def __init__(self, prompt: str=''):
self.prompt = prompt
self.input = Input(ui.grid(1, n_x=4, n_y=4, cells_x=3), '', '')
self.back = Button(ui.grid(0, n_x=4, n_y=4),
res.load(ui.ICON_BACK),
normal_style=ui.BTN_CLEAR,
active_style=ui.BTN_CLEAR_ACTIVE)
self.keys = key_buttons()
self.pbutton = None # pending key button
self.pindex = 0 # index of current pending char in pbutton
def render(self): def render(self):
if self.input.content:
# clear canvas under input line # content button and backspace
display.bar(0, 0, 205, 40, ui.BG) self.input.render()
self.back.render()
# input line
content_width = display.text_width(self.content, ui.BOLD)
display.text(20, 30, self.content, ui.BOLD, ui.FG, ui.BG)
# pending marker
if self.pending_button is not None:
pending_width = display.text_width(self.content[-1:], ui.BOLD)
pending_x = 20 + content_width - pending_width
display.bar(pending_x, 33, pending_width + 2, 3, ui.FG)
# auto-suggest
if self.sugg_word is not None:
sugg_rest = self.sugg_word[len(self.content):]
sugg_x = 20 + content_width
display.text(sugg_x, 30, sugg_rest, ui.BOLD, ui.GREY, ui.BG)
# render backspace button
if self.content:
self.bs_button.render()
else: else:
display.bar(240 - 48, 0, 48, 42, ui.BG) # prompt
display.bar(0, 8, 240, 60, ui.BG)
display.text(20, 40, self.prompt, ui.BOLD, ui.GREY, ui.BG)
# key buttons # key buttons
for btn in self.key_buttons: for btn in self.keys:
btn.render() btn.render()
def touch(self, event, pos): def touch(self, event, pos):
if self.bs_button.touch(event, pos) == BTN_CLICKED: content = self.input.content
self.content = self.content[:-1] word = self.input.word
self.pending_button = None
self.pending_index = 0 if self.back.touch(event, pos) == BTN_CLICKED:
self._update_suggestion() # backspace, delete the last character of input
self._update_buttons() self.edit(content[:-1])
return return
if self.sugg_button.touch(event, pos) == BTN_CLICKED and self.sugg_word is not None:
self.content = self.sugg_word if self.input.touch(event, pos) == BTN_CLICKED:
self.pending_button = None # input press, either auto-complete or confirm
self.pending_index = 0 if content == word:
self._update_suggestion() self.edit('')
self._update_buttons() return content
return else:
for btn in self.key_buttons: self.edit(word)
if btn.touch(event, pos) == BTN_CLICKED:
if self.pending_button is btn:
self.pending_index = (
self.pending_index + 1) % len(btn.content)
self.content = self.content[:-1]
self.content += btn.content[self.pending_index]
self._update_suggestion()
else:
self.content += btn.content[0]
self._update_suggestion()
self.pending_button = btn
self.pending_index = 0
return return
def _update_suggestion(self): for btn in self.keys:
if self.content: if btn.touch(event, pos) == BTN_CLICKED:
self.sugg_word = bip39.find_word(self.content) # key press, add new char to input or cycle the pending button
self.sugg_mask = bip39.complete_word(self.content) if self.pbutton is btn:
else: index = (self.pindex + 1) % len(btn.content)
self.sugg_word = None content = content[:-1] + btn.content[index]
self.sugg_mask = 0xffffffff else:
index = 0
content += btn.content[0]
self.edit(content, btn, index)
return
def _update_buttons(self): def edit(self, content, button=None, index=0):
for btn in self.key_buttons: word = bip39.find_word(content) or ''
if compute_mask(btn.content) & self.sugg_mask: mask = bip39.complete_word(content)
self.pbutton = button
self.pindex = index
self.input.edit(content, word, button is not None)
# enable or disable key buttons
for btn in self.keys:
if btn is button or compute_mask(btn.content) & mask:
btn.enable() btn.enable()
else: else:
btn.disable() btn.disable()
def __iter__(self): async def __iter__(self):
timeout = loop.sleep(1000 * 1000 * 1) timeout = loop.sleep(1000 * 1000 * 1)
touch = loop.select(io.TOUCH) touch = loop.select(io.TOUCH)
wait = loop.wait(touch, timeout) wait_timeout = loop.wait(touch, timeout)
while True: wait_touch = loop.wait(touch)
content = None
self.back.taint()
self.input.taint()
while content is None:
self.render() self.render()
result = yield wait if self.pbutton is not None:
if touch in wait.finished: wait = wait_timeout
event, *pos = result
self.touch(event, pos)
else: else:
self.pending_button = None wait = wait_touch
self.pending_index = 0 result = await wait
self._update_suggestion()
self._update_buttons()
def zoom_buttons(keys, upper=False):
n_x = len(keys)
if upper:
keys = keys + keys.upper()
n_y = 2
else:
n_y = 1
return [Button(cell_area(i, n_x, n_y), key) for i, key in enumerate(keys)]
class KeyboardZooming(ui.Widget):
def __init__(self, content='', uppercase=True):
self.content = content
self.uppercase = uppercase
self.zoom_buttons = None
self.key_buttons = key_buttons()
self.bs_button = Button((240 - 35, 5, 30, 30),
res.load('trezor/res/pin_close.toig'),
normal_style=ui.BTN_CLEAR,
active_style=ui.BTN_CLEAR_ACTIVE)
def render(self):
self.render_input()
if self.zoom_buttons:
for btn in self.zoom_buttons:
btn.render()
else:
for btn in self.key_buttons:
btn.render()
def render_input(self):
if self.content:
display.bar(0, 0, 200, 40, ui.BG)
else:
display.bar(0, 0, 240, 40, ui.BG)
display.text(20, 30, self.content, ui.BOLD, ui.GREY, ui.BG)
if self.content:
self.bs_button.render()
def touch(self, event, pos):
if self.bs_button.touch(event, pos) == BTN_CLICKED:
self.content = self.content[:-1]
self.bs_button.taint()
return
if self.zoom_buttons:
return self.touch_zoom(event, pos)
else:
return self.touch_keyboard(event, pos)
def touch_zoom(self, event, pos):
for btn in self.zoom_buttons:
if btn.touch(event, pos) == BTN_CLICKED:
self.content += btn.content
self.zoom_buttons = None
for b in self.key_buttons:
b.taint()
self.bs_button.taint()
break
def touch_keyboard(self, event, pos):
for btn in self.key_buttons:
if btn.touch(event, pos) == BTN_CLICKED:
self.zoom_buttons = zoom_buttons(btn.content, self.uppercase)
for b in self.zoom_buttons:
b.taint()
self.bs_button.taint()
break
def __iter__(self):
timeout = loop.sleep(1000 * 1000 * 1)
touch = loop.select(io.TOUCH)
wait = loop.wait(touch, timeout)
while True:
self.render()
result = yield wait
if touch in wait.finished: if touch in wait.finished:
event, *pos = result event, *pos = result
self.touch(event, pos) content = self.touch(event, pos)
elif self.zoom_buttons: else:
self.zoom_buttons = None if self.input.word:
for btn in self.key_buttons: # just reset the pending state
btn.taint() self.edit(self.input.content)
else:
# invalid character, backspace it
Keyboard = KeyboardMultiTap self.edit(self.input.content[:-1])
return content

View File

@ -37,13 +37,13 @@ class Loader(ui.Widget):
s = self.normal_style s = self.normal_style
if s['icon'] is None: if s['icon'] is None:
ui.display.loader( ui.display.loader(
progress, -8, s['fg-color'], s['bg-color']) progress, -24, s['fg-color'], s['bg-color'])
elif s['icon-fg-color'] is None: elif s['icon-fg-color'] is None:
ui.display.loader( ui.display.loader(
progress, -8, s['fg-color'], s['bg-color'], res.load(s['icon'])) progress, -24, s['fg-color'], s['bg-color'], res.load(s['icon']))
else: else:
ui.display.loader( ui.display.loader(
progress, -8, s['fg-color'], s['bg-color'], res.load(s['icon']), s['icon-fg-color']) progress, -24, s['fg-color'], s['bg-color'], res.load(s['icon']), s['icon-fg-color'])
def __iter__(self): def __iter__(self):
sleep = loop.sleep(1000000 // 60) # 60 fps sleep = loop.sleep(1000000 // 60) # 60 fps

View File

@ -6,14 +6,9 @@ from trezor.ui.button import Button, BTN_CLICKED
def digit_area(i): def digit_area(i):
width = const(80)
height = const(48)
if i == 9: # 0-position if i == 9: # 0-position
i = 10 # display it in the middle i = 10 # display it in the middle
x = (i % 3) * width return ui.grid(i + 3) # skip the first line
y = (i // 3) * height
# 48px is offset of input line, -1px is the border size
return (x, y + 48, width - 1, height - 1)
def generate_digits(with_zero): def generate_digits(with_zero):
@ -36,19 +31,18 @@ class PinMatrix(ui.Widget):
# we lay out the buttons top-left to bottom-right, but the order of the # we lay out the buttons top-left to bottom-right, but the order of the
# digits is defined as bottom-left to top-right (on numpad) # digits is defined as bottom-left to top-right (on numpad)
reordered_digits = self.digits[6:] + self.digits[3:6] + self.digits[:3] reordered_digits = self.digits[6:] + self.digits[3:6] + self.digits[:3]
self.pin_buttons = [Button(digit_area(i), str(d)) self.pin_buttons = [Button(digit_area(i), str(d))
for i, d in enumerate(reordered_digits)] for i, d in enumerate(reordered_digits)]
self.onchange = None self.onchange = None
def render(self): def render(self):
header = '*' * len(self.pin) if self.pin else self.label
# clear canvas under input line # clear canvas under input line
display.bar(0, 0, 205, 48, ui.BG) display.bar(0, 0, 205, 48, ui.BG)
# input line with a header # input line with a header
display.text_center(120, 30, header, ui.NORMAL, ui.blend(ui.BG, ui.FG, 0.5), ui.BG) header = '*' * len(self.pin) if self.pin else self.label
display.text_center(120, 36, header, ui.BOLD, ui.GREY, ui.BG)
# pin matrix buttons # pin matrix buttons
for btn in self.pin_buttons: for btn in self.pin_buttons:

View File

@ -12,9 +12,7 @@ BACKLIGHT_NONE = const(2)
BACKLIGHT_MAX = const(255) BACKLIGHT_MAX = const(255)
# color palette # color palette
LIGHT_RED = rgb(0xFF, 0x00, 0x00) RED = rgb(0xFF, 0x00, 0x00)
RED = rgb(0xE4, 0x57, 0x2E) # RED E4572E
ACTIVE_RED = rgb(0xA6, 0x40, 0x22) # ACTIVE DARK RED A64022
PINK = rgb(0xE9, 0x1E, 0x63) PINK = rgb(0xE9, 0x1E, 0x63)
PURPLE = rgb(0x9C, 0x27, 0xB0) PURPLE = rgb(0x9C, 0x27, 0xB0)
DEEP_PURPLE = rgb(0x67, 0x3A, 0xB7) DEEP_PURPLE = rgb(0x67, 0x3A, 0xB7)
@ -23,8 +21,7 @@ BLUE = rgb(0x21, 0x96, 0xF3)
LIGHT_BLUE = rgb(0x03, 0xA9, 0xF4) LIGHT_BLUE = rgb(0x03, 0xA9, 0xF4)
CYAN = rgb(0x00, 0xBC, 0xD4) CYAN = rgb(0x00, 0xBC, 0xD4)
TEAL = rgb(0x00, 0x96, 0x88) TEAL = rgb(0x00, 0x96, 0x88)
GREEN = rgb(0x4C, 0xC1, 0x48) # GREEN 4CC148 GREEN = rgb(0x00, 0xAE, 0x0B)
ACTIVE_GREEN = rgb(0x1A, 0x8C, 0x14) # ACTIVE DARK GREEN 1A8C14
LIGHT_GREEN = rgb(0x87, 0xCE, 0x26) LIGHT_GREEN = rgb(0x87, 0xCE, 0x26)
LIME = rgb(0xCD, 0xDC, 0x39) LIME = rgb(0xCD, 0xDC, 0x39)
YELLOW = rgb(0xFF, 0xEB, 0x3B) YELLOW = rgb(0xFF, 0xEB, 0x3B)
@ -38,7 +35,10 @@ DARK_GREY = rgb(0x3E, 0x3E, 0x3E)
BLUE_GRAY = rgb(0x60, 0x7D, 0x8B) BLUE_GRAY = rgb(0x60, 0x7D, 0x8B)
BLACK = rgb(0x00, 0x00, 0x00) BLACK = rgb(0x00, 0x00, 0x00)
WHITE = rgb(0xFA, 0xFA, 0xFA) WHITE = rgb(0xFA, 0xFA, 0xFA)
BLACKISH = rgb(0x20, 0x20, 0x20) BLACKISH = rgb(0x30, 0x30, 0x30)
TITLE_GREY = rgb(0x9B, 0x9B, 0x9B)
ORANGE_ICON = rgb(0xF5, 0xA6, 0x23)
# common color styles # common color styles
BG = BLACK BG = BLACK
@ -47,11 +47,14 @@ FG = WHITE
# icons # icons
ICON_RESET = 'trezor/res/header_icons/reset.toig' ICON_RESET = 'trezor/res/header_icons/reset.toig'
ICON_WIPE = 'trezor/res/header_icons/wipe.toig' ICON_WIPE = 'trezor/res/header_icons/wipe.toig'
ICON_RECOVERY = 'trezor/res/header_icons/recovery.toig' ICON_RECOVERY = 'trezor/res/recovery.toig'
ICON_CLEAR = 'trezor/res/clear.toig' ICON_CLEAR = 'trezor/res/cross2.toig'
ICON_CONFIRM = 'trezor/res/confirm.toig' ICON_CONFIRM = 'trezor/res/confirm2.toig'
ICON_CONFIRM2 = 'trezor/res/confirm.toig'
ICON_LOCK = 'trezor/res/lock.toig' ICON_LOCK = 'trezor/res/lock.toig'
ICON_SEND = 'trezor/res/send.toig' ICON_SEND = 'trezor/res/send.toig'
ICON_CLICK = 'trezor/res/click.toig'
ICON_BACK = 'trezor/res/left.toig'
# buttons # buttons
BTN_DEFAULT = { BTN_DEFAULT = {
@ -76,24 +79,24 @@ BTN_DEFAULT_DISABLED = {
'radius': RADIUS, 'radius': RADIUS,
} }
BTN_CANCEL = { BTN_CANCEL = {
'bg-color': LIGHT_RED, 'bg-color': RED,
'fg-color': FG, 'fg-color': FG,
'text-style': BOLD, 'text-style': BOLD,
'border-color': LIGHT_RED, 'border-color': BG,
'radius': RADIUS, 'radius': RADIUS,
} }
BTN_CANCEL_ACTIVE = { BTN_CANCEL_ACTIVE = {
'bg-color': FG, 'bg-color': FG,
'fg-color': LIGHT_RED, 'fg-color': RED,
'text-style': BOLD, 'text-style': BOLD,
'border-color': LIGHT_RED, 'border-color': FG,
'radius': RADIUS, 'radius': RADIUS,
} }
BTN_CONFIRM = { BTN_CONFIRM = {
'bg-color': GREEN, 'bg-color': GREEN,
'fg-color': FG, 'fg-color': FG,
'text-style': BOLD, 'text-style': BOLD,
'border-color': GREEN, 'border-color': BG,
'radius': RADIUS, 'radius': RADIUS,
} }
BTN_CONFIRM_ACTIVE = { BTN_CONFIRM_ACTIVE = {
@ -104,7 +107,7 @@ BTN_CONFIRM_ACTIVE = {
'radius': RADIUS, 'radius': RADIUS,
} }
BTN_CLEAR = { BTN_CLEAR = {
'bg-color': BG, 'bg-color': ORANGE,
'fg-color': FG, 'fg-color': FG,
'text-style': NORMAL, 'text-style': NORMAL,
'border-color': BG, 'border-color': BG,
@ -122,18 +125,27 @@ BTN_KEY = {
'fg-color': FG, 'fg-color': FG,
'text-style': MONO, 'text-style': MONO,
'border-color': BG, 'border-color': BG,
'radius': RADIUS,
} }
BTN_KEY_ACTIVE = { BTN_KEY_ACTIVE = {
'bg-color': GREY, 'bg-color': FG,
'fg-color': BG, 'fg-color': BLACKISH,
'text-style': MONO, 'text-style': MONO,
'border-color': GREY, 'border-color': FG,
'radius': RADIUS,
}
BTN_KEY_DISABLED = {
'bg-color': BG,
'fg-color': GREY,
'text-style': MONO,
'border-color': BG,
'radius': RADIUS,
} }
# loader # loader
LDR_DEFAULT = { LDR_DEFAULT = {
'bg-color': BG, 'bg-color': BG,
'fg-color': FG, 'fg-color': GREEN,
'icon': None, 'icon': None,
'icon-fg-color': None, 'icon-fg-color': None,
} }

View File

@ -1,16 +1,17 @@
from micropython import const from micropython import const
from trezor import ui from trezor import ui
TEXT_HEADER_HEIGHT = const(32) TEXT_HEADER_HEIGHT = const(48)
TEXT_LINE_HEIGHT = const(23) TEXT_LINE_HEIGHT = const(26)
TEXT_MARGIN_LEFT = const(10) TEXT_MARGIN_LEFT = const(14)
class Text(ui.Widget): class Text(ui.Widget):
def __init__(self, header_text, header_icon, *content): def __init__(self, header_text, header_icon, *content, icon_color=ui.ORANGE_ICON):
self.header_text = header_text self.header_text = header_text
self.header_icon = header_icon self.header_icon = header_icon
self.icon_color = icon_color
self.content = content self.content = content
def render(self): def render(self):
@ -19,7 +20,7 @@ class Text(ui.Widget):
style = ui.NORMAL style = ui.NORMAL
fg = ui.FG fg = ui.FG
bg = ui.BG bg = ui.BG
ui.header(self.header_text, self.header_icon, ui.GREEN, ui.BG) ui.header(self.header_text, self.header_icon, ui.TITLE_GREY, ui.BG, self.icon_color)
for item in self.content: for item in self.content:
if isinstance(item, str): if isinstance(item, str):

View File

@ -0,0 +1,47 @@
from micropython import const
from trezor import loop
from trezor import ui
from trezor.ui import Widget
from trezor.ui.button import Button, BTN_CLICKED
_W12 = const(12)
_W15 = const(15)
_W18 = const(18)
_W24 = const(24)
class WordSelector(Widget):
def __init__(self, content):
self.content = content
self.w12 = Button(ui.grid(8, n_y=4, n_x=4, cells_x=2), str(_W12),
normal_style=ui.BTN_KEY,
active_style=ui.BTN_KEY_ACTIVE)
self.w15 = Button(ui.grid(10, n_y=4, n_x=4, cells_x=2), str(_W15),
normal_style=ui.BTN_KEY,
active_style=ui.BTN_KEY_ACTIVE)
self.w18 = Button(ui.grid(12, n_y=4, n_x=4, cells_x=2), str(_W18),
normal_style=ui.BTN_KEY,
active_style=ui.BTN_KEY_ACTIVE)
self.w24 = Button(ui.grid(14, n_y=4, n_x=4, cells_x=2), str(_W24),
normal_style=ui.BTN_KEY,
active_style=ui.BTN_KEY_ACTIVE)
def render(self):
self.w12.render()
self.w15.render()
self.w18.render()
self.w24.render()
def touch(self, event, pos):
if self.w12.touch(event, pos) == BTN_CLICKED:
return _W12
if self.w15.touch(event, pos) == BTN_CLICKED:
return _W15
if self.w18.touch(event, pos) == BTN_CLICKED:
return _W18
if self.w24.touch(event, pos) == BTN_CLICKED:
return _W24
async def __iter__(self):
return await loop.wait(super().__iter__(), self.content)