login UX for Q1
This commit is contained in:
parent
0493aee1ae
commit
eb77c7cbde
@ -236,7 +236,9 @@ class Display:
|
||||
self.dis.fill_rect(x,y, w,h, 0x0000)
|
||||
|
||||
def show(self):
|
||||
#self.dis.show()
|
||||
# used to be critical, pushing internal screen representation to device
|
||||
# however, we can be more selective/auto now...
|
||||
# - but might still use... idk
|
||||
pass
|
||||
|
||||
# rather than clearing and redrawing, use this buffer w/ fixed parts of screen
|
||||
|
||||
151
shared/login.py
151
shared/login.py
@ -5,12 +5,11 @@
|
||||
import pincodes, version, random
|
||||
from glob import dis
|
||||
from display import FontLarge, FontTiny
|
||||
from ux import PressRelease, ux_wait_keyup, ux_show_story
|
||||
from utils import pretty_delay
|
||||
from ux import PressRelease, ux_wait_keyup, ux_show_story, ux_show_pin
|
||||
from callgate import show_logout
|
||||
from pincodes import pa
|
||||
from uasyncio import sleep_ms
|
||||
from charcodes import KEY_DELETE, KEY_SELECT, KEY_CANCEL
|
||||
from charcodes import KEY_DELETE, KEY_SELECT, KEY_CANCEL, KEY_CLEAR
|
||||
|
||||
MAX_PIN_PART_LEN = 6
|
||||
MIN_PIN_PART_LEN = 2
|
||||
@ -41,104 +40,41 @@ class LoginUX:
|
||||
self.words_ok = False
|
||||
self.footer = None
|
||||
|
||||
def show_pin_randomized(self, force_draw):
|
||||
# screen redraw, when we are "randomized"
|
||||
|
||||
if force_draw:
|
||||
dis.clear()
|
||||
|
||||
# prompt
|
||||
dis.text(5+3, 2, "ENTER PIN")
|
||||
dis.text(5+6, 17, ('1st part' if not self.pin_prefix else '2nd part'))
|
||||
|
||||
# remapped keypad
|
||||
y = 2
|
||||
x = 89
|
||||
h = 16
|
||||
for i in range(0, 10, 3):
|
||||
if i == 9:
|
||||
dis.text(x, y, ' %s' % self.randomize[0])
|
||||
else:
|
||||
dis.text(x, y, ' '.join(self.randomize[1+i:1+i+3]))
|
||||
y += h
|
||||
else:
|
||||
# just clear what we need to: the PIN area
|
||||
dis.clear_rect(0, 40, 88, 20)
|
||||
|
||||
# placeholder text
|
||||
msg = '[' + ('*'*len(self.pin)) + ']'
|
||||
x = 40 - ((10*len(msg))//2)
|
||||
dis.text(x, 40, msg, FontLarge)
|
||||
|
||||
dis.show()
|
||||
|
||||
def show_pin(self, force_draw=False):
|
||||
if self.randomize:
|
||||
return self.show_pin_randomized(force_draw)
|
||||
|
||||
filled = len(self.pin)
|
||||
y = 27
|
||||
|
||||
if force_draw:
|
||||
dis.clear()
|
||||
|
||||
if not self.pin_prefix:
|
||||
prompt="Enter PIN Prefix"
|
||||
else:
|
||||
prompt="Enter rest of PIN"
|
||||
|
||||
|
||||
if self.subtitle:
|
||||
dis.text(None, 0, self.subtitle)
|
||||
dis.text(None, 16, prompt, FontTiny)
|
||||
else:
|
||||
dis.text(None, 4, prompt)
|
||||
|
||||
if self.footer:
|
||||
footer = self.footer
|
||||
elif self.is_repeat:
|
||||
footer = "CONFIRM PIN VALUE"
|
||||
elif not self.pin_prefix:
|
||||
footer = "X to CANCEL, or OK when DONE"
|
||||
else:
|
||||
footer = "X to CANCEL, or OK to CONTINUE"
|
||||
|
||||
dis.text(None, -1, footer, FontTiny)
|
||||
|
||||
else:
|
||||
# just clear what we need to: the PIN area
|
||||
dis.clear_rect(0, y, 128, 21)
|
||||
|
||||
w = 18
|
||||
|
||||
# extra (empty) box after
|
||||
if not filled:
|
||||
dis.icon(64-(w//2), y, 'box')
|
||||
else:
|
||||
x = 64 - ((w*filled)//2)
|
||||
# filled boxes
|
||||
for idx in range(filled):
|
||||
dis.icon(x, y, 'xbox')
|
||||
x += w
|
||||
|
||||
dis.show()
|
||||
# redraw screen with prompting
|
||||
ux_show_pin(dis, self.pin, self.subtitle, not self.pin_prefix, self.is_repeat,
|
||||
force_draw, footer=self.footer, randomize=self.randomize)
|
||||
|
||||
def _show_words(self):
|
||||
# Show the anti-phising words, but coordinate w/ the large delay from the SE.
|
||||
|
||||
# - show prompt w/o any words first
|
||||
dis.clear()
|
||||
dis.text(None, 0, "Recognize these?" if (not self.is_setting) or self.is_repeat \
|
||||
else "Write these down:")
|
||||
prompt = "Recognize these?" if (not self.is_setting) or self.is_repeat \
|
||||
else "Write these down:"
|
||||
dis.text(None, 2 if dis.has_lcd else 0, prompt)
|
||||
|
||||
dis.show()
|
||||
|
||||
# - show as busy for 1-2 seconds
|
||||
dis.busy_bar(True)
|
||||
words = pincodes.PinAttempt.prefix_words(self.pin.encode())
|
||||
|
||||
y = 15
|
||||
x = 18
|
||||
dis.text(x, y, words[0], FontLarge)
|
||||
dis.text(x, y+18, words[1], FontLarge)
|
||||
|
||||
dis.text(None, -1, "X to CANCEL, or OK to CONTINUE", FontTiny)
|
||||
# - show rest of screen and CTA
|
||||
if dis.has_lcd:
|
||||
# Q1
|
||||
x = 12
|
||||
y = 4
|
||||
dis.text(x, y, words[0])
|
||||
dis.text(x, y+1, words[1])
|
||||
dis.text(None, -1, "CANCEL or SELECT to continue")
|
||||
else:
|
||||
# Old style
|
||||
y = 15
|
||||
x = 18
|
||||
dis.text(x, y, words[0], FontLarge)
|
||||
dis.text(x, y+18, words[1], FontLarge)
|
||||
dis.text(None, -1, "X to CANCEL, or OK to CONTINUE", FontTiny)
|
||||
|
||||
dis.busy_bar(False) # includes a dis.show()
|
||||
#dis.show()
|
||||
@ -146,7 +82,6 @@ class LoginUX:
|
||||
def cancel(self):
|
||||
self.reset()
|
||||
self.show_pin(True)
|
||||
|
||||
|
||||
async def interact(self):
|
||||
# Prompt for prefix and pin. Returns string or None if the abort.
|
||||
@ -162,6 +97,9 @@ class LoginUX:
|
||||
if self.pin:
|
||||
self.pin = self.pin[:-1]
|
||||
self.show_pin()
|
||||
elif ch == KEY_CLEAR:
|
||||
self.pin = ''
|
||||
self.show_pin()
|
||||
elif ch == KEY_CANCEL:
|
||||
if not self.pin and self.pin_prefix:
|
||||
# cancel on empty 2nd-stage: start over
|
||||
@ -202,13 +140,13 @@ class LoginUX:
|
||||
callgate.fast_wipe(False)
|
||||
# not reached
|
||||
|
||||
if nxt == 'y' or nxt == KEY_SELECT:
|
||||
if nxt == KEY_SELECT:
|
||||
self.pin_prefix = self.pin
|
||||
self.pin = ''
|
||||
|
||||
if self.randomize:
|
||||
self.shuffle_keys()
|
||||
elif nxt == 'x' or nxt == KEY_CANCEL:
|
||||
elif nxt == KEY_CANCEL:
|
||||
self.reset()
|
||||
|
||||
self.show_pin(True)
|
||||
@ -225,23 +163,9 @@ class LoginUX:
|
||||
|
||||
self.show_pin()
|
||||
else:
|
||||
# XXX other key on Q1? Ignore for now, but we will need to support
|
||||
# passwords in future.
|
||||
# other key on Q1? Ignore
|
||||
pass
|
||||
|
||||
async def do_delay(self):
|
||||
# show # of failures and implement the delay, which could be
|
||||
# very long.
|
||||
dis.clear()
|
||||
dis.text(None, 0, "Checking...", FontLarge)
|
||||
dis.text(None, 24, 'Wait '+pretty_delay(pa.delay_required * pa.seconds_per_tick))
|
||||
dis.text(None, 40, "(%d failures)" % pa.num_fails)
|
||||
|
||||
while pa.is_delay_needed():
|
||||
dis.progress_bar_show(pa.delay_achieved / pa.delay_required)
|
||||
|
||||
pa.delay()
|
||||
|
||||
async def we_are_ewaste(self, num_fails):
|
||||
msg = '''After %d failed PIN attempts this Coldcard is locked forever. \
|
||||
By design, there is no way to reset or recover the secure element, and its contents \
|
||||
@ -278,9 +202,7 @@ Press OK to continue, X to stop for now.
|
||||
self.reset()
|
||||
|
||||
if pa.num_fails:
|
||||
self.footer = '%d failures' % pa.num_fails
|
||||
if version.has_608:
|
||||
self.footer += ', %d tries left' % pa.attempts_left
|
||||
self.footer = '%d failures, %d tries left' % (pa.num_fails, pa.attempts_left)
|
||||
|
||||
pin = await self.interact()
|
||||
|
||||
@ -291,13 +213,10 @@ Press OK to continue, X to stop for now.
|
||||
dis.fullscreen("Wait...")
|
||||
pa.setup(pin)
|
||||
|
||||
if version.has_608 and pa.num_fails > 3:
|
||||
if pa.num_fails > 3:
|
||||
# they are approaching brickage, so warn them each attempt
|
||||
await self.confirm_attempt(pa.attempts_left, pa.num_fails, pin)
|
||||
dis.fullscreen("Wait...")
|
||||
elif pa.is_delay_needed():
|
||||
# mark 1/2 might come here, never mark3
|
||||
await self.do_delay()
|
||||
|
||||
# do the actual login attempt now
|
||||
try:
|
||||
|
||||
21
shared/ux.py
21
shared/ux.py
@ -6,24 +6,23 @@ from uasyncio import sleep_ms
|
||||
from queues import QueueEmpty
|
||||
import utime, gc, version
|
||||
from utils import word_wrap
|
||||
from charcodes import (KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME,
|
||||
from charcodes import (KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME, KEY_NFC, KEY_QR,
|
||||
KEY_END, KEY_PAGE_UP, KEY_PAGE_DOWN, KEY_SELECT, KEY_CANCEL)
|
||||
|
||||
DEFAULT_IDLE_TIMEOUT = const(4*3600) # (seconds) 4 hours
|
||||
|
||||
# see ux_mk or ux_q1 for these...
|
||||
|
||||
# How many characters can we fit on each line? How many lines?
|
||||
# See ux_mk or ux_q1 for some display functions now
|
||||
if version.has_qwerty:
|
||||
from lcd_display import CHARS_W, CHARS_H
|
||||
CH_PER_W = CHARS_W
|
||||
STORY_H = CHARS_H
|
||||
from ux_q1 import PressRelease, ux_enter_number, ux_input_numbers, ux_input_text
|
||||
from ux_q1 import PressRelease, ux_enter_number, ux_input_numbers, ux_input_text, ux_show_pin
|
||||
else:
|
||||
# How many characters can we fit on each line? How many lines?
|
||||
# (using FontSmall)
|
||||
CH_PER_W = 17
|
||||
STORY_H = 5
|
||||
from ux_mk4 import PressRelease, ux_enter_number, ux_input_numbers, ux_input_text
|
||||
from ux_mk4 import PressRelease, ux_enter_number, ux_input_numbers, ux_input_text, ux_show_pin
|
||||
|
||||
# This signals the need to switch from current
|
||||
# menu (or whatever) to show something new. The
|
||||
@ -137,8 +136,7 @@ async def ux_show_story(msg, title=None, escape=None, sensitive=False, strict_es
|
||||
# - returns character used to get out (X or Y)
|
||||
# - can accept other chars to 'escape' as well.
|
||||
# - accepts a stream or string
|
||||
from glob import dis, numpad
|
||||
from display import FontLarge
|
||||
from glob import dis
|
||||
|
||||
lines = []
|
||||
if title:
|
||||
@ -164,9 +162,9 @@ async def ux_show_story(msg, title=None, escape=None, sensitive=False, strict_es
|
||||
del msg
|
||||
gc.collect()
|
||||
else:
|
||||
# simple string
|
||||
# simple string being shown
|
||||
if version.has_qwerty:
|
||||
msg = msg.replace(' X ', ' CANCEL ').replace('OK', 'SELECT')
|
||||
msg = msg.replace('\nX ', 'CANCEL ').replace(' X ', ' CANCEL ').replace('OK', 'SELECT')
|
||||
|
||||
for ln in msg.split('\n'):
|
||||
if len(ln) > CH_PER_W:
|
||||
@ -214,6 +212,9 @@ async def ux_show_story(msg, title=None, escape=None, sensitive=False, strict_es
|
||||
top = max(0, top-1)
|
||||
elif ch == '8' or ch == KEY_DOWN:
|
||||
top = min(len(lines)-2, top+1)
|
||||
elif not strict_escape:
|
||||
if ch in { KEY_NFC, KEY_QR }:
|
||||
return ch
|
||||
|
||||
|
||||
|
||||
|
||||
@ -336,4 +336,92 @@ Add more characters by moving past end (right side).'''
|
||||
help_msg += '\nTo quit without changes, delete everything.'
|
||||
await ux_show_story(help_msg)
|
||||
|
||||
|
||||
def ux_show_pin(dis, pin, subtitle, is_first_part, is_confirmation, force_draw,
|
||||
footer=None, randomize=None):
|
||||
|
||||
# Draw PIN (placeholder)
|
||||
from display import FontTiny, FontLarge
|
||||
|
||||
if randomize:
|
||||
# screen redraw, when we are "randomized"
|
||||
# - only used at login, none of the other cases
|
||||
|
||||
if force_draw:
|
||||
dis.clear()
|
||||
|
||||
# prompt
|
||||
dis.text(5+3, 2, "ENTER PIN")
|
||||
dis.text(5+6, 17, ('1st part' if is_first_part else '2nd part'))
|
||||
|
||||
# remapped keypad
|
||||
y = 2
|
||||
x = 89
|
||||
h = 16
|
||||
for i in range(0, 10, 3):
|
||||
if i == 9:
|
||||
dis.text(x, y, ' %s' % randomize[0])
|
||||
else:
|
||||
dis.text(x, y, ' '.join(randomize[1+i:1+i+3]))
|
||||
y += h
|
||||
else:
|
||||
# just clear what we need to: the PIN area
|
||||
dis.clear_rect(0, 40, 88, 20)
|
||||
|
||||
# placeholder text
|
||||
msg = '[' + ('*'*len(self.pin)) + ']'
|
||||
x = 40 - ((10*len(msg))//2)
|
||||
dis.text(x, 40, msg, FontLarge)
|
||||
|
||||
dis.show()
|
||||
|
||||
return
|
||||
|
||||
filled = len(pin)
|
||||
y = 27
|
||||
|
||||
if force_draw:
|
||||
dis.clear()
|
||||
|
||||
if is_first_part:
|
||||
prompt="Enter PIN Prefix"
|
||||
else:
|
||||
prompt="Enter rest of PIN"
|
||||
|
||||
|
||||
if subtitle:
|
||||
dis.text(None, 0, subtitle)
|
||||
dis.text(None, 16, prompt, FontTiny)
|
||||
else:
|
||||
dis.text(None, 4, prompt)
|
||||
|
||||
if footer:
|
||||
pass
|
||||
elif is_confirmation:
|
||||
footer = "CONFIRM PIN VALUE"
|
||||
elif is_confirmation:
|
||||
footer = "X to CANCEL, or OK when DONE"
|
||||
else:
|
||||
footer = "X to CANCEL, or OK to CONTINUE"
|
||||
|
||||
dis.text(None, -1, footer, FontTiny)
|
||||
|
||||
else:
|
||||
# just clear what we need to: the PIN area
|
||||
dis.clear_rect(0, y, 128, 21)
|
||||
|
||||
w = 18
|
||||
|
||||
# extra (empty) box after
|
||||
if not filled:
|
||||
dis.icon(64-(w//2), y, 'box')
|
||||
else:
|
||||
x = 64 - ((w*filled)//2)
|
||||
# filled boxes
|
||||
for idx in range(filled):
|
||||
dis.icon(x, y, 'xbox')
|
||||
x += w
|
||||
|
||||
dis.show()
|
||||
|
||||
# EOF
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
from uasyncio import sleep_ms
|
||||
import utime, gc
|
||||
from charcodes import *
|
||||
from lcd_display import CHARS_W
|
||||
|
||||
CURSOR = '█ '
|
||||
|
||||
@ -93,6 +94,9 @@ async def ux_enter_number(prompt, max_value, can_cancel=False):
|
||||
elif ch == KEY_DELETE:
|
||||
if value:
|
||||
value = value[0:-1]
|
||||
elif ch == KEY_CLEAR:
|
||||
value = ''
|
||||
dis.text(0, 4, ' '*CHARS_W)
|
||||
elif ch == KEY_CANCEL:
|
||||
if can_cancel:
|
||||
# quit if they press X on empty screen
|
||||
@ -116,9 +120,9 @@ async def ux_input_text(value, confirm_exit=True, hex_only=False, max_len=100):
|
||||
# - Should allow full unicode, NKDN
|
||||
# - but our font is mostly just ascii
|
||||
# - no control chars allowed either
|
||||
# - TODO: editing, completion, etc
|
||||
# - TODO: editing, line wrap, completion, etc
|
||||
# - TODO: press QR -> do scan and use that text
|
||||
from glob import dis
|
||||
from lcd_display import CHARS_W
|
||||
from ux import ux_show_story
|
||||
|
||||
dis.clear()
|
||||
@ -143,6 +147,9 @@ async def ux_input_text(value, confirm_exit=True, hex_only=False, max_len=100):
|
||||
if len(value) > 0:
|
||||
# delete current char
|
||||
value = value[:-1]
|
||||
elif ch == KEY_CLEAR:
|
||||
value = ''
|
||||
dis.text(0, 1, ' '*CHARS_W)
|
||||
elif ch == KEY_CANCEL:
|
||||
if confirm_exit:
|
||||
pp = await ux_show_story(
|
||||
@ -152,4 +159,63 @@ async def ux_input_text(value, confirm_exit=True, hex_only=False, max_len=100):
|
||||
elif ' ' <= ch < chr(127):
|
||||
value += ch
|
||||
|
||||
def ux_show_pin(dis, pin, subtitle, is_first_part, is_confirmation, force_draw,
|
||||
footer=None, randomize=None):
|
||||
|
||||
# Draw PIN during entry / reentry / changing or setting
|
||||
#MAX_PIN_PART_LEN = 6
|
||||
|
||||
# extra (empty) box after
|
||||
ln = len(pin)
|
||||
FILLED = '◉'
|
||||
EMPTY = '◯' #, '◌'
|
||||
msg = ''.join(FILLED if n < ln else EMPTY for n in range(6))
|
||||
y = 1 if randomize else 2
|
||||
|
||||
if force_draw:
|
||||
dis.clear()
|
||||
|
||||
if randomize and force_draw:
|
||||
# screen redraw, when we are "randomized"
|
||||
# - only used at login, none of the other cases
|
||||
# - test w/ "simulator.py --q1 -g --eff --set rngk=1"
|
||||
|
||||
# remapped numbers along bottom
|
||||
x = 3
|
||||
dis.text(x-1, -4, ' 1 2 3 4 5 6 7 8 9 0 ', invert=1)
|
||||
dis.text(x , -3, ' '.join(randomize[1:]) + ' ' + randomize[0])
|
||||
|
||||
if force_draw:
|
||||
|
||||
if is_first_part:
|
||||
prompt="Enter PIN prefix"
|
||||
else:
|
||||
prompt="Enter second part of PIN"
|
||||
|
||||
|
||||
if subtitle:
|
||||
# "New Main PIN" ... so not really a SUB title.
|
||||
dis.text(None, 0, subtitle)
|
||||
dis.text(None, y, prompt)
|
||||
else:
|
||||
dis.text(None, y, prompt)
|
||||
|
||||
if footer:
|
||||
# ie. '1 failures, 12 tries left'
|
||||
dis.text(None, -2, footer)
|
||||
|
||||
if is_confirmation:
|
||||
cta = "Confirm pin value"
|
||||
if is_confirmation:
|
||||
cta = "CANCEL or SELECT when done"
|
||||
else:
|
||||
cta = "CANCEL or SELECT to continue"
|
||||
|
||||
dis.text(None, -1, cta)
|
||||
|
||||
# auto-center broken w/ double-wides
|
||||
dis.text(10, y+2, msg)
|
||||
dis.show()
|
||||
|
||||
|
||||
# EOF
|
||||
|
||||
Loading…
Reference in New Issue
Block a user