bugfix: Delta Mode Trick PIN restore from backup
This commit is contained in:
parent
02bd428786
commit
be614dab92
@ -4,7 +4,7 @@ This lists the new changes that have not yet been published in a normal release.
|
||||
|
||||
# Shared Improvements - Both Mk and Q
|
||||
|
||||
- tbd
|
||||
- Bugfix: Delta Mode Trick PIN was never restored from backup
|
||||
|
||||
# Mk Specific Changes
|
||||
|
||||
|
||||
@ -211,14 +211,14 @@ class TrickPinMgmt:
|
||||
def update_slot(self, pin, new=False, new_pin=None, tc_flags=None, tc_arg=None, secret=None):
|
||||
# create or update a trick pin
|
||||
# - doesn't support wallet to no-wallet transitions
|
||||
'''
|
||||
>>> from pincodes import pa; pa.setup(b'12-12'); pa.login(); from trick_pins import *
|
||||
'''
|
||||
#
|
||||
# from pincodes import pa; pa.setup(b'12-12'); pa.login(); from trick_pins import *
|
||||
#
|
||||
assert isinstance(pin, bytes)
|
||||
|
||||
b, slot = self.get_by_pin(pin)
|
||||
if not slot:
|
||||
if not new: raise KeyError("wrong pin")
|
||||
assert new, "wrong pin"
|
||||
|
||||
# Making a new entry
|
||||
b, slot = make_slot()
|
||||
@ -398,17 +398,17 @@ class TrickPinMgmt:
|
||||
continue
|
||||
|
||||
if flags & TC_DELTA_MODE:
|
||||
prob = validate_delta_pin(true_pin, pin)
|
||||
prob, _ = validate_delta_pin(true_pin, pin)
|
||||
if prob:
|
||||
# just forget it, no UI here to report issue
|
||||
continue
|
||||
continue
|
||||
|
||||
try:
|
||||
# might need to construct a BIP-85 or XPRV secret to match
|
||||
path, new_secret = construct_duress_secret(flags, arg)
|
||||
|
||||
b, slot = tp.update_slot(pin.encode(), new=True,
|
||||
tc_flags=flags, tc_arg=arg, secret=new_secret)
|
||||
tp.update_slot(pin.encode(), new=True, secret=new_secret,
|
||||
tc_flags=flags, tc_arg=arg)
|
||||
except: pass
|
||||
|
||||
@staticmethod
|
||||
|
||||
@ -2,10 +2,12 @@
|
||||
#
|
||||
import pytest, time, pdb, itertools
|
||||
from charcodes import KEY_ENTER
|
||||
from core_fixtures import _pick_menu_item, _cap_story, _press_select
|
||||
from core_fixtures import _need_keypress, _cap_menu, _sim_exec
|
||||
from core_fixtures import _pick_menu_item, _cap_story, _press_select, _word_menu_entry
|
||||
from core_fixtures import _need_keypress, _cap_menu, _sim_exec, _pass_word_quiz
|
||||
from run_sim_tests import ColdcardSimulator, clean_sim_data
|
||||
from ckcc_protocol.cli import wait_and_download
|
||||
from ckcc_protocol.client import ColdcardDevice
|
||||
from ckcc_protocol.protocol import CCProtocolPacker
|
||||
|
||||
|
||||
def _clone(source, target):
|
||||
@ -123,4 +125,99 @@ def test_clone(source, target):
|
||||
_clone(source, target)
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def test_backup_restore_delta_pin():
|
||||
# SOURCE
|
||||
# clone with multisig wallet
|
||||
clean_sim_data() # remove all from previous
|
||||
sim_source = ColdcardSimulator(args=["--ms", "--p2wsh", "--set", "nfc=1", "--set", "vidsk=1"],
|
||||
segregate=True) # in /tmp/cc-simulators
|
||||
sim_source.start(start_wait=6)
|
||||
device_source = ColdcardDevice(is_simulator=True, sn=sim_source.socket)
|
||||
_pick_menu_item(device_source, False, "Settings")
|
||||
time.sleep(.1)
|
||||
_pick_menu_item(device_source, False, "Login Settings")
|
||||
time.sleep(.1)
|
||||
_pick_menu_item(device_source, False, "Trick PINs")
|
||||
time.sleep(.1)
|
||||
_pick_menu_item(device_source, False, "Add New Trick")
|
||||
time.sleep(.1)
|
||||
|
||||
# twice, first select, then verify
|
||||
for _ in range(2):
|
||||
pin = "11-11"
|
||||
pre, suff = pin.split("-")
|
||||
for ch in pre:
|
||||
_need_keypress(device_source, ch)
|
||||
time.sleep(.1)
|
||||
_press_select(device_source, False)
|
||||
|
||||
time.sleep(.2)
|
||||
|
||||
for ch in suff:
|
||||
_need_keypress(device_source, ch)
|
||||
time.sleep(.1)
|
||||
_press_select(device_source, False)
|
||||
|
||||
time.sleep(.2)
|
||||
_pick_menu_item(device_source, False, "Delta Mode")
|
||||
time.sleep(.1)
|
||||
title, story = _cap_story(device_source)
|
||||
assert "trick PIN must be same length as true PIN and differ only in final 4 positions" in story
|
||||
_press_select(device_source, False)
|
||||
time.sleep(.1)
|
||||
_press_select(device_source, False)
|
||||
time.sleep(.1)
|
||||
m = _cap_menu(device_source)
|
||||
assert "11-11" in m[1]
|
||||
|
||||
ok = device_source.send_recv(CCProtocolPacker.start_backup())
|
||||
assert ok is None
|
||||
time.sleep(1)
|
||||
title, story = _cap_story(device_source)
|
||||
assert "backup file password" in story
|
||||
word_list = [item.split()[-1] for item in story.split("\n")[1:-4]]
|
||||
assert len(word_list) == 12
|
||||
_pass_word_quiz(device_source, False, word_list)
|
||||
_press_select(device_source, False) # bkpw
|
||||
result, chk = wait_and_download(device_source, CCProtocolPacker.get_backup_file(), 0)
|
||||
sim_source.stop()
|
||||
|
||||
|
||||
# TARGET Q (empty)
|
||||
sim_target = ColdcardSimulator(args=["--q1", "-l"])
|
||||
sim_target.start(start_wait=6)
|
||||
device_target = ColdcardDevice(is_simulator=True)
|
||||
|
||||
name = "backup-delta.7z"
|
||||
path = f"../unix/work/MicroSD/{name}"
|
||||
with open(path, "wb") as f:
|
||||
f.write(result)
|
||||
|
||||
_pick_menu_item(device_target, True, "Import Existing")
|
||||
_pick_menu_item(device_target, True, "Restore Backup")
|
||||
_pick_menu_item(device_target, True, name)
|
||||
time.sleep(.1)
|
||||
|
||||
_word_menu_entry(device_target, True, word_list, has_checksum=False)
|
||||
_press_select(device_target, True) # allow backup restore
|
||||
time.sleep(.1)
|
||||
_press_select(device_target, True) # best security practices config
|
||||
time.sleep(.1)
|
||||
_press_select(device_target, True) # success
|
||||
|
||||
sim_target.stop()
|
||||
time.sleep(1)
|
||||
sim_target = ColdcardSimulator(args=["--q1"])
|
||||
sim_target.start(start_wait=6)
|
||||
device_target = ColdcardDevice(is_simulator=True, sn=sim_target.socket)
|
||||
_pick_menu_item(device_target, True, "Settings")
|
||||
time.sleep(.1)
|
||||
_pick_menu_item(device_target, True, "Login Settings")
|
||||
time.sleep(.1)
|
||||
_pick_menu_item(device_target, True, "Trick PINs")
|
||||
time.sleep(.1)
|
||||
m = _cap_menu(device_target)
|
||||
assert "11-11" in m[1]
|
||||
|
||||
# EOF
|
||||
@ -15,6 +15,7 @@ from constants import *
|
||||
from charcodes import *
|
||||
from core_fixtures import _need_keypress, _sim_exec, _cap_story, _cap_menu, _cap_screen, _sim_eval
|
||||
from core_fixtures import _press_select, _pick_menu_item, _enter_complex, _dev_hw_label
|
||||
from core_fixtures import _do_keypresses
|
||||
from txn import render_address
|
||||
from bbqr import split_qrs
|
||||
|
||||
@ -207,14 +208,11 @@ def enter_pin(enter_number, press_select, cap_screen, is_q1):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def do_keypresses(need_keypress):
|
||||
def do_keypresses(dev):
|
||||
# do a series of keypresses, any kind
|
||||
def doit(value):
|
||||
for ch in value:
|
||||
need_keypress(ch)
|
||||
f = functools.partial(_do_keypresses, dev)
|
||||
return f
|
||||
|
||||
return doit
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def enter_text(need_keypress, is_q1):
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
# Below functions are injected with proper scoped `device` in conftest.py
|
||||
# using funtools.partial.
|
||||
#
|
||||
import time
|
||||
import time, re
|
||||
from charcodes import *
|
||||
from ckcc_protocol.client import CCProtocolPacker
|
||||
|
||||
@ -149,4 +149,116 @@ def _enter_complex(device, is_Q, target, apply=False, b39pass=True):
|
||||
if apply:
|
||||
_pick_menu_item(device, is_Q, "APPLY")
|
||||
|
||||
|
||||
def _pass_word_quiz(device, is_Q, words, prefix='', preload=None):
|
||||
if not preload:
|
||||
_press_select(device, is_Q)
|
||||
time.sleep(.01)
|
||||
|
||||
count = 0
|
||||
last_title = None
|
||||
while 1:
|
||||
title, body = preload or _cap_story(device)
|
||||
preload = None
|
||||
|
||||
if not title.startswith('Word ' + prefix): break
|
||||
assert title.endswith(' is?')
|
||||
assert not last_title or last_title != title, "gave wrong ans?"
|
||||
|
||||
wn = int(title.split()[1][len(prefix):])
|
||||
assert 1 <= wn <= len(words)
|
||||
wn -= 1
|
||||
|
||||
ans = [w[3:].strip() for w in body.split('\n') if w and w[2] == ':']
|
||||
assert len(ans) == 3
|
||||
|
||||
correct = ans.index(words[wn])
|
||||
assert 0 <= correct < 3
|
||||
|
||||
# print("Pick %d: %s" % (correct, ans[correct]))
|
||||
|
||||
_need_keypress(device, chr(49 + correct))
|
||||
time.sleep(.1)
|
||||
count += 1
|
||||
|
||||
last_title = title
|
||||
|
||||
return count, title, body
|
||||
|
||||
|
||||
def _do_keypresses(device, value):
|
||||
for ch in value:
|
||||
_need_keypress(device, ch)
|
||||
|
||||
def _word_menu_entry(device, is_Q, words, has_checksum=True, q_accept=True):
|
||||
if is_Q:
|
||||
# easier for us on Q, but have to anticipate the autocomplete
|
||||
for n, w in enumerate(words, start=1):
|
||||
_do_keypresses(device, w[0:2])
|
||||
time.sleep(0.1)
|
||||
if 'Next key' in _cap_screen(device):
|
||||
_do_keypresses(device, w[2])
|
||||
time.sleep(.01)
|
||||
|
||||
if 'Next key' in _cap_screen(device):
|
||||
if len(w) > 3:
|
||||
_do_keypresses(device, w[3])
|
||||
else:
|
||||
_do_keypresses(device, KEY_DOWN)
|
||||
time.sleep(.01)
|
||||
|
||||
pat = rf'{n}:\s?{w}'
|
||||
for x in range(10):
|
||||
if re.search(pat, _cap_screen(device)):
|
||||
break
|
||||
time.sleep(0.02)
|
||||
else:
|
||||
raise RuntimeError('timeout')
|
||||
|
||||
if len(words) == 23:
|
||||
_do_keypresses(device, KEY_DOWN)
|
||||
time.sleep(.03)
|
||||
cap_scr = _cap_screen(device)
|
||||
while 'Next key' in cap_scr:
|
||||
target = cap_scr.split("\n")[-1].replace("Next key: ", "")
|
||||
# picks first choice!?
|
||||
_do_keypresses(device, target[0])
|
||||
time.sleep(.03)
|
||||
cap_scr = _cap_screen(device)
|
||||
else:
|
||||
cap_scr = _cap_screen(device)
|
||||
|
||||
if has_checksum:
|
||||
assert 'Valid words' in cap_scr
|
||||
else:
|
||||
assert 'Press ENTER if all done' in cap_scr
|
||||
|
||||
if q_accept:
|
||||
_do_keypresses(device, '\r')
|
||||
return
|
||||
|
||||
# do the massive drilling-down to pick a specific pass phrase
|
||||
assert len(words) in {1, 12, 18, 23, 24}
|
||||
|
||||
for word in words:
|
||||
while 1:
|
||||
menu = _cap_menu(device)
|
||||
which = None
|
||||
for m in menu:
|
||||
if '-' not in m:
|
||||
if m == word:
|
||||
which = m
|
||||
break
|
||||
else:
|
||||
assert m[-1] == '-'
|
||||
if m == word[0:len(m)-1]+'-':
|
||||
which = m
|
||||
break
|
||||
|
||||
assert which, "cant find: " + word
|
||||
|
||||
_pick_menu_item(device, is_Q, which)
|
||||
if '-' not in which:
|
||||
break
|
||||
|
||||
# EOF
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
#
|
||||
import pytest, time, os, re, hashlib, shutil
|
||||
from helpers import xfp2str, prandom, addr_from_display_format
|
||||
from charcodes import KEY_DOWN, KEY_QR, KEY_NFC, KEY_DELETE, KEY_UP
|
||||
import pytest, time, os, re, hashlib, shutil, functools
|
||||
from helpers import xfp2str, prandom
|
||||
from charcodes import KEY_QR, KEY_NFC, KEY_DELETE
|
||||
from constants import AF_CLASSIC, simulator_fixed_words, simulator_fixed_xfp
|
||||
from mnemonic import Mnemonic
|
||||
from bip32 import BIP32Node
|
||||
from core_fixtures import _pass_word_quiz, _word_menu_entry
|
||||
|
||||
mnem = Mnemonic('english')
|
||||
wordlist = mnem.wordlist
|
||||
@ -97,117 +98,14 @@ def test_home_menu(cap_menu, cap_story, cap_screen, need_keypress, reset_seed_wo
|
||||
press_cancel()
|
||||
|
||||
@pytest.fixture
|
||||
def word_menu_entry(cap_menu, pick_menu_item, is_q1, do_keypresses, cap_screen):
|
||||
def doit(words, has_checksum=True, q_accept=True):
|
||||
if is_q1:
|
||||
# easier for us on Q, but have to anticipate the autocomplete
|
||||
for n, w in enumerate(words, start=1):
|
||||
do_keypresses(w[0:2])
|
||||
time.sleep(0.05)
|
||||
if 'Next key' in cap_screen():
|
||||
do_keypresses(w[2])
|
||||
time.sleep(.01)
|
||||
if 'Next key' in cap_screen():
|
||||
if len(w) > 3:
|
||||
do_keypresses(w[3])
|
||||
else:
|
||||
do_keypresses(KEY_DOWN)
|
||||
time.sleep(.01)
|
||||
|
||||
pat = rf'{n}:\s?{w}'
|
||||
for x in range(10):
|
||||
if re.search(pat, cap_screen()):
|
||||
break
|
||||
time.sleep(0.02)
|
||||
else:
|
||||
raise RuntimeError('timeout')
|
||||
|
||||
if len(words) == 23:
|
||||
do_keypresses(KEY_DOWN)
|
||||
time.sleep(.03)
|
||||
cap_scr = cap_screen()
|
||||
while 'Next key' in cap_scr:
|
||||
target = cap_scr.split("\n")[-1].replace("Next key: ", "")
|
||||
# picks first choice!?
|
||||
do_keypresses(target[0])
|
||||
time.sleep(.03)
|
||||
cap_scr = cap_screen()
|
||||
else:
|
||||
cap_scr = cap_screen()
|
||||
|
||||
if has_checksum:
|
||||
assert 'Valid words' in cap_scr
|
||||
else:
|
||||
assert 'Press ENTER if all done' in cap_scr
|
||||
|
||||
if q_accept:
|
||||
do_keypresses('\r')
|
||||
return
|
||||
|
||||
# do the massive drilling-down to pick a specific pass phrase
|
||||
assert len(words) in {1, 12, 18, 23, 24}
|
||||
|
||||
for word in words:
|
||||
while 1:
|
||||
menu = cap_menu()
|
||||
which = None
|
||||
for m in menu:
|
||||
if '-' not in m:
|
||||
if m == word:
|
||||
which = m
|
||||
break
|
||||
else:
|
||||
assert m[-1] == '-'
|
||||
if m == word[0:len(m)-1]+'-':
|
||||
which = m
|
||||
break
|
||||
|
||||
assert which, "cant find: " + word
|
||||
|
||||
pick_menu_item(which)
|
||||
if '-' not in which:
|
||||
break
|
||||
|
||||
return doit
|
||||
def word_menu_entry(dev, cap_menu, pick_menu_item, is_q1, do_keypresses, cap_screen):
|
||||
f = functools.partial(_word_menu_entry, dev, is_q1)
|
||||
return f
|
||||
|
||||
@pytest.fixture
|
||||
def pass_word_quiz(need_keypress, cap_story, press_select):
|
||||
def doit(words, prefix='', preload=None):
|
||||
if not preload:
|
||||
press_select()
|
||||
time.sleep(.01)
|
||||
|
||||
count = 0
|
||||
last_title = None
|
||||
while 1:
|
||||
title, body = preload or cap_story()
|
||||
preload = None
|
||||
|
||||
if not title.startswith('Word '+prefix): break
|
||||
assert title.endswith(' is?')
|
||||
assert not last_title or last_title != title, "gave wrong ans?"
|
||||
|
||||
wn = int(title.split()[1][len(prefix):])
|
||||
assert 1 <= wn <= len(words)
|
||||
wn -= 1
|
||||
|
||||
ans = [w[3:].strip() for w in body.split('\n') if w and w[2] == ':']
|
||||
assert len(ans) == 3
|
||||
|
||||
correct = ans.index(words[wn])
|
||||
assert 0 <= correct < 3
|
||||
|
||||
#print("Pick %d: %s" % (correct, ans[correct]))
|
||||
|
||||
need_keypress(chr(49 + correct))
|
||||
time.sleep(.1)
|
||||
count += 1
|
||||
|
||||
last_title = title
|
||||
|
||||
return count, title, body
|
||||
|
||||
return doit
|
||||
def pass_word_quiz(dev, need_keypress, cap_story, press_select, is_q1):
|
||||
f = functools.partial(_pass_word_quiz, dev, is_q1)
|
||||
return f
|
||||
|
||||
|
||||
@pytest.mark.qrcode
|
||||
|
||||
Loading…
Reference in New Issue
Block a user