login UX for Q1

This commit is contained in:
Peter D. Gray 2023-06-02 11:22:33 -04:00 committed by scgbckbone
parent 0493aee1ae
commit eb77c7cbde
5 changed files with 205 additions and 129 deletions

View File

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

View File

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

View File

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

View File

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

View File

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