bugfix: incorrect xfp shown in meta for passphrase on top of eph secret

(cherry picked from commit 8d304f408a)
This commit is contained in:
scgbckbone 2023-10-05 02:43:41 +02:00 committed by doc-hex
parent fdbfc85fd4
commit ccc0ac039c
6 changed files with 257 additions and 41 deletions

View File

@ -223,7 +223,7 @@ DangerZoneMenu = [
on_change=change_seed_vault,
story=("Enable Seed Vault? Adds prompt to store ephemeral secrets "
"into Seed Vault, where they can easily be reused later.\n\n"
"WARNING: Seed Vault is encrypted (AES-256-CTR) by master seed,"
"WARNING: Seed Vault is encrypted (AES-256-CTR) by ephemeral master seed,"
" but not held directly inside secure elements. Backups are required"
" after any change to vault! Recommended for experiments or temporary use."),
predicate=has_secrets),

View File

@ -230,8 +230,7 @@ class SettingsObject:
def _used_slots(self):
# mk4: faster list of slots in use; doesn't open them
files = os.listdir(MK4_WORKDIR)
x = [int(fn[0:-4], 16) for fn in files if fn.endswith('.aes')]
return x
return [int(fn[0:-4], 16) for fn in files if fn.endswith('.aes')]
def _nonempty_slots(self, dis=None):
# generate slots that are non-empty

View File

@ -428,18 +428,22 @@ async def add_seed_to_vault(encoded, meta=None):
new_xfp_str = xfp2str(new_xfp)
tmp_val = None
# do not offer to store main seed
if new_xfp == settings.get("xfp", 0):
return
# do not offer to store secrets that are already in vault
if await in_seed_vault(new_xfp_str, settings.get("seeds", [])):
return
if pa.tmp_value:
dis.fullscreen("Wait...")
tmp_val = pa.tmp_value[:]
await restore_to_main_secret(preserve_settings=True)
# do not offer to store main seed
if new_xfp == settings.get("xfp", 0):
if tmp_val:
pa.tmp_secret(tmp_val)
return
seeds = settings.get("seeds", [])
xfp_ui = "[%s]" % new_xfp_str
@ -459,6 +463,7 @@ async def add_seed_to_vault(encoded, meta=None):
await ux_show_story(xfp_ui + "\nSaved to Seed Vault")
if tmp_val:
dis.fullscreen("Wait...")
pa.tmp_secret(tmp_val)
settings.set("seeds", seeds)
settings.save()
@ -649,15 +654,23 @@ def set_seed_value(words=None, encoded=None, chain=None):
async def calc_bip39_passphrase(pw, bypass_tmp=False):
from glob import dis, settings
from pincodes import pa
dis.fullscreen("Working...")
current_xfp = settings.get("xfp", 0)
# generate secret
# get xfp of parent reliably - cannot go to settings for this if in ephemeral
if pa.tmp_value:
with stash.SensitiveValues(bypass_tmp=bypass_tmp) as sv:
assert sv.mode == 'words'
current_xfp = swab32(sv.node.my_fp())
else:
current_xfp = settings.get("xfp", 0)
with stash.SensitiveValues(bip39pw=pw, bypass_tmp=bypass_tmp) as sv:
# can't do it without original seed words (late, but caller has checked)
assert sv.mode == 'words'
nv = SecretStash.encode(xprv=sv.node)
xfp = swab32(sv.node.my_fp())
return nv, xfp, current_xfp
async def set_bip39_passphrase(pw, bypass_tmp=False, summarize_ux=True):
@ -801,7 +814,7 @@ class SeedVaultMenu(MenuSystem):
"only remove from seed vault and keep "
"encrypted settings for later use.\n\n"
"WARNING: Funds will be lost if wallet is"
"not backed up elsewhere.")
" not backed up elsewhere.")
ch = await ux_show_story(title="[" + xfp_str + "]",
msg=msg, escape="1")
@ -842,10 +855,12 @@ class SeedVaultMenu(MenuSystem):
finally:
if tmp_val and (not wipe_slot):
# we were in ephemeral mode before and have not
# wiped seed - return back to ephemral
# wiped seed - return back to ephemeral
pa.tmp_secret(tmp_val)
settings.set("seeds", seeds)
settings.save()
elif tmp_val and wipe_slot:
goto_top_menu()
# pop menu stack
the_ux.pop()
@ -989,18 +1004,12 @@ class EphemeralSeedMenu(MenuSystem):
MenuItem("Import XPRV", f=import_xprv, arg=True), # ephemeral=True
MenuItem("Tapsigner Backup", f=import_tapsigner_backup_file, arg=True), # ephemeral=True
]
if pa.tmp_value:
xfp = settings.get("xfp", "")
if xfp:
rv.insert(0, MenuItem("[%s]" % xfp2str(xfp)))
else:
rv.insert(0, MenuItem("[Active]"))
return rv
async def make_ephemeral_seed_menu(*a):
if not (pa.tmp_value or settings.get("seedvault", False)):
if (not pa.tmp_value) and (not settings.get("seedvault", False)):
# force a warning on them, unless they are already doing it.
ch = await ux_show_story(
"Ephemeral seed is a temporary secret completely separate "

View File

@ -2,6 +2,8 @@
#
# BIP-39 seed word encryption
#
import pdb
import pytest, time, struct
from pycoin.key.BIP32Node import BIP32Node
from binascii import a2b_hex
@ -202,19 +204,29 @@ def test_lockdown(stype, pick_menu_item, set_bip39_pw, goto_home, cap_story,
@pytest.mark.parametrize("stype", ["words", "xprv"])
@pytest.mark.parametrize("passphrase", ["@coinkite rulez!!", "!@#!@", "AAAAAAAAAAA"])
@pytest.mark.parametrize("on_eph", [True, False])
@pytest.mark.parametrize("seed_vault", [True, False])
def test_bip39pass_on_ephemeral_seed(generate_ephemeral_words, import_ephemeral_xprv,
need_keypress, pick_menu_item, goto_home,
reset_seed_words, goto_eph_seed_menu, stype,
enter_complex, cap_story, cap_menu, passphrase):
enter_complex, cap_story, cap_menu, on_eph,
settings_set, seed_vault):
passphrase = "@coinkite rulez!!"
reset_seed_words()
settings_set("seedvault", 1)
settings_set("seeds", [])
goto_eph_seed_menu()
if stype == "words":
# words
sec = generate_ephemeral_words(24, from_main=True)
sec = generate_ephemeral_words(24, from_main=True, seed_vault=seed_vault)
parent = Mnemonic.to_seed(" ".join(sec))
parent_node = BIP32Node.from_master_secret(parent)
parent_fp = parent_node.fingerprint().hex().upper()
else:
# node
sec = import_ephemeral_xprv("sd", from_main=True)
sec = import_ephemeral_xprv("sd", from_main=True, seed_vault=seed_vault)
goto_home()
if stype == "xprv":
@ -235,13 +247,58 @@ def test_bip39pass_on_ephemeral_seed(generate_ephemeral_words, import_ephemeral_
expect0 = BIP32Node.from_master_secret(seed0)
assert expect0.fingerprint().hex().upper() == xfp0
assert "press (2) to add passphrase to the current active ephemeral seed" in story
need_keypress("2")
time.sleep(.5)
if on_eph:
need_keypress("2")
time.sleep(.5)
title, story = cap_story()
xfp1 = title[1:-1]
seed1 = Mnemonic.to_seed(" ".join(sec), passphrase=passphrase)
expect1 = BIP32Node.from_master_secret(seed1)
assert expect1.fingerprint().hex().upper() == xfp1
assert "press (2)" not in story
need_keypress("y")
else:
need_keypress("y")
time.sleep(.3)
title, story = cap_story()
xfp1 = title[1:-1]
seed1 = Mnemonic.to_seed(" ".join(sec), passphrase=passphrase)
expect1 = BIP32Node.from_master_secret(seed1)
assert expect1.fingerprint().hex().upper() == xfp1
assert "press (2)" not in story
to_check = None
if seed_vault:
assert "Press (1) to store ephemeral secret into Seed Vault" in story
need_keypress("1")
time.sleep(.2)
title, story = cap_story()
assert "Saved to Seed Vault" in story
if on_eph:
assert xfp1 in story
to_check = xfp1
else:
assert xfp0 in story
to_check = xfp0
need_keypress("y")
else:
assert "Press (1)" not in story
if seed_vault:
# check correct meta in seed vault
pick_menu_item("Seed Vault")
m = cap_menu()
for i in m:
if to_check in i:
pick_menu_item(i)
break
else:
pytest.fail("not in menu")
# choose first info item in submenu
need_keypress("y")
time.sleep(.1)
_, story = cap_story()
assert to_check in story
if on_eph:
assert ("BIP-39 Passphrase on [%s]" % parent_fp) in story
else:
assert "BIP-39 Passphrase on [0F056943]" in story
# EOF

View File

@ -109,7 +109,10 @@ def goto_eph_seed_menu(goto_home, pick_menu_item, cap_story, need_keypress):
title, story = cap_story()
if title == "WARNING":
assert "temporary secret stored solely in device RAM" in story
assert "Ephemeral seed is a temporary secret completely separate from the master seed" in story
assert "typically held in device RAM" in story
assert "not persisted between reboots in the Secure Element." in story
assert "Enable the Seed Vault feature to store these secrets longer-term." in story
assert "Press (4) to prove you read to the end of this message and accept all consequences." in story
need_keypress("4") # understand consequences
@ -185,7 +188,11 @@ def verify_ephemeral_secret_ui(cap_story, need_keypress, cap_menu, dev, fake_txn
menu = cap_menu()
assert expected_xfp in menu[0] if expected_xfp else True
if expected_xfp:
assert expected_xfp in menu[0]
else:
assert menu[0].startswith("[") # ephemeral xfp
assert menu[1] == "Ready To Sign" # returned to main menu
assert menu[-1] == "Restore Master" # restore main from ephemeral
@ -193,13 +200,13 @@ def verify_ephemeral_secret_ui(cap_story, need_keypress, cap_menu, dev, fake_txn
pick_menu_item("Seed Vault")
time.sleep(.1)
sc_menu = cap_menu()
assert "Restore Master" in sc_menu
item = [i for i in sc_menu if in_effect_xfp in i][0]
pick_menu_item(item)
time.sleep(.1)
m = cap_menu()
assert "Delete" in m
assert "Rename" in m
assert "Restore Master" in m
assert len(m) == 4 # xfp is top item (works same as "Use this seed")
if "Use This Seed" in m:
pick_menu_item("Use This Seed")
@ -230,7 +237,6 @@ def verify_ephemeral_secret_ui(cap_story, need_keypress, cap_menu, dev, fake_txn
time.sleep(.1)
m = cap_menu()
assert item not in m
assert "Restore Master" not in m
else:
# Seed Vault disabled
m = cap_menu()
@ -264,12 +270,6 @@ def verify_ephemeral_secret_ui(cap_story, need_keypress, cap_menu, dev, fake_txn
try_sign(psbt, accept=True, finalize=True) # MUST NOT raise
need_keypress("y")
goto_eph_seed_menu()
time.sleep(0.1)
menu = cap_menu()
# ephemeral seed chosen -> [xfp] will be visible
assert menu[0] == f"[{ident_xfp}]"
restore_main_seed(preserve_settings)
goto_eph_seed_menu()
@ -712,6 +712,7 @@ def test_activate_current_tmp_secret(reset_seed_words, goto_eph_seed_menu,
time.sleep(0.2)
title, story = cap_story()
assert 'ephemeral master key is in effect now' in story
in_effect_xfp = title[1:-1]
need_keypress("y")
goto_eph_seed_menu()
@ -723,6 +724,13 @@ def test_activate_current_tmp_secret(reset_seed_words, goto_eph_seed_menu,
word_menu_entry(words.split())
time.sleep(0.2)
title, story = cap_story()
if seed_vault:
assert "Press (1) to store ephemeral secret into Seed Vault" in story
# do not save
need_keypress("y")
time.sleep(0.2)
title, story = cap_story()
assert "Ephemeral master key already in use" in story
already_used_xfp = title[1:-1]
assert already_used_xfp == in_effect_xfp == expected_xfp
@ -936,7 +944,7 @@ def test_seed_vault_captures(request, dev, settings_set, settings_get, pick_menu
title, story = cap_story()
assert 'New ephemeral master key' in story
assert 'power down' in story
assert 'power down' not in story
assert xfp in title
need_keypress("y") # confirm activation of ephemeral secret
@ -954,4 +962,147 @@ def test_seed_vault_captures(request, dev, settings_set, settings_get, pick_menu
settings_set("seedvault", None)
settings_set("seeds", [])
def test_seed_vault_modifications(settings_set, reset_seed_words, pick_menu_item,
generate_ephemeral_words, import_ephemeral_xprv,
goto_home, cap_story, cap_menu, restore_main_seed,
need_keypress):
reset_seed_words()
settings_set("seedvault", 1)
settings_set("seeds", [])
generate_ephemeral_words(num_words=24, seed_vault=True)
generate_ephemeral_words(num_words=12, seed_vault=True)
import_ephemeral_xprv("sd", seed_vault=True)
import_ephemeral_xprv("sd", seed_vault=True)
goto_home()
pick_menu_item("Seed Vault")
m = cap_menu()
# 4 entries + Restore Master (as we are in ephemeral)
assert len(m) == 5
# restore to main seed
restore_main_seed(seed_vault=True, preserve_settings=True)
time.sleep(.1)
m = cap_menu()
# no ephemeral xfp at the top
assert m[0] == "Ready To Sign"
pick_menu_item("Seed Vault")
# we are no longer in ephemral
assert "Restore Master" not in m
# first entry in menu
need_keypress("y")
m = cap_menu()
assert "Rename" in m
assert "Use This Seed" in m # we are in master - so this must be there
assert "Delete" in m
# delete entry 0
pick_menu_item("Delete")
need_keypress("y")
time.sleep(.1)
m = cap_menu()
assert len(m) == 3
# first entry again
need_keypress("y")
pick_menu_item("Rename")
for _ in range(9):
need_keypress("x")
need_keypress("1") # big letters
need_keypress("9")
need_keypress("1")
# name changed to AA
need_keypress("y")
m = cap_menu()
assert m[0] == "AA"
assert "Rename" in m
assert "Use This Seed" in m # we are in master - so this must be there
assert "Delete" in m
# go back
need_keypress("x")
# second item
need_keypress("8")
need_keypress("y")
time.sleep(.1)
pick_menu_item("Use This Seed")
title, _ = cap_story()
need_keypress("y") # confirm new eph
time.sleep(.1)
m = cap_menu()
assert m[0] == title
pick_menu_item("Seed Vault")
need_keypress("8")
need_keypress("y")
time.sleep(.1)
m = cap_menu()
assert "Rename" in m
assert "In Use" in m
assert "Delete" in m
pick_menu_item("Rename")
for _ in range(9):
need_keypress("x")
need_keypress("1") # big letters
need_keypress("9")
need_keypress("1")
need_keypress("9")
need_keypress("1")
# name changed to AAA
need_keypress("y")
time.sleep(.1)
m = cap_menu()
assert m[0] == "AAA"
pick_menu_item("Delete")
need_keypress("y")
time.sleep(.1)
m = cap_menu()
# after we delete from seed vault together with its settings
# we're back to master secret
assert m[0] == "Ready To Sign"
pick_menu_item("Seed Vault")
time.sleep(.1)
m = cap_menu()
assert len(m) == 2
need_keypress("8")
need_keypress("y")
pick_menu_item("Use This Seed")
title, _ = cap_story()
need_keypress("y") # confirm new eph
time.sleep(.1)
m = cap_menu()
assert m[0] == title
pick_menu_item("Seed Vault")
need_keypress("8")
need_keypress("y")
time.sleep(.1)
m = cap_menu()
assert "Rename" in m
assert "In Use" in m
assert "Delete" in m
pick_menu_item("Delete")
need_keypress("1") # only delete from seed vault
time.sleep(.1)
m = cap_menu()
assert len(m) == 2
need_keypress("y")
# this is now different eph - modification not allowed
time.sleep(.1)
m = cap_menu()
assert "Rename" not in m
assert "Delete" not in m
assert "Use This Seed" in m
goto_home()
time.sleep(.1)
m = cap_menu()
# still in ephemeral
assert title == m[0]
# EOF

View File

@ -127,7 +127,7 @@ def restore_seed_xor(set_seed_words, goto_home, pick_menu_item, cap_story,
time.sleep(0.01)
title, body = cap_story()
assert 'New ephemeral master key in effect' in body
assert 'New ephemeral master key is in effect now' in body
need_keypress("y")
assert get_secrets()['mnemonic'] == expect