add ability to use master bkpw for tmp seeds; add bkpw override
(cherry picked from commit 70d303af78)
This commit is contained in:
parent
132315f72b
commit
7134f03eab
@ -16,6 +16,7 @@ This lists the new changes that have not yet been published in a normal release.
|
||||
about successful master seed verification.
|
||||
- Enhancement: Catch more DeltaMode cases in XOR path.
|
||||
Thanks to [@dmonakhov](https://github.com/dmonakhov))
|
||||
- Enhancement: BKPW override (for "developers")
|
||||
- Change: If derivation path is omitted during message signing, default is used
|
||||
based on address format (`m/44h/0h/0h/0/0` for p2pkh, and `m/84h/0h/0h/0/0` for p2wpkh).
|
||||
Default is no longer root (m).
|
||||
@ -46,4 +47,3 @@ This lists the new changes that have not yet been published in a normal release.
|
||||
- New Feature: Sign message from QR scan (format has to be JSON)
|
||||
- Enhancement: Sign scanned Simple Text by pressing (0). Next screens query information about key to use.
|
||||
- Bugfix: Properly re-draw status bar after Restore Master on COLDCARD without master seed.
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ from ubinascii import hexlify as b2a_hex
|
||||
from utils import imported, problem_file_line, get_filesize, encode_seed_qr
|
||||
from utils import xfp2str, B2A, txid_from_fname
|
||||
from ux import ux_show_story, the_ux, ux_confirm, ux_dramatic_pause, ux_aborted
|
||||
from ux import ux_enter_bip32_index, ux_input_text, import_export_prompt, OK, X
|
||||
from ux import ux_enter_bip32_index, ux_input_text, import_export_prompt, OK, X, ux_render_words
|
||||
from export import make_json_wallet, make_summary_file, make_descriptor_wallet_export
|
||||
from export import make_bitcoin_core_wallet, generate_wasabi_wallet, generate_generic_export
|
||||
from export import generate_unchained_export, generate_electrum_wallet
|
||||
@ -603,7 +603,6 @@ consequences.''', escape='4')
|
||||
def render_master_secrets(mode, raw, node):
|
||||
# Render list of words, or XPRV / master secret to text.
|
||||
import stash, chains
|
||||
from ux import ux_render_words
|
||||
|
||||
c = chains.current_chain()
|
||||
qr_alnum = False
|
||||
@ -1422,6 +1421,59 @@ async def restore_everything_cleartext(*A):
|
||||
if prob:
|
||||
await ux_show_story(prob, title='FAILED')
|
||||
|
||||
async def bkpw_override(*A):
|
||||
# allows user to:
|
||||
# 1.) manually set bkpw
|
||||
# 2.) remove existing bkpw setting
|
||||
# 3.) view current active bkpw
|
||||
from backups import bkpw_min_len
|
||||
|
||||
if pa.is_secret_blank():
|
||||
return
|
||||
|
||||
if pa.is_deltamode():
|
||||
import callgate
|
||||
callgate.fast_wipe()
|
||||
|
||||
while True:
|
||||
pwd = settings.get("bkpw", None)
|
||||
|
||||
msg = ("Password used to encrypt COLDCARD backup."
|
||||
"\n\nPress (0) to change backup password")
|
||||
esc = "0"
|
||||
if pwd:
|
||||
esc += "12"
|
||||
msg += ", (1) to forget current password, (2) to show current active backup password."
|
||||
|
||||
ch = await ux_show_story(title="BKPW", msg=msg, escape=esc)
|
||||
if ch == "x": return
|
||||
elif ch == "1":
|
||||
if await ux_confirm("Delete current stored password?"):
|
||||
settings.remove_key("bkpw")
|
||||
settings.save()
|
||||
await ux_dramatic_pause("Deleted.", 2)
|
||||
|
||||
elif ch == "2":
|
||||
if await ux_confirm('The next screen will show current active backup password.'
|
||||
'\n\nAnyone with knowledge of the password will '
|
||||
'be able to decrypt your backups.'):
|
||||
await ux_show_story(pwd)
|
||||
|
||||
elif ch == "0":
|
||||
if version.has_qwerty:
|
||||
from notes import get_a_password
|
||||
npwd = await get_a_password(pwd, min_len=bkpw_min_len)
|
||||
else:
|
||||
npwd = await ux_input_text(pwd, prompt="Your Backup Password",
|
||||
min_len=bkpw_min_len, max_len=128)
|
||||
|
||||
if (npwd is None) or (npwd == pwd): continue
|
||||
|
||||
settings.set('bkpw', npwd)
|
||||
settings.save()
|
||||
await ux_dramatic_pause("Saved.", 2)
|
||||
|
||||
|
||||
async def wipe_filesystem(*A):
|
||||
if not await ux_confirm('''\
|
||||
Erase internal filesystem and rebuild it. Resets contents of internal flash area \
|
||||
|
||||
@ -15,6 +15,7 @@ from pincodes import pa
|
||||
|
||||
# we make passwords with this number of words
|
||||
num_pw_words = const(12)
|
||||
bkpw_min_len = const(32)
|
||||
|
||||
# max size we expect for a backup data file (encrypted or cleartext)
|
||||
# - limited by size of LFS area of flash, since all settings are held there
|
||||
@ -309,7 +310,7 @@ async def restore_from_dict(vals):
|
||||
async def make_complete_backup(fname_pattern='backup.7z', write_sflash=False):
|
||||
from stash import bip39_passphrase
|
||||
|
||||
words = None
|
||||
pwd = None
|
||||
skip_quiz = False
|
||||
bypass_tmp = False
|
||||
|
||||
@ -329,28 +330,40 @@ async def make_complete_backup(fname_pattern='backup.7z', write_sflash=False):
|
||||
"so backup will be of that seed."):
|
||||
return
|
||||
|
||||
stored_words = settings.get('bkpw', None)
|
||||
# first check if bkpw already defined on tmp seed settings
|
||||
stored_pwd = None
|
||||
master_pwd = settings.master_get("bkpw", None)
|
||||
if pa.tmp_value:
|
||||
stored_pwd = settings.get('bkpw', None)
|
||||
|
||||
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 not stored_pwd and master_pwd:
|
||||
stored_pwd = master_pwd
|
||||
|
||||
if stored_pwd:
|
||||
# we can have words or other type of password here
|
||||
split_pwd = stored_pwd.split()
|
||||
if len(split_pwd) == num_pw_words: # weak
|
||||
hint = " 1: %s\n ...\n%d: %s" % (split_pwd[0], len(split_pwd), split_pwd[-1])
|
||||
else:
|
||||
hint = " %s...%s" % (stored_pwd[0], stored_pwd[-1])
|
||||
|
||||
ch = await ux_show_story("Use same backup file password as last time?\n\n" + hint,
|
||||
sensitive=True)
|
||||
|
||||
if ch == 'y':
|
||||
words = stored_words
|
||||
pwd = stored_pwd # string, not list
|
||||
skip_quiz = True
|
||||
|
||||
if not words:
|
||||
if not pwd:
|
||||
# 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]
|
||||
pwd = bip39.b2a_words(b).rsplit(' ', num_pw_words)[0]
|
||||
|
||||
ch = await seed.show_words(words,
|
||||
prompt="Record this (%d word) backup file password:\n", escape='6')
|
||||
ch = await seed.show_words(prompt="Record this (%d word) backup file password:\n",
|
||||
words=pwd.split(" "), escape='6')
|
||||
|
||||
if ch == '6' and not write_sflash:
|
||||
# Secret feature: plaintext mode
|
||||
@ -367,43 +380,43 @@ async def make_complete_backup(fname_pattern='backup.7z', write_sflash=False):
|
||||
|
||||
break
|
||||
|
||||
if words and not skip_quiz:
|
||||
if pwd 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))
|
||||
ch = await seed.word_quiz(pwd.split(" "), limited=(num_pw_words//3))
|
||||
if ch == 'x': return
|
||||
|
||||
if words and words != stored_words:
|
||||
if pwd and pwd != stored_pwd:
|
||||
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.set('bkpw', pwd) # if on tmp save to tmp, do not update master
|
||||
settings.save()
|
||||
# stop droping bkpw just because someone decided to use differrent password
|
||||
# elif stored_words:
|
||||
# settings.remove_key('bkpw')
|
||||
# settings.save()
|
||||
|
||||
return await write_complete_backup(words, fname_pattern, write_sflash=write_sflash,
|
||||
return await write_complete_backup(pwd, fname_pattern, write_sflash=write_sflash,
|
||||
bypass_tmp=bypass_tmp)
|
||||
|
||||
async def write_complete_backup(words, fname_pattern, write_sflash=False,
|
||||
async def write_complete_backup(pwd, fname_pattern, write_sflash=False,
|
||||
allow_copies=True, bypass_tmp=False):
|
||||
# Just do the writing
|
||||
from glob import dis
|
||||
from files import CardSlot
|
||||
|
||||
# Show progress:
|
||||
dis.fullscreen('Encrypting...' if words else 'Generating...')
|
||||
dis.fullscreen('Encrypting...' if pwd else 'Generating...')
|
||||
body = render_backup_contents(bypass_tmp=bypass_tmp).encode()
|
||||
|
||||
gc.collect()
|
||||
|
||||
if words:
|
||||
if pwd:
|
||||
# NOTE: Takes a few seconds to do the key-streching, but little actual
|
||||
# time to do the encryption.
|
||||
|
||||
pw = ' '.join(words)
|
||||
zz = compat7z.Builder(password=pw, progress_fcn=dis.progress_bar_show)
|
||||
zz = compat7z.Builder(password=pwd, progress_fcn=dis.progress_bar_show)
|
||||
zz.add_data(body)
|
||||
|
||||
# pick random filename, but ending in .txt
|
||||
@ -742,11 +755,9 @@ async def clone_write_data(*a):
|
||||
my_pubkey = pair.pubkey().to_bytes(False)
|
||||
session_key = pair.ecdh_multiply(his_pubkey)
|
||||
|
||||
words = [b2a_hex(session_key).decode()]
|
||||
|
||||
fname = b2a_hex(my_pubkey).decode() + '-ccbk.7z'
|
||||
|
||||
await write_complete_backup(words, fname, allow_copies=False, bypass_tmp=True)
|
||||
await write_complete_backup(b2a_hex(session_key).decode(), fname, allow_copies=False, bypass_tmp=True)
|
||||
|
||||
await ux_show_story("Done.\n\nTake this MicroSD card back to other Coldcard and continue from there.")
|
||||
|
||||
|
||||
@ -236,6 +236,7 @@ DevelopersMenu = [
|
||||
MenuItem("Serial REPL", f=dev_enable_repl),
|
||||
MenuItem('Warm Reset', f=reset_self),
|
||||
MenuItem("Restore Txt Bkup", f=restore_everything_cleartext),
|
||||
MenuItem("BKPW Override", menu=bkpw_override),
|
||||
]
|
||||
|
||||
AdvancedVirginMenu = [ # No PIN, no secrets yet (factory fresh)
|
||||
|
||||
@ -50,7 +50,7 @@ Press ENTER to enable and get started otherwise CANCEL.''',
|
||||
|
||||
return NotesMenu(NotesMenu.construct())
|
||||
|
||||
async def get_a_password(old_value):
|
||||
async def get_a_password(old_value, min_len=0, max_len=128):
|
||||
# Get a (new) password as a string.
|
||||
# - does some fun generation as well.
|
||||
|
||||
@ -104,9 +104,9 @@ async def get_a_password(old_value):
|
||||
handlers = {KEY_F1: _pick_12, KEY_F2: _pick_24, KEY_F3: _pick_dense,
|
||||
KEY_F4: _do_dumb, KEY_F6: _toggle_case, KEY_F5: _bip85}
|
||||
|
||||
return await ux_input_text(old_value, confirm_exit=False, max_len=128, scan_ok=True,
|
||||
b39_complete=True, prompt='Password', placeholder='(optional)',
|
||||
funct_keys=(fmsg, handlers))
|
||||
return await ux_input_text(old_value, confirm_exit=False, max_len=max_len, min_len=min_len,
|
||||
scan_ok=True, b39_complete=True, prompt='Password',
|
||||
placeholder='(optional)', funct_keys=(fmsg, handlers))
|
||||
|
||||
class NotesMenu(MenuSystem):
|
||||
|
||||
|
||||
@ -84,10 +84,11 @@ from utils import call_later_ms
|
||||
# prelogin settings - do not need to be part of other saved settings
|
||||
# PRELOGIN_SETTINGS = ["_skip_pin", "nick", "rngk", "lgto", "kbtn", "terms_ok"]
|
||||
# keep these settings only if unspecified on the other end
|
||||
KEEP_IF_BLANK_SETTINGS = ["bkpw", "wa", "sighshchk", "emu", "rz", "b39skip",
|
||||
"axskip", "del", "pms", "idle_to", "batt_to", "bright"]
|
||||
KEEP_IF_BLANK_SETTINGS = ["wa", "sighshchk", "emu", "rz", "b39skip",
|
||||
"axskip", "del", "pms", "idle_to", "batt_to",
|
||||
"bright"]
|
||||
|
||||
SEEDVAULT_FIELDS = ['seeds', 'seedvault', 'xfp', 'words']
|
||||
SEEDVAULT_FIELDS = ['seeds', 'seedvault', 'xfp', 'words', "bkpw"]
|
||||
|
||||
NUM_SLOTS = const(100)
|
||||
SLOTS = range(NUM_SLOTS)
|
||||
|
||||
@ -162,7 +162,6 @@ async def ux_input_text(value, confirm_exit=False, hex_only=False, max_len=100,
|
||||
# to make longer single-line value onto screen
|
||||
# - confirm_exit default False here, because so easy to re-enter w/ qwerty, True on mk4
|
||||
from glob import dis
|
||||
from ux import ux_show_story
|
||||
|
||||
MAX_LINES = 7 # without scroll
|
||||
can_scroll = False
|
||||
|
||||
@ -2,23 +2,104 @@
|
||||
#
|
||||
# Testing backups.
|
||||
#
|
||||
import pytest, time, json, os, shutil
|
||||
import pytest, time, json, os, shutil, re
|
||||
from constants import simulator_fixed_words, simulator_fixed_tprv
|
||||
from charcodes import KEY_QR
|
||||
from bip32 import BIP32Node
|
||||
from mnemonic import Mnemonic
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def override_bkpw(goto_home, pick_menu_item, cap_story, need_keypress, seed_story_to_words,
|
||||
cap_menu, press_select, press_cancel, enter_complex, is_q1):
|
||||
|
||||
def purge_current(exit=False):
|
||||
time.sleep(.1)
|
||||
title, story = cap_story()
|
||||
if "(1) to forget current" in story:
|
||||
need_keypress("1")
|
||||
time.sleep(.1)
|
||||
title, story = cap_story()
|
||||
assert "Delete current stored password?" in story
|
||||
press_select()
|
||||
time.sleep(.1)
|
||||
title, story = cap_story()
|
||||
assert "(1) to forget current" not in story
|
||||
if exit:
|
||||
press_cancel()
|
||||
|
||||
def doit(password=None, old_password=None):
|
||||
goto_home()
|
||||
pick_menu_item("Advanced/Tools")
|
||||
pick_menu_item("Danger Zone")
|
||||
pick_menu_item("I Am Developer.")
|
||||
pick_menu_item("BKPW Override")
|
||||
time.sleep(.1)
|
||||
title, story = cap_story()
|
||||
current_bkpw = None
|
||||
if "(2) to show current active backup password" in story:
|
||||
need_keypress("2")
|
||||
time.sleep(.1)
|
||||
title, story = cap_story()
|
||||
assert 'Anyone with knowledge of the password will be able to decrypt your backups.' in story
|
||||
press_select()
|
||||
time.sleep(.1)
|
||||
title, current_bkpw = cap_story()
|
||||
current_bkpw = current_bkpw.strip()
|
||||
press_select()
|
||||
|
||||
if old_password:
|
||||
assert current_bkpw == old_password, "old_password mismatch"
|
||||
|
||||
if password is None:
|
||||
# purge current bkpw
|
||||
purge_current(exit=True)
|
||||
return
|
||||
|
||||
# purge what was there from before
|
||||
purge_current()
|
||||
|
||||
need_keypress("0")
|
||||
enter_complex(password, apply=False, b39pass=False)
|
||||
|
||||
time.sleep(.1)
|
||||
title, story = cap_story()
|
||||
assert "(2) to show current active backup password" in story
|
||||
need_keypress("2")
|
||||
press_select() # are you sure?
|
||||
time.sleep(.1)
|
||||
title, story = cap_story()
|
||||
new_current_bkpw = story.strip()
|
||||
press_select()
|
||||
|
||||
time.sleep(.1)
|
||||
title, story = cap_story()
|
||||
if ((3*" ") in password) and not is_q1:
|
||||
assert password.replace(" ", " ") == new_current_bkpw
|
||||
else:
|
||||
assert new_current_bkpw == password
|
||||
|
||||
assert "(1) to forget current password" in story
|
||||
assert "(0) to change" in story
|
||||
|
||||
return doit
|
||||
|
||||
@pytest.fixture
|
||||
def backup_system(settings_set, settings_remove, goto_home, pick_menu_item,
|
||||
cap_story, need_keypress, cap_screen_qr, pass_word_quiz,
|
||||
get_setting, seed_story_to_words, press_cancel, is_q1,
|
||||
press_select, is_headless):
|
||||
def doit(reuse_pw=False, save_pw=False, st=None, ct=False):
|
||||
def doit(reuse_pw=None, save_pw=False, st=None, ct=False):
|
||||
# st -> seed type
|
||||
# ct -> cleartext backup
|
||||
if reuse_pw:
|
||||
settings_set('bkpw', ' '.join('zoo' for _ in range(12)))
|
||||
if isinstance(reuse_pw, list):
|
||||
assert len(reuse_pw) == 12
|
||||
else:
|
||||
assert reuse_pw is True # default
|
||||
reuse_pw = ['zoo' for _ in range(12)]
|
||||
|
||||
settings_set('bkpw', ' '.join(reuse_pw))
|
||||
else:
|
||||
settings_remove('bkpw')
|
||||
|
||||
@ -55,13 +136,10 @@ def backup_system(settings_set, settings_remove, goto_home, pick_menu_item,
|
||||
return # nothing more to be done
|
||||
|
||||
if reuse_pw:
|
||||
assert ' 1: zoo' in body
|
||||
assert '12: zoo' in body
|
||||
assert (' 1: %s' % reuse_pw[0]) in body
|
||||
assert ('12: %s' % reuse_pw[-1]) in body
|
||||
press_select()
|
||||
words = ['zoo'] * 12
|
||||
|
||||
time.sleep(0.1)
|
||||
title, body = cap_story()
|
||||
else:
|
||||
assert title == 'NO-TITLE'
|
||||
assert 'Record this' in body
|
||||
@ -102,7 +180,7 @@ def backup_system(settings_set, settings_remove, goto_home, pick_menu_item,
|
||||
@pytest.mark.qrcode
|
||||
@pytest.mark.parametrize('multisig', [False, 'multisig'])
|
||||
@pytest.mark.parametrize('st', ["b39pass", "eph", None])
|
||||
@pytest.mark.parametrize('reuse_pw', [False, True])
|
||||
@pytest.mark.parametrize('reuse_pw', [True, False])
|
||||
@pytest.mark.parametrize('save_pw', [False, True])
|
||||
@pytest.mark.parametrize('seedvault', [False, True])
|
||||
@pytest.mark.parametrize('pass_way', ["qr", None])
|
||||
@ -147,6 +225,10 @@ def test_make_backup(multisig, goto_home, pick_menu_item, cap_story, need_keypre
|
||||
time.sleep(.1)
|
||||
assert len(get_setting('multisig')) == 1
|
||||
|
||||
if not reuse_pw:
|
||||
# drop saved bkpw before we get to ephemeral settings
|
||||
settings_remove("bkpw")
|
||||
|
||||
if st == "b39pass":
|
||||
xfp_pass = set_bip39_pw("coinkite", reset=False, seed_vault=seedvault)
|
||||
assert not get_setting('multisig', None)
|
||||
@ -441,7 +523,6 @@ def test_seed_vault_backup(settings_set, reset_seed_words, generate_ephemeral_wo
|
||||
assert "Press (1) to save" in body
|
||||
press_cancel()
|
||||
time.sleep(.01)
|
||||
assert get_setting('bkpw', 'xxx') == 'xxx'
|
||||
title, story = cap_story()
|
||||
assert "Backup file written:" in story
|
||||
fn = story.split("\n\n")[1]
|
||||
@ -516,4 +597,40 @@ def test_clone_start(reset_seed_words, pick_menu_item, cap_story, goto_home):
|
||||
# TODO check file made is a good backup, with correct password
|
||||
|
||||
|
||||
def test_bkpw_override(reset_seed_words, override_bkpw, goto_home, pick_menu_item,
|
||||
cap_story, press_select, garbage_collector, microsd_path):
|
||||
reset_seed_words() # clean slate
|
||||
old_pw = None
|
||||
test_cases = [
|
||||
" ".join(12 * ["elevator"]),
|
||||
" ".join(12 * ["fever"]),
|
||||
32 * "a",
|
||||
(16 * "0") + " " + (16 *"1"),
|
||||
64 * "Q",
|
||||
(26 * "?") + "!@#$%^&*()",
|
||||
]
|
||||
for pw in test_cases:
|
||||
override_bkpw(pw, old_pw)
|
||||
|
||||
goto_home()
|
||||
pick_menu_item("Advanced/Tools")
|
||||
pick_menu_item("Backup")
|
||||
pick_menu_item("Backup System")
|
||||
time.sleep(1)
|
||||
title, story = cap_story()
|
||||
split_pw = pw.split(" ")
|
||||
if len(split_pw) == 12:
|
||||
assert (' 1: %s' % split_pw[0]) in story
|
||||
assert ('12: %s' % split_pw[-1]) in story
|
||||
else:
|
||||
# not words of len 12
|
||||
assert ("%s...%s" % (pw[0], pw[-1])) in story
|
||||
|
||||
press_select()
|
||||
time.sleep(1)
|
||||
title, story = cap_story()
|
||||
assert "Backup file written" in story
|
||||
garbage_collector.append(microsd_path(story.split("\n\n")[1]))
|
||||
press_select()
|
||||
|
||||
# EOF
|
||||
|
||||
Loading…
Reference in New Issue
Block a user