283 lines
9.1 KiB
Python
283 lines
9.1 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, os
|
|
from helpers import xfp2str
|
|
|
|
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 get_duress_secret(sim_exec):
|
|
def doit(pin):
|
|
# read the duress secret
|
|
rv = sim_exec(f'from pincodes import pa; RV.write(repr(pa.fetch(duress_pin=b"{pin}")))')
|
|
if rv.startswith('Traceback'):
|
|
raise RuntimeError(rv)
|
|
assert rv[0:2] == "b'"
|
|
return eval(rv)
|
|
return doit
|
|
|
|
@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 get_secondary_login(sim_exec):
|
|
def doit():
|
|
return sim_exec('from pincodes import pa; RV.write(repr(pa.is_secondary))') == 'True'
|
|
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):
|
|
def doit(pin):
|
|
time.sleep(.01) # required?
|
|
scr = cap_screen().split('\n')
|
|
title = scr[1]
|
|
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 == '-':
|
|
need_keypress('y')
|
|
time.sleep(.1) # required
|
|
|
|
scr = cap_screen().split('\n')
|
|
assert ('Recognize these?' in scr) or ('Write these down:' in scr)
|
|
words = scr[2:4]
|
|
need_keypress('y')
|
|
|
|
time.sleep(.1) # required
|
|
scr = cap_screen().split('\n')
|
|
assert scr[-1] == 'Enter rest of PIN'
|
|
|
|
need_keypress('y')
|
|
time.sleep(0.1)
|
|
|
|
return title, words
|
|
|
|
|
|
return doit
|
|
|
|
@pytest.fixture
|
|
def change_pin(cap_screen, cap_story, cap_menu, need_keypress, my_enter_pin):
|
|
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 ('We strongly recommend' in story) or (CLR_PIN in story)
|
|
need_keypress('y')
|
|
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
|
|
need_keypress('x')
|
|
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, need_keypress, change_pin, new_pin, verify_pin_set, get_secondary_login, under_duress):
|
|
goto_pin_options()
|
|
|
|
try:
|
|
pick_menu_item('Change Main PIN')
|
|
except KeyError:
|
|
# secondary login, for example
|
|
assert get_secondary_login()
|
|
raise pytest.skip('cant change main from secondary')
|
|
|
|
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)
|
|
|
|
@pytest.mark.parametrize('new_pin', ['77-77', '123456-654321', '79-654321', '123456-12'])
|
|
def test_duress_pin(goto_pin_options, pick_menu_item, cap_story, cap_screen, need_keypress, change_pin, new_pin, get_duress_secret, under_duress, only_mk3):
|
|
goto_pin_options()
|
|
|
|
|
|
pick_menu_item('Duress PIN')
|
|
try:
|
|
change_pin(None, new_pin, 'Duress PIN')
|
|
except ValueError:
|
|
if under_duress:
|
|
raise pytest.xfail("cant change duress while under duress")
|
|
else:
|
|
raise
|
|
|
|
# duress secret should be complex
|
|
d_secret = get_duress_secret(new_pin)
|
|
assert len(set(d_secret)) > 20
|
|
assert d_secret[0] == 0x01, "not xprv?"
|
|
|
|
# changing PIN shouldn't change secret
|
|
pick_menu_item('Duress PIN')
|
|
change_pin(new_pin, '123-123', 'Duress PIN')
|
|
rb = get_duress_secret('123-123')
|
|
assert rb == d_secret
|
|
|
|
pick_menu_item('Duress PIN')
|
|
change_pin('123-123', CLR_PIN, 'Duress PIN')
|
|
|
|
# clearing PIN should clear secret
|
|
zz = get_duress_secret('')
|
|
assert zz == b'\0'*72
|
|
|
|
@pytest.mark.parametrize('new_pin', ['77-77', '123456-654321', '79-654321', '123456-12'])
|
|
def test_secondary_pin(is_mark3, goto_pin_options, pick_menu_item, cap_story, cap_screen, need_keypress, change_pin, new_pin, verify_pin_set, get_secondary_login, only_mk3):
|
|
|
|
if get_secondary_login():
|
|
raise pytest.skip('not intended for use under secondary login')
|
|
|
|
goto_pin_options()
|
|
|
|
if is_mark3:
|
|
raise pytest.skip('mark3 doesnt support secondary wallet')
|
|
|
|
pick_menu_item('Second Wallet')
|
|
try:
|
|
change_pin(None, new_pin, 'Second PIN')
|
|
except ValueError:
|
|
if under_duress:
|
|
raise pytest.xfail("cant change secondary while under duress")
|
|
else:
|
|
raise
|
|
verify_pin_set(new_pin, secondary=1)
|
|
|
|
pick_menu_item('Second Wallet')
|
|
change_pin(new_pin, CLR_PIN, 'Second PIN')
|
|
verify_pin_set('', secondary=1)
|
|
|
|
@pytest.mark.parametrize('new_pin', ['77-77', '123456-654321', '79-654321', '123456-12'])
|
|
def test_secondary_from_secondary_pin(is_mark3, goto_pin_options, pick_menu_item, cap_story, cap_screen, need_keypress, change_pin, new_pin, verify_pin_set, get_secondary_login, only_mk3):
|
|
# XXX Obsolete now?
|
|
|
|
# when logged into secondary wallet, you can't clear PIN, and we use a 23-23 as value
|
|
if not get_secondary_login():
|
|
raise pytest.skip('intended for use under secondary login')
|
|
if is_mark3:
|
|
raise pytest.skip('mark3 doesnt support secondary wallet')
|
|
|
|
goto_pin_options()
|
|
|
|
ASSUME_PIN = '23-23'
|
|
|
|
pick_menu_item('Second Wallet')
|
|
change_pin(ASSUME_PIN, new_pin, 'Second PIN')
|
|
verify_pin_set(new_pin)
|
|
|
|
pick_menu_item('Second Wallet')
|
|
change_pin(new_pin, ASSUME_PIN, 'Second PIN')
|
|
verify_pin_set(ASSUME_PIN)
|
|
|
|
@pytest.mark.parametrize('new_pin', ['77-77', '123456-654321', '79-654321', '123456-12'])
|
|
def test_brickme_pin(goto_pin_options, pick_menu_item, cap_story, cap_screen, need_keypress, change_pin, new_pin, verify_pin_set, get_secondary_login, under_duress, only_mk3):
|
|
|
|
goto_pin_options()
|
|
|
|
try:
|
|
pick_menu_item('Brick Me PIN')
|
|
except KeyError:
|
|
# secondary login, for example
|
|
assert get_secondary_login()
|
|
raise pytest.skip('cant do brickme in this mode')
|
|
|
|
try:
|
|
change_pin(None, new_pin, 'Brickme PIN')
|
|
except ValueError:
|
|
if under_duress:
|
|
raise pytest.xfail("cant change brickme while under duress")
|
|
else:
|
|
raise
|
|
|
|
verify_pin_set(new_pin, brickme=1)
|
|
|
|
pick_menu_item('Brick Me PIN')
|
|
change_pin(new_pin, CLR_PIN, 'Brickme PIN')
|
|
verify_pin_set('', brickme=1)
|
|
|
|
# EOF
|