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

206 lines
6.5 KiB

from trezor import io, loop, res, ui
from trezor.crypto import slip39
from trezor.ui import display
from trezor.ui.button import Button, ButtonClear, ButtonMono, ButtonMonoConfirm
def check_mask(mask: int, index: int) -> bool:
return bool((1 << (index - 1)) & mask)
# TODO: ask UX if we want to finish sooner than 4 words
# example: 'hairy'
def _is_final(content: str) -> bool:
return len(content) > 3
class KeyButton(Button):
def __init__(self, area, content, keyboard, index):
self.keyboard = keyboard
self.index = index
super().__init__(area, content)
def on_click(self):
self.keyboard.on_key_click(self)
class InputButton(Button):
def __init__(self, area, content, word):
super().__init__(area, content)
self.word = word
self.pending_button = None
self.pending_index = None
self.icon = None # rendered icon
self.disable()
def edit(self, content, word, pending_button, pending_index):
self.word = word
self.content = content
self.pending_button = pending_button
self.pending_index = pending_index
self.repaint = True
if word:
self.enable()
self.normal_style = ButtonMonoConfirm.normal
self.active_style = ButtonMonoConfirm.active
self.icon = ui.ICON_CONFIRM
else: # disabled button
self.disabled_style = ButtonMono.normal
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
tx = ax + 24 # x-offset of the content
ty = ay + ah // 2 + 8 # y-offset of the content
if not _is_final(self.content):
to_display = len(self.content) * "*"
if self.pending_button:
to_display = (
to_display[:-1] + self.pending_button.content[self.pending_index]
)
else:
to_display = self.word
display.text(tx, ty, to_display, text_style, fg_color, bg_color)
if self.pending_button and not _is_final(self.content):
width = display.text_width(to_display, text_style)
pw = display.text_width(self.content[-1:], text_style)
px = tx + width - pw
display.bar(px, ty + 2, pw + 1, 3, fg_color)
if self.icon:
ix = ax + aw - 16 * 2
iy = ty - 16
display.icon(ix, iy, res.load(self.icon), fg_color, bg_color)
class Prompt(ui.Control):
def __init__(self, prompt):
self.prompt = prompt
self.repaint = True
def on_render(self):
if self.repaint:
display.bar(0, 8, ui.WIDTH, 60, ui.BG)
display.text(20, 40, self.prompt, ui.BOLD, ui.GREY, ui.BG)
self.repaint = False
class Slip39Keyboard(ui.Layout):
def __init__(self, prompt):
self.prompt = Prompt(prompt)
icon_back = res.load(ui.ICON_BACK)
self.back = Button(ui.grid(0, n_x=4, n_y=4), icon_back, ButtonClear)
self.back.on_click = self.on_back_click
self.input = InputButton(ui.grid(1, n_x=4, n_y=4, cells_x=3), "", "")
self.input.on_click = self.on_input_click
self.keys = [
KeyButton(ui.grid(i + 3, n_y=4), k, self, i + 1)
for i, k in enumerate(
("ab", "cd", "ef", "ghij", "klm", "nopq", "rs", "tuv", "wxyz")
)
]
self.pending_button = None
self.pending_index = 0
self.button_sequence = ""
def dispatch(self, event: int, x: int, y: int):
for btn in self.keys:
btn.dispatch(event, x, y)
if self.input.content:
self.input.dispatch(event, x, y)
self.back.dispatch(event, x, y)
else:
self.prompt.dispatch(event, x, y)
def on_back_click(self):
# Backspace was clicked, let's delete the last character of input.
self.button_sequence = self.button_sequence[:-1]
self.edit()
def on_input_click(self):
# Input button was clicked. If the content matches the suggested word,
# let's confirm it, otherwise just auto-complete.
result = self.input.word
if _is_final(self.input.content):
self.button_sequence = ""
self.edit()
self.on_confirm(result)
def on_key_click(self, btn: KeyButton):
# Key button was clicked. If this button is pending, let's cycle the
# pending character in input. If not, let's just append the first
# character.
if self.pending_button is btn:
index = (self.pending_index + 1) % len(btn.content)
else:
index = 0
self.button_sequence += str(btn.index)
self.edit(btn, index)
def on_timeout(self):
# Timeout occurred. Let's redraw to draw asterisks.
self.edit()
def on_confirm(self, word):
# Word was confirmed by the user.
raise ui.Result(word)
def edit(self, button: KeyButton = None, index: int = 0):
self.pending_button = button
self.pending_index = index
# find the completions
mask = 0
word = ""
if _is_final(self.button_sequence):
word = slip39.button_sequence_to_word(self.button_sequence)
else:
mask = slip39.compute_mask(self.button_sequence)
# modify the input state
self.input.edit(
self.button_sequence, word, self.pending_button, self.pending_index
)
# enable or disable key buttons
for btn in self.keys:
if (not _is_final(self.button_sequence) and btn is button) or check_mask(
mask, btn.index
):
btn.enable()
else:
btn.disable()
# invalidate the prompt if we display it next frame
if not self.input.content:
self.prompt.repaint = True
async def handle_input(self):
touch = loop.wait(io.TOUCH)
timeout = loop.sleep(1000 * 1000 * 1)
spawn_touch = loop.spawn(touch)
spawn_timeout = loop.spawn(touch, timeout)
while True:
if self.pending_button is not None:
spawn = spawn_timeout
else:
spawn = spawn_touch
result = await spawn
if touch in spawn.finished:
event, x, y = result
self.dispatch(event, x, y)
else:
self.on_timeout()