185 lines
5.7 KiB
Python
185 lines
5.7 KiB
Python
# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
|
#
|
|
# UX and interactions related to Settings > Pin Options
|
|
#
|
|
# - altho this can pass on the simulator, it's much more interesting to test with "bare metal"
|
|
# - need this to recover:
|
|
# from main import pa; pa.change(is_duress=1, new_pin=b'', old_pin=b'77-77')
|
|
# - same tests are needed in three modes: normal login, secondary PIN used for login, and
|
|
# duress PIN used for login.
|
|
# - my convention for known PINS:
|
|
# 12-12 main wallet
|
|
# 23-23 secondary wallet (mk1/2)
|
|
# 33-33 main duress wallet
|
|
# 66-66 brickme
|
|
#
|
|
import pytest, time
|
|
from charcodes import KEY_RIGHT, KEY_ENTER
|
|
|
|
|
|
CLR_PIN = '999999-999999'
|
|
|
|
@pytest.fixture(scope='session')
|
|
def under_duress(request):
|
|
# add flag: --duress to commandline to indicate this mode
|
|
return request.config.getoption('duress')
|
|
|
|
|
|
@pytest.fixture
|
|
def verify_pin_set(sim_exec):
|
|
def doit(pin, secondary=False, duress=False, brickme=False):
|
|
# check the SE holds the indicated PIN code
|
|
kws = ''
|
|
if secondary:
|
|
kws += ", is_secondary=1"
|
|
if duress: # not used
|
|
kws += ", is_duress=1"
|
|
if brickme:
|
|
kws += ", is_brickme=1"
|
|
rv = sim_exec(f'from pincodes import pa; RV.write(repr(pa.change(new_pin=b"{pin}", old_pin=b"{pin}" {kws})))')
|
|
if rv != 'None':
|
|
raise RuntimeError(rv)
|
|
|
|
return doit
|
|
|
|
|
|
@pytest.fixture
|
|
def goto_pin_options(pick_menu_item, goto_home):
|
|
def doit():
|
|
goto_home()
|
|
pick_menu_item('Settings')
|
|
pick_menu_item('Login Settings')
|
|
|
|
return doit
|
|
|
|
@pytest.fixture
|
|
def my_enter_pin(cap_screen, need_keypress, is_q1, press_right, press_select):
|
|
def doit(pin):
|
|
time.sleep(.01) # required?
|
|
scr = cap_screen().split('\n')
|
|
title = scr[1]
|
|
|
|
if is_q1:
|
|
assert scr[2] == 'Enter first part of PIN'
|
|
prefix, suffix = pin.split("-")
|
|
for n in prefix:
|
|
need_keypress(n)
|
|
time.sleep(.1)
|
|
|
|
# move second part
|
|
press_right()
|
|
time.sleep(.1)
|
|
scr = cap_screen().split('\n')
|
|
|
|
assert scr[2] == 'Enter second part of PIN'
|
|
words = scr[-3].split(" ") # split on 2 spaces
|
|
assert len(words) == 2
|
|
|
|
for n in suffix:
|
|
need_keypress(n)
|
|
time.sleep(.1)
|
|
|
|
press_select()
|
|
|
|
else:
|
|
assert scr[2] == 'Enter PIN Prefix'
|
|
for ch in pin:
|
|
if ch != '-':
|
|
time.sleep(.05) # required?
|
|
need_keypress(ch)
|
|
time.sleep(.05) # required?
|
|
continue
|
|
|
|
if ch == '-':
|
|
press_select()
|
|
time.sleep(.1) # required
|
|
|
|
scr = cap_screen().split('\n')
|
|
|
|
assert ('Recognize these?' in scr) or ('Write these down:' in scr)
|
|
words = scr[2:4]
|
|
press_select()
|
|
|
|
time.sleep(.1) # required
|
|
scr = cap_screen().split('\n')
|
|
assert scr[-1] == 'Enter rest of PIN'
|
|
|
|
press_select()
|
|
|
|
time.sleep(0.1)
|
|
return title, words
|
|
|
|
return doit
|
|
|
|
|
|
@pytest.fixture
|
|
def change_pin(cap_screen, cap_story, cap_menu, press_select, my_enter_pin, press_cancel):
|
|
def doit(old_pin, new_pin, hdr_text, expect_fail=None):
|
|
# use standard menus and UX to change a PIN
|
|
title, story = cap_story()
|
|
assert title == hdr_text
|
|
assert "changing the main PIN used to unlock your Coldcard" in story
|
|
assert "ABSOLUTELY NO WAY TO RECOVER A FORGOTTEN PIN!" in story
|
|
assert "Write it down" in story
|
|
press_select()
|
|
time.sleep(0.01) # required
|
|
|
|
assert max(len(i) for i in new_pin.split('-')) <= 6
|
|
assert 2 <= min(len(i) for i in new_pin.split('-'))
|
|
|
|
# give old pin, if there was one
|
|
if old_pin != None:
|
|
title, words = my_enter_pin(old_pin)
|
|
assert title == 'Old '+hdr_text
|
|
|
|
title, words2 = my_enter_pin(new_pin)
|
|
if old_pin == None and title == 'Old '+hdr_text:
|
|
raise ValueError("PIN was set, but we though it wouldnt be")
|
|
assert title == 'New '+hdr_text
|
|
|
|
# confirm, if not clearing the PIN
|
|
if new_pin != CLR_PIN:
|
|
title, words3 = my_enter_pin(new_pin)
|
|
assert title == 'New '+hdr_text
|
|
assert words2 == words3
|
|
|
|
if expect_fail:
|
|
title, story = cap_story()
|
|
assert title == 'Try Again'
|
|
assert expect_fail in story
|
|
press_cancel()
|
|
return
|
|
|
|
# saving/verifying can take tens of seconds.
|
|
time.sleep(5)
|
|
for retries in range(10):
|
|
try:
|
|
if 'Test Login Now' in cap_menu():
|
|
break
|
|
except:
|
|
# USB not ready when busy in bootloader code
|
|
pass
|
|
time.sleep(1)
|
|
else:
|
|
raise pytest.fail("Menu didn't come back")
|
|
|
|
return words2
|
|
|
|
return doit
|
|
|
|
@pytest.mark.parametrize('new_pin', ['77-77', '123456-654321', '79-654321', '123456-12'])
|
|
def test_main_pin(goto_pin_options, pick_menu_item, cap_story, cap_screen,
|
|
change_pin, new_pin, verify_pin_set, under_duress):
|
|
goto_pin_options()
|
|
pick_menu_item("Change Main PIN")
|
|
DEF_PIN = '12-12' if not under_duress else '33-33'
|
|
|
|
change_pin(DEF_PIN, new_pin, 'Main PIN')
|
|
verify_pin_set(new_pin)
|
|
|
|
pick_menu_item('Change Main PIN')
|
|
change_pin(new_pin, DEF_PIN, 'Main PIN')
|
|
verify_pin_set(DEF_PIN)
|
|
|
|
# EOF
|