Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35920635f3 |
@ -19,6 +19,9 @@
|
|||||||
- Ability to export all supported wallets via NFC (instead of SD card only)
|
- 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'
|
- Change electrum export file name from 'new-wallet.json' to 'new-electrum.json'
|
||||||
- Allow export of Wasabi skeleton for Bitcoin Regtest.
|
- 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
|
- 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
|
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.
|
added to prevent users from generating low-entropy seeds by rolling same value repeatedly.
|
||||||
|
|||||||
@ -96,6 +96,7 @@ def render_backup_contents():
|
|||||||
if k[0] == '_': continue # debug stuff in simulator
|
if k[0] == '_': continue # debug stuff in simulator
|
||||||
if k == 'xpub': continue # redundant, and wrong if bip39pw
|
if k == 'xpub': continue # redundant, and wrong if bip39pw
|
||||||
if k == 'xfp': 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)
|
ADD('setting.' + k, v)
|
||||||
|
|
||||||
if version.has_fatram:
|
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):
|
async def make_complete_backup(fname_pattern='backup.7z', write_sflash=False):
|
||||||
|
words = None
|
||||||
|
skip_quiz = False
|
||||||
|
|
||||||
if pa.tmp_value:
|
if pa.tmp_value:
|
||||||
if not await ux_confirm("An ephemeral seed is in effect, so backup will be of that seed."):
|
if not await ux_confirm("An ephemeral seed is in effect, so backup will be of that seed."):
|
||||||
return
|
return
|
||||||
|
|
||||||
# pick a password: like bip39 but no checksum word
|
stored_words = settings.get('bkpw', None)
|
||||||
#
|
|
||||||
b = bytearray(32)
|
|
||||||
while 1:
|
|
||||||
ckcc.rng_bytes(b)
|
|
||||||
words = bip39.b2a_words(b).split(' ')[0:num_pw_words]
|
|
||||||
|
|
||||||
ch = await seed.show_words(words,
|
if stored_words:
|
||||||
prompt="Record this (%d word) backup file password:\n", escape='6')
|
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:
|
if ch == 'y':
|
||||||
# Secret feature: plaintext mode
|
words = stored_words
|
||||||
# - only safe for people living in faraday cages inside locked vaults.
|
skip_quiz = True
|
||||||
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 == 'x':
|
if not words:
|
||||||
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]
|
||||||
|
|
||||||
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.
|
# quiz them, but be nice and do a shorter test.
|
||||||
ch = await seed.word_quiz(words, limited=(num_pw_words//3))
|
ch = await seed.word_quiz(words, limited=(num_pw_words//3))
|
||||||
if ch == 'x': return
|
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):
|
async def write_complete_backup(words, fname_pattern, write_sflash=False, allow_copies=True):
|
||||||
# Just do the writing
|
# Just do the writing
|
||||||
|
|||||||
@ -56,6 +56,7 @@ from glob import PSRAM
|
|||||||
# wa = (bool) if set, enables menu wraparound
|
# wa = (bool) if set, enables menu wraparound
|
||||||
# hsmcmd = (bool) if set, enables all user management and hsm-only USB commands
|
# 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
|
# 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
|
# Stored w/ key=00 for access before login
|
||||||
# _skip_pin = hard code a PIN value (dangerous, only for debug)
|
# _skip_pin = hard code a PIN value (dangerous, only for debug)
|
||||||
# nick = optional nickname for this coldcard (personalization)
|
# nick = optional nickname for this coldcard (personalization)
|
||||||
|
|||||||
@ -115,7 +115,9 @@ def pass_word_quiz(need_keypress, cap_story):
|
|||||||
|
|
||||||
@pytest.mark.qrcode
|
@pytest.mark.qrcode
|
||||||
@pytest.mark.parametrize('multisig', [False, 'multisig'])
|
@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!
|
# Make an encrypted 7z backup, verify it, and even restore it!
|
||||||
|
|
||||||
if multisig:
|
if multisig:
|
||||||
@ -124,30 +126,45 @@ def test_make_backup(multisig, goto_home, pick_menu_item, cap_story, need_keypre
|
|||||||
time.sleep(.1)
|
time.sleep(.1)
|
||||||
assert len(get_setting('multisig')) == 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()
|
goto_home()
|
||||||
pick_menu_item('Advanced/Tools')
|
pick_menu_item('Advanced/Tools')
|
||||||
pick_menu_item('Backup')
|
pick_menu_item('Backup')
|
||||||
pick_menu_item('Backup System')
|
pick_menu_item('Backup System')
|
||||||
|
|
||||||
title, body = cap_story()
|
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] == ':']
|
if reuse_pw:
|
||||||
assert len(words) == 12
|
assert ' 1: zoo' in body
|
||||||
|
assert '12: zoo' in body
|
||||||
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')
|
need_keypress('y')
|
||||||
|
words = ['zoo']*12
|
||||||
|
|
||||||
# pass the quiz!
|
time.sleep(0.1)
|
||||||
count, title, body = pass_word_quiz(words)
|
title, body = cap_story()
|
||||||
assert count >= 4
|
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)
|
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"
|
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.
|
# Check on-device verify UX works.
|
||||||
goto_home()
|
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
|
# avoid simulator reboot; restore normal state
|
||||||
unit_test('devtest/abort_ux.py')
|
unit_test('devtest/abort_ux.py')
|
||||||
reset_seed_words()
|
reset_seed_words()
|
||||||
|
settings_remove('multisig')
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.qrcode
|
@pytest.mark.qrcode
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user