Compare commits

...

12 Commits

Author SHA1 Message Date
Peter D. Gray
e848559214
add nfc, import tests 2022-10-04 11:27:06 -04:00
Peter D. Gray
0c80cb3547
updates, add dice test 2022-10-04 10:49:24 -04:00
Peter D. Gray
a9a0b58308
updated 2022-10-04 10:30:12 -04:00
Peter D. Gray
e4a3ef1fc4
copy changes 2022-10-04 10:22:34 -04:00
Peter D. Gray
45fac102e7
warn before backup 2022-10-04 10:19:10 -04:00
Peter D. Gray
76a379307c
remove clear eph seed option (broken, not useful) 2022-10-04 10:07:48 -04:00
Peter D. Gray
233b719c5c
because no-one every uses the middle choice 2022-10-04 09:58:04 -04:00
Peter D. Gray
4bad3d8167
reorder 2022-10-04 09:43:12 -04:00
Peter D. Gray
eaca1ef07f
edits 2022-10-04 09:43:03 -04:00
Peter D. Gray
7c491ccc18
speelign 2022-10-04 09:42:54 -04:00
Peter D. Gray
02233b6b12
Merge branch 'ephemeral_seeds' of github.com:scgbckbone/firmware into eph-seeds 2022-10-04 09:36:16 -04:00
scgbckbone
21c9952641
Ephemeral seeds 2022-09-20 13:34:16 +02:00
11 changed files with 435 additions and 72 deletions

24
docs/ephemeral.md Normal file
View File

@ -0,0 +1,24 @@
# Ephemeral Seeds
Ephemeral seed is temporary secret stored only in Coldcard volatile
memory (RAM). It only survives single boot, meaning after Coldcard
restart it is gone. Ephemeral seeds *completely* defeats the design
of Coldcard's security model based on secure elements.
Make sure you know what you're doing!
## Usage
- go to `Advanced/Tools -> Ephemeral Seed`
- if ephemeral seed is already in use, the menu option `CLEAR [<xfp>]` is visible
with fingerprint of ephemeral master secret
- an ephemeral seed can be Imported or Generated at random
- `Generate`:
- `Advanced/Tools -> Ephemeral Seed -> Generate`
- same options as generating new seeds, dice rolls included
- `Import`:
- `Advanced/Tools -> Ephemeral Seed -> Import`
- same options as importing seeds
- an ephemeral seed can also be a BIP-85 derived value

View File

@ -1,5 +1,6 @@
## 5.0.7 - 2022-09-1X
## 5.0.7 - 2022-10-06
- Enhancement: Ephemeral Seeds: Advanced/Tools > Ephemeral Seed (more info in `docs/ephemeral.md`)
- Enhancement: In older versions, multisig NFC import not offered if a MicroSD card was
inserted, now this option provided Settings > Multisig Wallets > Import via NFC. NFC has
to be enabled for this option to be visible in the menu.

View File

@ -102,6 +102,9 @@ Extended Master Key:
if stash.bip39_passphrase:
msg += '\nBIP-39 passphrase is in effect.\n'
if pa.tmp_value:
msg += '\nEphemeral seed is in effect.\n'
bn = callgate.get_bag_number()
if bn:
msg += '\nShipping Bag:\n %s\n' % bn
@ -523,10 +526,6 @@ More on our website:
.com
""")
async def start_seed_import(menu, label, item):
import seed
return seed.WordNestMenu(item.arg)
async def start_b39_pw(menu, label, item):
if not settings.get('b39skip', False):
ch = await ux_show_story('''\
@ -559,22 +558,17 @@ X to go back. Or press 2 to hide this message forever.
import seed
return seed.PassphraseMenu()
def pick_new_seed_24(*a):
async def start_seed_import(menu, label, item):
import seed
return seed.make_new_wallet(24)
return seed.WordNestMenu(item.arg)
def pick_new_seed_12(*a):
def pick_new_seed(menu, label, item):
import seed
return seed.make_new_wallet(12)
return seed.make_new_wallet(item.arg)
def new_from_dice_24(*a):
def new_from_dice(menu, label, item):
import seed
return seed.new_from_dice(24)
def new_from_dice_12(*a):
import seed
return seed.new_from_dice(12)
return seed.new_from_dice(item.arg)
async def convert_bip39_to_bip32(*a):
import seed, stash
@ -647,7 +641,7 @@ def render_master_secrets(mode, raw, node):
pw = stash.bip39_passphrase
if pw:
msg += '\n\nBIP-39 Passphrase:\n%s' % stash.bip39_passphrase
msg += '\n\nBIP-39 Passphrase:\n%s' % pw
elif mode == 'xprv':
import chains
msg = chains.current_chain().serialize_private(node)
@ -1325,6 +1319,13 @@ async def nfc_share_file(*A):
await NFC.share_file()
async def nfc_recv_ephemeral(*A):
# Mk4: Share txt, txn and PSBT files over NFC.
from glob import NFC
if NFC:
await NFC.import_ephemeral_seed_words_nfc()
async def list_files(*A):
# list files, don't do anything with them?
fn = await file_picker('Lists all files, select one and SHA256(file contents) will be shown.', min_size=0)

View File

@ -216,6 +216,10 @@ async def restore_from_dict(vals):
async def make_complete_backup(fname_pattern='backup.7z', write_sflash=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)

View File

@ -6,12 +6,12 @@
# Using the system's BIP-32 master key, safely derive seeds phrases/entropy for other
# wallet systems, which may expect seed phrases, XPRV, or other entropy.
#
import stash, ngu, chains, bip39, version, glob
import stash, seed, ngu, chains, bip39, version, glob
from ux import ux_show_story, ux_enter_number, the_ux, ux_confirm, ux_dramatic_pause
from menu import MenuItem, MenuSystem
from ubinascii import hexlify as b2a_hex
from ubinascii import b2a_base64
from serializations import hash160
BIP85_PWD_LEN = 21
@ -239,10 +239,7 @@ async def drv_entro_step2(_1, picked, _2):
# switch over to new secret!
dis.fullscreen("Applying...")
pa.tmp_secret(encoded)
await ux_show_story("New master key in effect until next power down.")
await seed.set_ephemeral_seed(encoded)
if encoded is not None:
stash.blank_object(encoded)

View File

@ -9,6 +9,7 @@ from glob import settings
from actions import *
from choosers import *
from multisig import make_multisig_menu
from seed import make_ephemeral_seed_menu
from address_explorer import address_explore
from users import make_users_menu
from drv_entro import drv_entro_start, password_entry
@ -210,6 +211,7 @@ else:
AdvancedVirginMenu = [ # No PIN, no secrets yet (factory fresh)
# xxxxxxxxxxxxxxxx
MenuItem("View Identity", f=view_ident),
MenuItem("Ephemeral Seed", menu=make_ephemeral_seed_menu),
MenuItem('Upgrade Firmware', menu=UpgradeMenu),
MenuItem('Paper Wallets', f=make_paper_wallet, predicate=lambda: make_paper_wallet),
MenuItem('Perform Selftest', f=start_selftest),
@ -219,6 +221,7 @@ AdvancedVirginMenu = [ # No PIN, no secrets yet (factory fresh)
AdvancedPinnedVirginMenu = [ # Has PIN but no secrets yet
# xxxxxxxxxxxxxxxx
MenuItem("View Identity", f=view_ident),
MenuItem("Ephemeral Seed", menu=make_ephemeral_seed_menu),
MenuItem("Upgrade Firmware", menu=UpgradeMenu),
MenuItem("File Management", menu=FileMgmtMenu),
MenuItem('Paper Wallets', f=make_paper_wallet, predicate=lambda: make_paper_wallet),
@ -282,6 +285,7 @@ AdvancedNormalMenu = [
MenuItem("File Management", menu=FileMgmtMenu),
MenuItem('Derive Seed B85', f=drv_entro_start),
MenuItem("View Identity", f=view_ident),
MenuItem("Ephemeral Seed", menu=make_ephemeral_seed_menu),
MenuItem('Paper Wallets', f=make_paper_wallet, predicate=lambda: make_paper_wallet),
ToggleMenuItem('Enable HSM', 'hsmcmd', ['Default Off', 'Enable'],
story="Enable HSM? Enables all user management commands, and other HSM-only USB commands. \
@ -314,10 +318,10 @@ ImportWallet = [
NewSeedMenu = [
# xxxxxxxxxxxxxxxx
MenuItem("24 Word (default)", f=pick_new_seed_24),
MenuItem("12 Word", f=pick_new_seed_12),
MenuItem("24 Word Dice Roll", f=new_from_dice_24),
MenuItem("12 Word Dice Roll", f=new_from_dice_12),
MenuItem("24 Word (default)", f=pick_new_seed, arg=24),
MenuItem("12 Word", f=pick_new_seed, arg=12),
MenuItem("24 Word Dice Roll", f=new_from_dice, arg=24),
MenuItem("12 Word Dice Roll", f=new_from_dice, arg=12),
]
# has PIN, but no secret seed yet

View File

@ -7,7 +7,7 @@
# - has GPIO signal "??" which is multipurpose on its own pin
# - this chip chosen because it can disable RF interaction
#
import ngu, ckcc, utime, ngu, ndef
import ngu, utime, ngu, ndef
from uasyncio import sleep_ms
from utils import B2A, problem_file_line
from ustruct import pack, unpack
@ -558,4 +558,30 @@ class NFCHandler:
#import sys; sys.print_exception(e)
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
async def import_ephemeral_seed_words_nfc(self, *a):
data = await self.start_nfc_rx()
if not data: return
winner = None
try:
for urn, msg, meta in ndef.record_parser(data):
msg = bytes(msg).decode().strip() # from memory view
split_msg = msg.split(" ")
if len(split_msg) in (12, 18, 24):
winner = split_msg
break
except Exception:
pass
if not winner:
await ux_show_story('Unable to find data expected in NDEF')
return
try:
from seed import set_ephemeral_seed_words
await set_ephemeral_seed_words(winner)
except Exception as e:
#import sys; sys.print_exception(e)
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
# EOF

View File

@ -21,7 +21,7 @@ from actions import goto_top_menu
from stash import SecretStash, SensitiveValues
from ubinascii import hexlify as b2a_hex
from pwsave import PassphraseSaver
from glob import settings
from glob import settings, dis
from pincodes import pa
# seed words lengths we support: 24=>256 bits, and recommended
@ -267,10 +267,14 @@ individual words if you wish.''')
dis.text(-18-(6 if count >= 10 else 0), y, self.tr_label(), FontTiny, invert=invert)
async def show_words(words, prompt=None, escape=None, extra=''):
async def show_words(words, prompt=None, escape=None, extra='', ephemeral=False):
msg = (prompt or 'Record these %d secret words!\n') % len(words)
msg += '\n'.join('%2d: %s' % (i+1, w) for i,w in enumerate(words))
msg += '\n\nPlease check and double check your notes. There will be a test!'
msg += '\n'.join('%2d: %s' % (i, w) for i, w in enumerate(words, start=1))
msg += '\n\nPlease check and double check your notes.'
if not ephemeral:
# user can skip quiz for ephemeral secrets
msg += " There will be a test!"
if version.has_fatram:
escape = (escape or '') + '1'
@ -356,26 +360,73 @@ async def new_from_dice(nwords):
count = 0
count, seed = await add_dice_rolls(count, seed, True)
if count == 0: return
await approve_word_list(seed, nwords)
words = await approve_word_list(seed, nwords)
if words:
set_seed_value(words)
# send them to home menu, now with a wallet enabled
goto_top_menu(first_time=True)
async def set_ephemeral_seed(encoded):
pa.tmp_secret(encoded)
xfp = settings.get("xfp", "")
if xfp:
xfp = "[" + xfp2str(xfp) + "]\n"
await ux_show_story("%sNew ephemeral master key in effect until next power down.\n\nIt is NOT stored anywhere." % xfp)
async def set_ephemeral_seed_words(words):
encoded = seed_words_to_encoded_secret(words)
await set_ephemeral_seed(encoded)
goto_top_menu()
async def ephemeral_seed_generate_from_dice(nwords):
# Use lots of (D6) dice rolls to create seed entropy.
# Note: only 2.585 bits of entropy per roll, so need lots!
# 50 => 128bits, 99 => 256bits
seed = b''
count = 0
count, seed = await add_dice_rolls(count, seed, True)
if count == 0: return
words = await approve_word_list(seed, nwords, ephemeral=True)
if words:
await set_ephemeral_seed_words(words)
def generate_seed():
seed = random.bytes(32)
assert len(set(seed)) > 4 # TRNG failure
# hash to mitigate possible bias in TRNG
seed = ngu.hash.sha256s(seed)
return seed
async def make_new_wallet(nwords):
# Pick a new random seed.
await ux_dramatic_pause('Generating...', 3)
seed = generate_seed()
words = await approve_word_list(seed, nwords)
if words:
set_seed_value(words)
# send them to home menu, now with a wallet enabled
goto_top_menu(first_time=True)
# starting point
seed = random.bytes(32)
assert len(set(seed)) > 4 # TRNG failure
async def ephemeral_seed_import_done_cb(words):
dis.fullscreen("Applying...")
await set_ephemeral_seed_words(words)
# hash to mitigate possible bias in TRNG
seed = ngu.hash.sha256s(seed)
async def ephemeral_seed_import(nwords):
return WordNestMenu(nwords, done_cb=ephemeral_seed_import_done_cb)
await approve_word_list(seed, nwords)
async def ephemeral_seed_generate(nwords):
await ux_dramatic_pause('Generating...', 3)
seed = generate_seed()
words = await approve_word_list(seed, nwords, ephemeral=True)
if words:
await set_ephemeral_seed_words(words)
async def approve_word_list(seed, nwords):
async def approve_word_list(seed, nwords, ephemeral=False):
# Force the user to write the seeds words down, give a quiz, then save them.
# LESSON LEARNED: if the user is writting down the words, as we have
@ -387,12 +438,14 @@ async def approve_word_list(seed, nwords):
words = bip39.b2a_words(seed).split(' ')
assert len(words) == nwords
extra_msg = 'Press 4 to add some dice rolls into the mix. '
if ephemeral:
# document quiz skipping if generating ephemeral seed
extra_msg += "Press 6 to skip word quiz. "
while 1:
# show the seed words
ch = await show_words(words, escape='46',
extra='Press 4 to add some dice rolls into the mix. ')
ch = await show_words(words, escape='46', extra=extra_msg, ephemeral=ephemeral)
if ch == 'x':
# user abort, but confirm it!
if await ux_confirm("Throw away those words and stop this process?"):
@ -428,37 +481,20 @@ async def approve_word_list(seed, nwords):
# quiz passed
break
# Done!
set_seed_value(words)
return words
# send them to home menu, now with a wallet enabled
goto_top_menu(first_time=True)
def seed_words_to_encoded_secret(words):
# seed without checksum
seed = bip39.a2b_words(words) # checksum check
# encode it for our limited secret space
nv = SecretStash.encode(seed_phrase=seed)
return nv
def set_seed_value(words=None, encoded=None):
# Save the seed words into secure element, and reboot. BIP-39 password
# is not set at this point (empty string)
if words:
bip39.a2b_words(words) # checksum check
# map words to bip39 wordlist indices
data = [bip39.wordlist_en.index(w) for w in words]
# map to packed binary representation.
val = 0
for v in data:
val <<= 11
val |= v
# remove the checksum part
vlen = (len(words) * 4) // 3
val >>= (len(words) // 3)
# convert to bytes
seed = val.to_bytes(vlen, 'big')
assert len(seed) == vlen
# encode it for our limited secret space
nv = SecretStash.encode(seed_phrase=seed)
nv = seed_words_to_encoded_secret(words)
else:
nv = encoded
@ -616,6 +652,69 @@ async def word_quiz(words, limited=None, title='Word %d is?'):
return
class EphemeralSeedMenu(MenuSystem):
@staticmethod
async def ephemeral_seed_import(menu, label, item):
return await ephemeral_seed_import(item.arg)
@staticmethod
async def ephemeral_seed_generate(menu, label, item):
return await ephemeral_seed_generate(item.arg)
@staticmethod
async def ephemeral_seed_generate_from_dice(menu, label, item):
return await ephemeral_seed_generate_from_dice(item.arg)
@classmethod
def construct(cls):
from glob import NFC, settings
from actions import nfc_recv_ephemeral
import_ephemeral_menu = [
MenuItem("12 Words", f=cls.ephemeral_seed_import, arg=12),
MenuItem("18 Words", f=cls.ephemeral_seed_import, arg=18),
MenuItem("24 Words", f=cls.ephemeral_seed_import, arg=24),
MenuItem("Import via NFC", f=nfc_recv_ephemeral, predicate=lambda: NFC is not None),
]
gen_ephemeral_menu = [
MenuItem("12 Words", f=cls.ephemeral_seed_generate, arg=12),
MenuItem("24 Words", f=cls.ephemeral_seed_generate, arg=24),
MenuItem("12 Word Dice Roll", f=cls.ephemeral_seed_generate_from_dice, arg=12),
MenuItem("24 Word Dice Roll", f=cls.ephemeral_seed_generate_from_dice, arg=24),
]
rv = [
MenuItem("Generate Seed", menu=gen_ephemeral_menu),
MenuItem("Import Seed", menu=import_ephemeral_menu),
]
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:
# force a warning on them, unless they are already doing it.
ch = await ux_show_story(
"Ephemeral seed is a temporary secret stored solely in device RAM, persisted for only a single boot. "
"This defeats all of the benefits of Coldcard's secure element design."
"\n\nPress 4 to prove you read to the end of this message and accept all consequences.",
title="WARNING",
escape="4"
)
if ch != "4":
return
rv = EphemeralSeedMenu.construct()
return EphemeralSeedMenu(rv)
pp_sofar = ''
class PassphraseMenu(MenuSystem):

View File

@ -1314,6 +1314,19 @@ def nfc_write(request, only_mk4):
except:
return doit_usb
def ccfile_wrap(recs):
CC_FILE = bytes([0xE2, 0x43, 0x00, 0x01, 0x00, 0x00, 0x04, 0x00, 0x03])
assert len(recs) < 255 # code limitation here
return CC_FILE + bytes([len(recs)]) + recs + b'\xfe'
@pytest.fixture()
def nfc_write_text(nfc_write):
def doit(text):
msg = b''.join(ndef.message_encoder([ndef.TextRecord(text), ]))
return nfc_write(ccfile_wrap(msg))
return doit
@pytest.fixture()
def nfc_read_json(nfc_read):
def doit():
@ -1366,4 +1379,4 @@ from test_multisig import (import_ms_wallet, make_multisig, offer_ms_import, fak
make_ms_address, clear_ms, make_myself_wallet)
from test_bip39pw import set_bip39_pw, clear_bip39_pw
#EOF
# EOF

View File

@ -153,9 +153,16 @@ def test_bip_vectors(mode, index, entropy, expect,
time.sleep(0.1)
need_keypress('2')
if 0: # screen was removed
time.sleep(0.1)
title, story = cap_story()
assert title == "WARNING"
assert 'Press 4 to prove you read to the end of this message and accept all consequences.' in story
need_keypress("4")
time.sleep(0.1)
title, story = cap_story()
assert 'New master key in effect' in story
assert 'master key in effect' in story
encoded = sim_exec('from pincodes import pa; RV.write(repr(pa.fetch()))')
print(encoded)

187
testing/test_ephemeral.py Normal file
View File

@ -0,0 +1,187 @@
# (c) Copyright 2022 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# Ephemeral Seeds tests
#
import pytest, time, re
from constants import simulator_fixed_words, simulator_fixed_xfp, simulator_fixed_xpub
from helpers import xfp2str
from txn import fake_txn
from ckcc.protocol import CCProtocolPacker
from test_ux import word_menu_entry, pass_word_quiz
def seed_story_to_words(story: str):
# filter those that starts with space, number and colon --> actual words
words = [
line.strip().split(":")[1].strip()
for line in story.split("\n")
if re.search(r"\s\d:", line) or re.search(r"\d{2}:", line)
]
return words
@pytest.fixture
def get_seed_value_ux(goto_home, pick_menu_item, need_keypress, cap_story):
def doit():
goto_home()
pick_menu_item("Advanced/Tools")
pick_menu_item("Danger Zone")
pick_menu_item("Seed Functions")
pick_menu_item('View Seed Words')
time.sleep(.01)
title, body = cap_story()
assert 'Are you SURE' in body
assert 'can control all funds' in body
need_keypress('y') # skip warning
time.sleep(0.01)
title, story = cap_story()
words = seed_story_to_words(story)
return words
return doit
@pytest.fixture
def get_identity_story(goto_home, pick_menu_item, cap_story):
def doit():
goto_home()
pick_menu_item("Advanced/Tools")
pick_menu_item("View Identity")
time.sleep(0.1)
title, story = cap_story()
return story
return doit
@pytest.fixture
def goto_eph_seed_menu(goto_home, pick_menu_item, cap_story, need_keypress):
def doit():
goto_home()
pick_menu_item("Advanced/Tools")
pick_menu_item("Ephemeral Seed")
title, story = cap_story()
if title == "WARNING":
assert "temporary secret stored solely in device RAM" 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
return doit
@pytest.mark.parametrize("num_words", [12, 24])
@pytest.mark.parametrize("dice", [False, True])
def test_ephemeral_seed_generate(num_words, cap_menu, pick_menu_item, goto_home, cap_story, need_keypress,
reset_seed_words, get_seed_value_ux, get_identity_story, fake_txn, dev, try_sign, goto_eph_seed_menu, dice):
reset_seed_words()
goto_eph_seed_menu()
menu = cap_menu()
# no ephemeral seed chosen (yet)
assert len(menu) == 2
pick_menu_item("Generate Seed")
if not dice:
pick_menu_item(f"{num_words} Words")
time.sleep(0.1)
else:
pick_menu_item(f"{num_words} Word Dice Roll")
for ch in '123456yy':
need_keypress(ch)
title, story = cap_story()
assert f"Record these {num_words} secret words!" in story
assert "Press 6 to skip word quiz" in story
# filter those that starts with space, number and colon --> actual words
e_seed_words = seed_story_to_words(story)
assert len(e_seed_words) == num_words
need_keypress("6") # skip quiz
need_keypress("y") # yes - I'm sure
time.sleep(0.1)
need_keypress("4") # understand consequences
time.sleep(0.1)
title, story = cap_story()
in_effect_xfp = story[1:9]
assert "key in effect until next power down." in story
need_keypress("y") # just confirm new master key message
menu = cap_menu()
assert menu[0] == "Ready To Sign" # returned to main menu
seed_words = get_seed_value_ux()
assert e_seed_words == seed_words
ident_story = get_identity_story()
assert "Ephemeral seed is in effect" in ident_story
ident_xfp = ident_story.split("\n\n")[1].strip()
assert ident_xfp == in_effect_xfp
e_master_xpub = dev.send_recv(CCProtocolPacker.get_xpub(), timeout=5000)
assert e_master_xpub != simulator_fixed_xpub
psbt = fake_txn(3, 3, master_xpub=e_master_xpub, segwit_in=True)
try_sign(psbt, accept=True, finalize=True) # MUST NOT raise
goto_home()
pick_menu_item("Advanced/Tools")
pick_menu_item("Ephemeral Seed")
menu = cap_menu()
# ephemeral seed chosen -> [xfp] will be visible
assert len(menu) == 3
assert menu[0] == f"[{ident_xfp}]"
reset_seed_words()
goto_eph_seed_menu()
menu = cap_menu()
assert len(menu) == 2
@pytest.mark.parametrize("num_words", [12, 18, 24])
@pytest.mark.parametrize("nfc", [False, True])
@pytest.mark.parametrize("truncated", [False,]) # needs libngu upgrade to bip39.py
def test_ephemeral_seed_import(nfc, num_words, cap_menu, pick_menu_item, goto_home, cap_story,
need_keypress, reset_seed_words, get_seed_value_ux, get_identity_story, fake_txn,
dev, try_sign, goto_eph_seed_menu, word_menu_entry, nfc_write_text, truncated
):
if truncated and not nfc: return
wordlists = {
12: ( 'abandon ' * 11 + 'about', 0x0adac573),
18: ( 'abandon ' * 17 + 'agent', 0xc38a8be0),
24: ( 'abandon ' * 23 + 'art', 0x24d73654 ),
}
words, expect_xfp = wordlists[num_words]
reset_seed_words()
goto_eph_seed_menu()
menu = cap_menu()
# no ephemeral seed chosen (yet)
assert len(menu) == 2
pick_menu_item("Import Seed")
if not nfc:
pick_menu_item(f"{num_words} Words")
time.sleep(0.1)
word_menu_entry(words.split())
else:
menu = cap_menu()
if 'Import via NFC' not in menu:
raise pytest.xfail("NFC not enabled")
pick_menu_item('Import via NFC')
if truncated:
words = ' '.join(w[0:4] for w in words.split())
nfc_write_text(words)
need_keypress("4") # understand consequences
time.sleep(0.4)
title, story = cap_story()
in_effect_xfp = story[1:9]
assert "key in effect until next power down." in story
need_keypress("y") # just confirm new master key message
# EOF