Compare commits

...

1 Commits

Author SHA1 Message Date
Peter D. Gray
35920635f3
Save test backup password for next use 2023-02-13 11:05:30 -05:00
4 changed files with 101 additions and 38 deletions

View File

@ -19,6 +19,9 @@
- Ability to export all supported wallets via NFC (instead of SD card only)
- Change electrum export file name from 'new-wallet.json' to 'new-electrum.json'
- Allow export of Wasabi skeleton for Bitcoin Regtest.
- Backup Enhancement:
- Option to save the backup file's encryption password for next backup. Then next
backup is quick and simple: no need to record yet another 12 words.
- Enhancement: During seed generation from dice rolls, enforce at least 50 rolls
for 12 word seeds, and 99 rolls for 24 word seeds. Statistical distribution check
added to prevent users from generating low-entropy seeds by rolling same value repeatedly.

View File

@ -96,6 +96,7 @@ def render_backup_contents():
if k[0] == '_': continue # debug stuff in simulator
if k == 'xpub': continue # redundant, and wrong if bip39pw
if k == 'xfp': continue # redundant, and wrong if bip39pw
if k == 'bkpw': continue # confusing/circular
ADD('setting.' + k, v)
if version.has_fatram:
@ -215,43 +216,67 @@ async def restore_from_dict(vals):
async def make_complete_backup(fname_pattern='backup.7z', write_sflash=False):
words = None
skip_quiz = False
if pa.tmp_value:
if not await ux_confirm("An ephemeral seed is in effect, so backup will be of that seed."):
return
# pick a password: like bip39 but no checksum word
#
b = bytearray(32)
while 1:
ckcc.rng_bytes(b)
words = bip39.b2a_words(b).split(' ')[0:num_pw_words]
stored_words = settings.get('bkpw', None)
ch = await seed.show_words(words,
prompt="Record this (%d word) backup file password:\n", escape='6')
if stored_words:
stored_words = stored_words.split()
ch = await ux_show_story("Use same backup file password as last time?\n\n"
" 1: %s\n ...\n%d: %s"
% (stored_words[0], len(stored_words), stored_words[-1]), sensitive=True)
if ch == '6' and not write_sflash:
# Secret feature: plaintext mode
# - only safe for people living in faraday cages inside locked vaults.
if await ux_confirm("The file will **NOT** be encrypted and "
"anyone who finds the file will get all of your money for free!"):
words = []
fname_pattern = 'backup.txt'
break
continue
if ch == 'y':
words = stored_words
skip_quiz = True
if ch == 'x':
return
if not words:
# Pick a password: like bip39 but no checksum word
#
b = bytearray(32)
while 1:
ckcc.rng_bytes(b)
words = bip39.b2a_words(b).split(' ')[0:num_pw_words]
break
ch = await seed.show_words(words,
prompt="Record this (%d word) backup file password:\n", escape='6')
if ch == '6' and not write_sflash:
# Secret feature: plaintext mode
# - only safe for people living in faraday cages inside locked vaults.
if await ux_confirm("The file will **NOT** be encrypted and "
"anyone who finds the file will get all of your money for free!"):
words = []
fname_pattern = 'backup.txt'
break
continue
if words:
if ch == 'x':
return
break
if words and not skip_quiz:
# quiz them, but be nice and do a shorter test.
ch = await seed.word_quiz(words, limited=(num_pw_words//3))
if ch == 'x': return
return await write_complete_backup(words, fname_pattern, write_sflash=write_sflash)
await write_complete_backup(words, fname_pattern, write_sflash=write_sflash)
if words and words != stored_words:
ch = await ux_show_story("Would you like to use these same words next time you perform a backup? Press (1) to save them into this Coldcard for next time.", escape='1')
if ch == '1':
settings.put('bkpw', ' '.join(words))
settings.save()
elif stored_words:
settings.remove_key('bkpw')
settings.save()
async def write_complete_backup(words, fname_pattern, write_sflash=False, allow_copies=True):
# Just do the writing

View File

@ -56,6 +56,7 @@ from glob import PSRAM
# wa = (bool) if set, enables menu wraparound
# hsmcmd = (bool) if set, enables all user management and hsm-only USB commands
# sd2fa = (list of strings): track which SD card is needed for login
# bkpw = (string): last backup password, so can be re-used easily
# Stored w/ key=00 for access before login
# _skip_pin = hard code a PIN value (dangerous, only for debug)
# nick = optional nickname for this coldcard (personalization)

View File

@ -115,7 +115,9 @@ def pass_word_quiz(need_keypress, cap_story):
@pytest.mark.qrcode
@pytest.mark.parametrize('multisig', [False, 'multisig'])
def test_make_backup(multisig, goto_home, pick_menu_item, cap_story, need_keypress, open_microsd, microsd_path, unit_test, cap_menu, word_menu_entry, pass_word_quiz, reset_seed_words, import_ms_wallet, get_setting, cap_screen_qr):
@pytest.mark.parametrize('reuse_pw', [False, True])
@pytest.mark.parametrize('save_pw', [False, True])
def test_make_backup(multisig, goto_home, pick_menu_item, cap_story, need_keypress, open_microsd, microsd_path, unit_test, cap_menu, word_menu_entry, pass_word_quiz, reset_seed_words, import_ms_wallet, get_setting, cap_screen_qr, reuse_pw, save_pw, settings_set, settings_remove):
# Make an encrypted 7z backup, verify it, and even restore it!
if multisig:
@ -124,30 +126,45 @@ def test_make_backup(multisig, goto_home, pick_menu_item, cap_story, need_keypre
time.sleep(.1)
assert len(get_setting('multisig')) == 1
if reuse_pw:
settings_set('bkpw', ' '.join('zoo' for i in range(12)))
else:
settings_remove('bkpw')
goto_home()
pick_menu_item('Advanced/Tools')
pick_menu_item('Backup')
pick_menu_item('Backup System')
title, body = cap_story()
assert title == 'NO-TITLE'
assert 'Record this' in body
assert 'password:' in body
words = [w[3:].strip() for w in body.split('\n') if w and w[2] == ':']
assert len(words) == 12
print("Passphrase: %s" % ' '.join(words))
if 'QR Code' in body:
need_keypress('1')
got_qr = cap_screen_qr().decode('ascii').lower().split()
assert [w[0:4] for w in words] == got_qr
if reuse_pw:
assert ' 1: zoo' in body
assert '12: zoo' in body
need_keypress('y')
words = ['zoo']*12
# pass the quiz!
count, title, body = pass_word_quiz(words)
assert count >= 4
time.sleep(0.1)
title, body = cap_story()
else:
assert title == 'NO-TITLE'
assert 'Record this' in body
assert 'password:' in body
words = [w[3:].strip() for w in body.split('\n') if w and w[2] == ':']
assert len(words) == 12
print("Passphrase: %s" % ' '.join(words))
if 'QR Code' in body:
need_keypress('1')
got_qr = cap_screen_qr().decode('ascii').lower().split()
assert [w[0:4] for w in words] == got_qr
need_keypress('y')
# pass the quiz!
count, title, body = pass_word_quiz(words)
assert count >= 4
time.sleep(0.1)
@ -172,6 +189,22 @@ def test_make_backup(multisig, goto_home, pick_menu_item, cap_story, need_keypre
assert bk_a == bk_b, "contents mismatch"
need_keypress('x')
time.sleep(.01)
if not reuse_pw:
title, body = cap_story()
assert 'next time' in body
if save_pw:
need_keypress('1')
time.sleep(.01)
assert get_setting('bkpw') == ' '.join(words)
else:
need_keypress('x')
time.sleep(.01)
assert get_setting('bkpw', 'xxx') == 'xxx'
# Check on-device verify UX works.
goto_home()
@ -236,6 +269,7 @@ def test_make_backup(multisig, goto_home, pick_menu_item, cap_story, need_keypre
# avoid simulator reboot; restore normal state
unit_test('devtest/abort_ux.py')
reset_seed_words()
settings_remove('multisig')
@pytest.mark.qrcode