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)
|
||||
- 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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user