Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e848559214 | ||
|
|
0c80cb3547 | ||
|
|
a9a0b58308 | ||
|
|
e4a3ef1fc4 | ||
|
|
45fac102e7 | ||
|
|
76a379307c | ||
|
|
233b719c5c | ||
|
|
4bad3d8167 | ||
|
|
eaca1ef07f | ||
|
|
7c491ccc18 | ||
|
|
02233b6b12 | ||
|
|
21c9952641 |
24
docs/ephemeral.md
Normal file
24
docs/ephemeral.md
Normal 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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
183
shared/seed.py
183
shared/seed.py
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
187
testing/test_ephemeral.py
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user