Merge pull request #93 from trezor/recovery

Seed recovery with on-screen keyboard
pull/25/head
Jan Pochyla 7 years ago committed by GitHub
commit 1357ba7202
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

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

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

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

@ -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_resp = "PONGPONG";
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 r;

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

@ -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

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

@ -1,23 +1,55 @@
from trezor import ui, wire
from trezor.utils import unimport
def nth(n):
if 4 <= n % 100 <= 20:
sfx = 'th'
else:
sfx = {1: 'st', 2: 'nd', 3: 'rd'}.get(n % 10, 'th')
return str(n) + sfx
async def recovery_device(ctx, msg):
'''
Recover BIP39 seed into empty device.
1. Ask for the number of words in recovered seed.
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
@unimport
async def layout_recovery_device(ctx, msg):
if storage.is_initialized():
raise wire.FailureError(UnexpectedMessage, 'Already initialized')
msg = 'Please enter ' + nth(msg.word_count) + ' word'
wordcount = await request_words(ctx,
Text('Device recovery', ui.ICON_RECOVERY,
'Number of words?'))
mnemonic = await request_mnemonic(wordcount, 'Type %s. word')
ui.display.clear()
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)
if msg.enforce_wordlist and not bip39.check(mnemonic):
raise wire.FailureError(ProcessError, 'Mnemonic is not valid')
# TODO
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()
async def request_mnemonic(count: int, prompt: str) -> str:
from trezor.ui.keyboard import MnemonicKeyboard
words = []
board = MnemonicKeyboard()
for i in range(0, count):
board.prompt = prompt % (i + 1)
word = await board
words.append(word)
return ' '.join(words)

@ -11,6 +11,7 @@ if __debug__:
@unimport
async def layout_reset_device(ctx, msg):
from trezor import config
from trezor.ui.text import Text
from trezor.crypto import hashlib, random, bip39
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.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 import storage
@ -42,9 +43,11 @@ async def layout_reset_device(ctx, msg):
await require_confirm(ctx, entropy_content, ButtonRequestType.ResetDevice)
if msg.pin_protection:
pin = await request_pin_twice(ctx)
curpin = ''
newpin = await request_pin_confirm(ctx)
else:
pin = None
curpin = ''
newpin = ''
external_entropy_ack = await ctx.call(EntropyRequest(), EntropyAck)
ehash = hashlib.sha256()
@ -55,11 +58,11 @@ async def layout_reset_device(ctx, msg):
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_settings(pin=pin,
passphrase_protection=msg.passphrase_protection,
language=msg.language,
label=msg.label)
return Success(message='Initialized')

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -106,11 +106,36 @@ def layout(f):
return inner
def header(title: str, icon: bytes=ICON_RESET, fg: int=BG, bg: int=BG):
display.bar(0, 0, 240, 32, bg)
def header(title: str,
icon: bytes=ICON_RESET,
fg: int=BG,
bg: int=BG,
ifg: int=BG):
if icon is not None:
display.icon(8, 4, res.load(icon), fg, bg)
display.text(8 + 24 + 2, 24, title, BOLD, fg, bg)
display.icon(14, 18, res.load(icon), ifg, 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:

@ -15,6 +15,9 @@ BTN_ACTIVE = const(2)
BTN_DIRTY = const(4)
BTN_DISABLED = const(8)
ICON = const(16) # icon size in pixels
BORDER = const(4) # border size in pixels
class Button(Widget):
@ -25,18 +28,20 @@ class Button(Widget):
absolute=False):
self.area = area
self.content = content
self.normal_style = normal_style or ui.BTN_DEFAULT
self.active_style = active_style or ui.BTN_DEFAULT_ACTIVE
self.disabled_style = disabled_style or ui.BTN_DEFAULT_DISABLED
self.normal_style = normal_style or ui.BTN_KEY
self.active_style = active_style or ui.BTN_KEY_ACTIVE
self.disabled_style = disabled_style or ui.BTN_KEY_DISABLED
self.absolute = absolute
self.state = BTN_DIRTY
def enable(self):
self.state &= ~BTN_DISABLED
self.state |= BTN_DIRTY
if self.state & BTN_DISABLED:
self.state &= ~BTN_DISABLED
self.state |= BTN_DIRTY
def disable(self):
self.state |= BTN_DISABLED | BTN_DIRTY
if not self.state & BTN_DISABLED:
self.state |= BTN_DISABLED | BTN_DIRTY
def taint(self):
self.state |= BTN_DIRTY
@ -52,46 +57,66 @@ class Button(Widget):
else:
s = self.normal_style
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'])
self.render_background(s, ax, ay, aw, ah)
self.render_content(s, ax, ay, aw, ah)
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:
display.icon(tx - 15, ty - 20, self.content,
s['fg-color'],
s['bg-color'])
# render only the background
display.bar_radius(ax, ay,
aw, ah,
bg_color,
ui.BG,
radius)
self.state = state
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):
if self.state & BTN_DISABLED:
state = self.state
if state & BTN_DISABLED:
return
if not self.absolute:
pos = rotate(pos)
if event == io.TOUCH_START:
if contains(self.area, pos):
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 not self.state & BTN_ACTIVE:
if not state & BTN_ACTIVE:
self.state = BTN_STARTED | BTN_DIRTY | BTN_ACTIVE
else:
if self.state & BTN_ACTIVE:
if state & BTN_ACTIVE:
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
if contains(self.area, pos):
return BTN_CLICKED

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

@ -1,26 +1,18 @@
from trezor import ui, res, loop, io
from trezor.crypto import bip39
from trezor.ui import display
from trezor.ui.button import Button, BTN_CLICKED
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)
from trezor.ui.button import Button, BTN_CLICKED, ICON
def key_buttons():
keys = ['abc', 'def', 'ghi', 'jkl', 'mno', 'pqr', 'stu', 'vwx', 'yz']
# keys = [' ', 'abc', 'def', 'ghi', 'jkl', 'mno', 'pqrs', 'tuv', 'wxyz']
return [Button(cell_area(i), k,
return [Button(ui.grid(i + 3, n_y=4), k,
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
for c in text:
shift = ord(c) - 97 # ord('a') == 97
@ -30,199 +22,156 @@ def compute_mask(text):
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.sugg_mask = 0xffffffff
self.sugg_word = None
self.pending_button = None
self.pending_index = 0
self.key_buttons = key_buttons()
self.sugg_button = Button((5, 5, 240 - 35, 30), '')
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)
self.word = word
self.pending = pending
self.taint()
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
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
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):
# clear canvas under input line
display.bar(0, 0, 205, 40, ui.BG)
# 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()
if self.input.content:
# content button and backspace
self.input.render()
self.back.render()
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
for btn in self.key_buttons:
for btn in self.keys:
btn.render()
def touch(self, event, pos):
if self.bs_button.touch(event, pos) == BTN_CLICKED:
self.content = self.content[:-1]
self.pending_button = None
self.pending_index = 0
self._update_suggestion()
self._update_buttons()
return
if self.sugg_button.touch(event, pos) == BTN_CLICKED and self.sugg_word is not None:
self.content = self.sugg_word
self.pending_button = None
self.pending_index = 0
self._update_suggestion()
self._update_buttons()
content = self.input.content
word = self.input.word
if self.back.touch(event, pos) == BTN_CLICKED:
# backspace, delete the last character of input
self.edit(content[:-1])
return
for btn in self.key_buttons:
if self.input.touch(event, pos) == BTN_CLICKED:
# input press, either auto-complete or confirm
if content == word:
self.edit('')
return content
else:
self.edit(word)
return
for btn in self.keys:
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()
# 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:
self.content += btn.content[0]
self._update_suggestion()
self.pending_button = btn
self.pending_index = 0
index = 0
content += btn.content[0]
self.edit(content, btn, index)
return
def _update_suggestion(self):
if self.content:
self.sugg_word = bip39.find_word(self.content)
self.sugg_mask = bip39.complete_word(self.content)
else:
self.sugg_word = None
self.sugg_mask = 0xffffffff
def edit(self, content, button=None, index=0):
word = bip39.find_word(content) or ''
mask = bip39.complete_word(content)
def _update_buttons(self):
for btn in self.key_buttons:
if compute_mask(btn.content) & self.sugg_mask:
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()
else:
btn.disable()
def __iter__(self):
async 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:
event, *pos = result
self.touch(event, pos)
else:
self.pending_button = None
self.pending_index = 0
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)]
wait_timeout = loop.wait(touch, timeout)
wait_touch = loop.wait(touch)
content = None
class KeyboardZooming(ui.Widget):
self.back.taint()
self.input.taint()
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:
while content is None:
self.render()
result = yield wait
if self.pbutton is not None:
wait = wait_timeout
else:
wait = wait_touch
result = await wait
if touch in wait.finished:
event, *pos = result
self.touch(event, pos)
elif self.zoom_buttons:
self.zoom_buttons = None
for btn in self.key_buttons:
btn.taint()
Keyboard = KeyboardMultiTap
content = self.touch(event, pos)
else:
if self.input.word:
# just reset the pending state
self.edit(self.input.content)
else:
# invalid character, backspace it
self.edit(self.input.content[:-1])
return content

@ -37,13 +37,13 @@ class Loader(ui.Widget):
s = self.normal_style
if s['icon'] is None:
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:
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:
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):
sleep = loop.sleep(1000000 // 60) # 60 fps

@ -6,14 +6,9 @@ from trezor.ui.button import Button, BTN_CLICKED
def digit_area(i):
width = const(80)
height = const(48)
if i == 9: # 0-position
i = 10 # display it in the middle
x = (i % 3) * width
y = (i // 3) * height
# 48px is offset of input line, -1px is the border size
return (x, y + 48, width - 1, height - 1)
return ui.grid(i + 3) # skip the first line
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
# digits is defined as bottom-left to top-right (on numpad)
reordered_digits = self.digits[6:] + self.digits[3:6] + self.digits[:3]
self.pin_buttons = [Button(digit_area(i), str(d))
for i, d in enumerate(reordered_digits)]
self.onchange = None
def render(self):
header = '*' * len(self.pin) if self.pin else self.label
# clear canvas under input line
display.bar(0, 0, 205, 48, ui.BG)
# 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
for btn in self.pin_buttons:

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

@ -1,16 +1,17 @@
from micropython import const
from trezor import ui
TEXT_HEADER_HEIGHT = const(32)
TEXT_LINE_HEIGHT = const(23)
TEXT_MARGIN_LEFT = const(10)
TEXT_HEADER_HEIGHT = const(48)
TEXT_LINE_HEIGHT = const(26)
TEXT_MARGIN_LEFT = const(14)
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_icon = header_icon
self.icon_color = icon_color
self.content = content
def render(self):
@ -19,7 +20,7 @@ class Text(ui.Widget):
style = ui.NORMAL
fg = ui.FG
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:
if isinstance(item, str):

@ -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)
Loading…
Cancel
Save