add ability to use master bkpw for tmp seeds; add bkpw override

(cherry picked from commit 70d303af78)
This commit is contained in:
scgbckbone 2024-10-21 18:55:11 +02:00
parent 132315f72b
commit 7134f03eab
8 changed files with 230 additions and 49 deletions

View File

@ -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.

View File

@ -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 \

View File

@ -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.")

View File

@ -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)

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -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