Merge pull request #93 from trezor/recovery
Seed recovery with on-screen keyboard
BIN
assets/5390-200.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.1 KiB |
BIN
assets/left.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
assets/recovery-old.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.3 KiB |
BIN
assets/send-old.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/send.png
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:
|
||||
|
30
src/apps/common/request_words.py
Normal 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
|
@ -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
|
||||
|
||||
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 layout_recovery_device(ctx, msg):
|
||||
async def request_mnemonic(count: int, prompt: str) -> str:
|
||||
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()
|
||||
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
|
||||
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')
|
||||
|
||||
|
BIN
src/trezor/res/click.toig
Normal file
BIN
src/trezor/res/confirm2.toig
Normal file
BIN
src/trezor/res/cross2.toig
Normal file
BIN
src/trezor/res/left.toig
Normal file
BIN
src/trezor/res/recovery.toig
Normal file
BIN
src/trezor/res/send2.toig
Normal file
@ -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,17 +28,19 @@ 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):
|
||||
if self.state & BTN_DISABLED:
|
||||
self.state &= ~BTN_DISABLED
|
||||
self.state |= BTN_DIRTY
|
||||
|
||||
def disable(self):
|
||||
if not self.state & BTN_DISABLED:
|
||||
self.state |= BTN_DISABLED | BTN_DIRTY
|
||||
|
||||
def taint(self):
|
||||
@ -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'])
|
||||
|
||||
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
|
||||
|
||||
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):
|
||||
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.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
|
||||
|
||||
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'),
|
||||
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()
|
||||
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
|
||||
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()
|
||||
|
||||
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.key_buttons:
|
||||
|
||||
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:
|
||||
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()
|
||||
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)
|
||||
content = 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
|
||||
if self.input.word:
|
||||
# just reset the pending state
|
||||
self.edit(self.input.content)
|
||||
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:
|
||||
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
|
||||
# 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': GREY,
|
||||
'border-color': FG,
|
||||
'radius': RADIUS,
|
||||
}
|
||||
BTN_KEY_DISABLED = {
|
||||
'bg-color': BG,
|
||||
'fg-color': GREY,
|
||||
'text-style': MONO,
|
||||
'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):
|
||||
|
47
src/trezor/ui/word_select.py
Normal 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)
|