Main seed and ephemeral seed from Tapsigner backup + ephemeral seed from extended private key

This commit is contained in:
scgbckbone 2023-02-15 17:50:22 +01:00 committed by doc-hex
parent d6193d5636
commit 307df0c013
15 changed files with 636 additions and 187 deletions

2
.gitignore vendored
View File

@ -24,3 +24,5 @@ __pycache__/
.tags
pp
.idea/

View File

@ -11,15 +11,19 @@ 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
- if ephemeral seed is already in use, top menu item `[<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 be Imported or Generated at random
- go to `Advanced/Tools -> Ephemeral Seed`
- `Generate Words`:
- same options as generating new seed words, dice rolls included
- Import words via NFC with `Import via NFC` option
- `Import Words`:
- same options as importing seed words
- `Import XPRV`:
- import extended private key
- `Tapsigner Backup`
- import TAPSIGNER encrypted backup
- an ephemeral seed can also be a BIP-85 derived value
## Trick PIN Notes

View File

@ -1,5 +1,6 @@
## 5.1.0 - 2023-02-XX
- New Feature: Load TAPSIGNER encrypted backup as main or ephemeral seed
- New Feature: "MicroSD card as Second Factor". Specially marked MicroSD card must be
already inserted when (true) PIN is entered, or else seed is wiped. Add, remove and check
cards in menu: `Settings -> Login Settings -> MicroSD 2FA`
@ -13,6 +14,7 @@
- Add import multisig wallet via Virtual Disk
- Add import extended private key via Virtual Disk and via NFC
- Import seed in compact/truncated form (just 3-4 letters of each seed word)
- Import extended private key as ephemeral seed
- Export Enhancements:
- Samourai POST-MIX and PRE-MIX descriptor export options added
- Lily Wallet added

View File

@ -5,13 +5,14 @@
# Every function here is called directly by a menu item. They should all be async.
#
import ckcc, pyb, version, uasyncio, sys
from ux import ux_show_story, the_ux, ux_confirm, ux_dramatic_pause, ux_aborted, ux_enter_bip32_index, ux_input_text
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
from utils import imported, pretty_short_delay, problem_file_line, import_prompt_builder
from utils import xfp2str, decrypt_tapsigner_backup
from uasyncio import sleep_ms
from ubinascii import hexlify as b2a_hex
from files import CardSlot, CardMissingError, needs_microsd
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, MAX_TXN_LEN_MK4
from utils import xfp2str
from multisig import parse_extended_key
from glob import settings
from pincodes import pa
from menu import start_chooser
@ -647,8 +648,6 @@ def render_master_secrets(mode, raw, node):
qr = msg
elif mode == 'master':
from ubinascii import hexlify as b2a_hex
msg = '%d bytes:\n\n' % len(raw)
qr = str(b2a_hex(raw), 'ascii')
msg += qr
@ -1281,14 +1280,30 @@ async def verify_backup(*A):
# do a limited CRC-check over encrypted file
await backups.verify_backup_file(fn)
async def import_xprv(*A):
# read an XPRV from a text file and use it.
from stash import SecretStash
from glob import NFC
from ubinascii import hexlify as b2a_hex
from backups import restore_from_dict
async def import_extended_key_as_secret(extended_key, ephemeral):
try:
import seed
if ephemeral:
await seed.set_ephemeral_seed_extended_key(extended_key)
else:
await seed.set_seed_extended_key(extended_key)
except ValueError:
msg = ("Sorry, wasn't able to find a valid extended private key to import. "
"It should be at the start of a line, and probably starts with 'xprv'.")
await ux_show_story(title="FAILED", msg=msg)
except Exception as e:
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
assert pa.is_secret_blank() # "must not have secret"
async def import_xprv(_1, _2, item):
# read an XPRV from a text file and use it.
from glob import NFC
extended_key = None
label = "extended private key"
ephemeral = item.arg
if not ephemeral:
assert pa.is_secret_blank() # "must not have secret"
def contains_xprv(fname):
# just check if likely to be valid; not full check
@ -1303,13 +1318,12 @@ async def import_xprv(*A):
return False
force_vdisk = False
prompt, escape = import_prompt_builder("extended private key file")
prompt, escape = import_prompt_builder("%s file" % label)
if prompt:
ch = await ux_show_story(prompt, escape=escape)
if ch == "3":
force_vdisk = None
extended_key = await NFC.read_extended_private_key()
node, chain, addr_fmt = parse_extended_key(extended_key, private=True)
elif ch == "2":
force_vdisk = True
elif ch == "1":
@ -1320,38 +1334,19 @@ async def import_xprv(*A):
if force_vdisk is not None:
# only get here if NFC was not chosen
# pick a likely-looking file.
fn = await file_picker('Select file containing the XPRV to be imported.', min_size=50, max_size=2000,
taster=contains_xprv, force_vdisk=force_vdisk)
fn = await file_picker('Select file containing the %s to be imported.' % label, min_size=50,
max_size=2000, taster=contains_xprv, force_vdisk=force_vdisk)
if not fn: return
node = None
with CardSlot(force_vdisk=force_vdisk, readonly=True) as card:
with open(fn, 'rt') as fd:
for ln in fd.readlines():
if 'prv' not in ln: continue
node, chain, addr_fmt = parse_extended_key(ln, private=True)
if node: break
if not node:
# unable
await ux_show_story('''\
Sorry, wasn't able to find a valid extended private key to import. It should be at \
the start of a line, and probably starts with "xprv".''', title="FAILED")
return
# encode it in our style
d = dict(chain=chain.ctype, raw_secret=b2a_hex(SecretStash.encode(xprv=node)))
node.blank()
# Should capture the address format implied by SLIP32 version bytes
# (addr_fmt var here) but no means to store that in our settings, and we're
# not supposed to care anyway.
# TODO: would be nice for addr explorer tho
# restore as if it was a backup (code reuse)
await restore_from_dict(d)
if 'prv' in ln:
extended_key = ln
break
await import_extended_key_as_secret(extended_key, ephemeral)
# not reached; will do reset.
EMPTY_RESTORE_MSG = '''\
@ -1455,6 +1450,57 @@ async def nfc_recv_ephemeral(*A):
await ux_show_story(title="ERROR", msg="Failed to import ephemeral seed via NFC. %s" % str(e))
async def import_tapsigner_backup_file(_1, _2, item):
from glob import NFC
ephemeral = item.arg
if not ephemeral:
assert pa.is_secret_blank() # "must not have secret"
force_vdisk = False
label = "TAPSIGNER encrypted backup file"
prompt, escape = import_prompt_builder(label)
if prompt:
ch = await ux_show_story(prompt, escape=escape)
if ch == "3":
force_vdisk = None
data = await NFC.read_tapsigner_b64_backup()
elif ch == "2":
force_vdisk = True
elif ch == "1":
force_vdisk = False
else:
return
if force_vdisk is not None:
fn = await file_picker('Pick ' + label, suffix="aes", min_size=100, max_size=160,
force_vdisk=force_vdisk)
if not fn: return
with CardSlot(force_vdisk=force_vdisk) as card:
with open(fn, 'rb') as fp:
data = fp.read()
if await ux_show_story("Make sure to have your TAPSIGNER handy as you will need to provide "
"'Backup Password' from the back of the card in the next step. "
"Press OK to continue X to cancel.") != "y":
return
while True:
backup_key = await ux_input_text("", confirm_exit=False, hex_only=True, max_len=32)
if backup_key is None:
return
if len(backup_key) != 32:
await ux_show_story(title="FAILURE", msg="'Backup Key' length != 32")
continue
try:
extended_key, derivation = decrypt_tapsigner_backup(backup_key, data)
break
except ValueError as e:
await ux_show_story(title="FAILURE", msg=str(e))
continue
await import_extended_key_as_secret(extended_key, ephemeral)
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)
@ -1913,7 +1959,6 @@ We strongly recommend all PIN codes used be unique between each other.
async def show_version(*a):
# show firmware, bootload versions.
import callgate, version
from ubinascii import hexlify as b2a_hex
from glob import NFC
built, rel, *_ = version.get_mpy_version()

View File

@ -329,7 +329,8 @@ ImportWallet = [
MenuItem("12 Words", menu=start_seed_import, arg=12),
MenuItem("Restore Backup", f=restore_everything),
MenuItem("Clone Coldcard", menu=clone_start),
MenuItem("Import XPRV", f=import_xprv),
MenuItem("Import XPRV", f=import_xprv, arg=False), # ephemeral=False
MenuItem("Tapsigner Backup", f=import_tapsigner_backup_file, arg=False),
MenuItem("Seed XOR", f=xor_restore_start),
]

View File

@ -4,7 +4,7 @@
#
import stash, chains, ustruct, ure, uio, sys, ngu, uos, ujson
from utils import xfp2str, str2xfp, swab32, cleanup_deriv_path, keypath_to_str
from utils import str_to_keypath, problem_file_line, export_prompt_builder
from utils import str_to_keypath, problem_file_line, export_prompt_builder, parse_extended_key
from ux import ux_show_story, ux_confirm, ux_dramatic_pause, ux_clear_keys, ux_enter_bip32_index
from files import CardSlot, CardMissingError, needs_microsd
from descriptor import MultisigDescriptor, multisig_descriptor_template
@ -1446,28 +1446,6 @@ OK to continue. X to abort.'''.format(coin = chain.b44_cointype)
msg = '''Multisig XPUB file written:\n\n%s''' % nice
await ux_show_story(msg)
def parse_extended_key(ln, private=False):
# read an xpub/ypub/etc and return BIP-32 node and what chain it's on.
# - can handle any garbage line
# - returns (node, chain, addr_fmt)
# - people are using SLIP132 so we need this
ln = ln.strip()
node, chain, addr_fmt = None, None, None
if private:
rgx = r'.prv[A-Za-z0-9]+'
else:
rgx = r'.pub[A-Za-z0-9]+'
pat = ure.compile(rgx)
found = pat.search(ln)
# serialize, and note version code
try:
node, chain, addr_fmt, is_private = chains.slip32_deserialize(found.group(0))
except:
pass
return node, chain, addr_fmt
async def ondevice_multisig_create(mode='p2wsh', addr_fmt=AF_P2WSH, force_vdisk=False):
# collect all xpub- exports on current SD card (must be >= 1) to make "air gapped" wallet
# - ask for M value

View File

@ -11,7 +11,7 @@ import ngu, utime, ngu, ndef
from uasyncio import sleep_ms
from ustruct import pack, unpack
from ubinascii import unhexlify as a2b_hex
from ubinascii import b2a_base64
from ubinascii import b2a_base64, a2b_base64
from ux import ux_show_story, ux_poll_key
from utils import cleanup_deriv_path, B2A, problem_file_line, parse_addr_fmt_str
@ -700,4 +700,26 @@ class NFCHandler:
return winner
async def read_tapsigner_b64_backup(self):
data = await self.start_nfc_rx()
if not data:
await ux_show_story('Unable to find data expected in NDEF')
return
winner = None
for urn, msg, meta in ndef.record_parser(data):
msg = bytes(msg).decode() # from memory view
try:
if 150 <= len(msg) <= 280:
winner = a2b_base64(msg)
break
except:
pass
if not winner:
await ux_show_story('Unable to find base64 encoded TAPSIGNER backup in NDEF data')
return
return winner
# EOF

View File

@ -445,7 +445,7 @@ class PinAttempt:
# does not call settings.save() but caller should!
def tmp_secret(self, encoded):
def tmp_secret(self, encoded, chain=None):
# Use indicated secret and stop using the SE; operate like this until reboot
self.tmp_value = bytes(encoded + bytes(AE_SECRET_LEN - len(encoded)))
@ -459,7 +459,7 @@ class PinAttempt:
# Copies system settings to new encrypted-key value, calculates
# XFP, XPUB and saves into that, and starts using them.
self.new_main_secret(self.tmp_value)
self.new_main_secret(self.tmp_value, chain=chain)
def trick_request(self, method_num, data):
# send/recv a trick-pin related request (mk4 only)

View File

@ -11,7 +11,7 @@
# - 'abandon' * 11 + 'about'
#
from menu import MenuItem, MenuSystem
from utils import xfp2str
from utils import xfp2str, parse_extended_key
import ngu, uctypes, bip39, random, version
from uhashlib import sha256
from ux import ux_show_story, the_ux, ux_dramatic_pause, ux_confirm, show_qr_code
@ -20,6 +20,7 @@ from pincodes import AE_SECRET_LEN, AE_LONG_SECRET_LEN
from actions import goto_top_menu
from stash import SecretStash, SensitiveValues
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
from pwsave import PassphraseSaver
from glob import settings, dis
from pincodes import pa
@ -406,8 +407,8 @@ async def new_from_dice(nwords):
# 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)
async def set_ephemeral_seed(encoded, chain=None):
pa.tmp_secret(encoded, chain=chain)
dis.progress_bar_show(1)
xfp = settings.get("xfp", "")
if xfp:
@ -469,6 +470,15 @@ async def ephemeral_seed_generate(nwords):
dis.fullscreen("Applying...")
await set_ephemeral_seed_words(words)
async def set_seed_extended_key(extended_key):
encoded, chain = xprv_to_encoded_secret(extended_key)
set_seed_value(encoded=encoded, chain=chain)
async def set_ephemeral_seed_extended_key(extended_key):
encoded, chain = xprv_to_encoded_secret(extended_key)
await set_ephemeral_seed(encoded=encoded, chain=chain)
goto_top_menu()
async def approve_word_list(seed, nwords, ephemeral=False):
# Force the user to write the seeds words down, give a quiz, then save them.
@ -533,7 +543,16 @@ def seed_words_to_encoded_secret(words):
nv = SecretStash.encode(seed_phrase=seed)
return nv
def set_seed_value(words=None, encoded=None):
def xprv_to_encoded_secret(xprv):
node, chain, _ = parse_extended_key(xprv, private=True)
if node is None:
raise ValueError("Failed to parse extended private key.")
nv = SecretStash.encode(xprv=node)
node.blank()
return nv, chain # need to know chain
def set_seed_value(words=None, encoded=None, chain=None):
# Save the seed words into secure element, and reboot. BIP-39 password
# is not set at this point (empty string)
if words:
@ -549,7 +568,7 @@ def set_seed_value(words=None, encoded=None):
# re-read settings since key is now different
# - also captures xfp, xpub at this point
pa.new_main_secret(nv)
pa.new_main_secret(nv, chain=chain)
# check and reload secret
pa.reset()
@ -713,7 +732,7 @@ class EphemeralSeedMenu(MenuSystem):
@classmethod
def construct(cls):
from glob import NFC, settings
from actions import nfc_recv_ephemeral
from actions import nfc_recv_ephemeral, import_tapsigner_backup_file, import_xprv
import_ephemeral_menu = [
MenuItem("24 Words", f=cls.ephemeral_seed_import, arg=24),
@ -729,8 +748,10 @@ class EphemeralSeedMenu(MenuSystem):
]
rv = [
MenuItem("Generate Seed", menu=gen_ephemeral_menu),
MenuItem("Import Seed", menu=import_ephemeral_menu),
MenuItem("Generate Words", menu=gen_ephemeral_menu),
MenuItem("Import Words", menu=import_ephemeral_menu),
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", "")

View File

@ -2,7 +2,7 @@
#
# utils.py - Misc utils. My favourite kind of source file.
#
import gc, sys, ustruct, ngu
import gc, sys, ustruct, ngu, chains, ure
from ubinascii import unhexlify as a2b_hex
from ubinascii import hexlify as b2a_hex
from ubinascii import a2b_base64, b2a_base64
@ -453,6 +453,32 @@ def parse_addr_fmt_str(addr_fmt):
"Choose from p2pkh, p2wpkh, p2sh-p2wpkh." % addr_fmt)
def parse_extended_key(ln, private=False):
# read an xpub/ypub/etc and return BIP-32 node and what chain it's on.
# - can handle any garbage line
# - returns (node, chain, addr_fmt)
# - people are using SLIP132 so we need this
node, chain, addr_fmt = None, None, None
if ln is None:
return node, chain, addr_fmt
ln = ln.strip()
if private:
rgx = r'.prv[A-Za-z0-9]+'
else:
rgx = r'.pub[A-Za-z0-9]+'
pat = ure.compile(rgx)
found = pat.search(ln)
# serialize, and note version code
try:
node, chain, addr_fmt, is_private = chains.slip32_deserialize(found.group(0))
except:
pass
return node, chain, addr_fmt
def import_prompt_builder(title):
from glob import NFC, VD
prompt, escape = None, None
@ -485,4 +511,17 @@ def export_prompt_builder(title):
prompt += "."
return prompt, escape
def decrypt_tapsigner_backup(backup_key, data):
try:
backup_key = a2b_hex(backup_key)
decrypt = ngu.aes.CTR(backup_key, bytes(16)) # IV 0
decrypted = decrypt.cipher(data).decode().strip()
# format of TAPSIGNER backup is known in advance
# extended private key is expected at the beginning of the first line
assert decrypted[1:4] == "prv"
except Exception:
raise ValueError("Decryption failed - wrong key?")
return decrypted.split("\n")
# EOF

View File

@ -153,6 +153,18 @@ def enter_number(need_keypress):
return doit
@pytest.fixture(scope='module')
def enter_hex(need_keypress):
def doit(hex_str):
for ch in hex_str:
int_ch = int(ch, 16)
for i in range(int_ch):
need_keypress("5") # up
need_keypress("9") # next
need_keypress('y')
return doit
@pytest.fixture(scope='module')
def enter_pin(enter_number, need_keypress, cap_screen):
def doit(pin):
@ -1418,6 +1430,41 @@ def load_shared_mod():
return mod
return doit
@pytest.fixture
def tapsigner_encrypted_backup(microsd_path, virtdisk_path):
def doit(way, testnet=True):
# create backup
from pycoin.key.BIP32Node import BIP32Node
node = BIP32Node.from_master_secret(os.urandom(32), netcode="XTN" if testnet else "BTC")
plaintext = node.hwif(as_private=True) + '\n' + random.choice(["m", "m/84h/0h/0h", "m/44'/0'/0'/0'"])
if testnet:
assert "tprv" in plaintext
else:
assert "xprv" in plaintext
from bsms.encryption import aes_256_ctr_encrypt
from base64 import b64encode
backup_key = os.urandom(16) # 128 bit
backup_key_hex = backup_key.hex()
ciphertext_hex = aes_256_ctr_encrypt(backup_key, bytes(16), plaintext)
ciphertext = bytes.fromhex(ciphertext_hex)
ciphertext_b64 = b64encode(ciphertext).decode()
fname = "backup-A4MQA-3135-02-15T0113.aes"
if way == "sd":
fpath = microsd_path(fname)
elif way == "vdisk":
fpath = virtdisk_path(fname)
else:
fpath = None
fname = ciphertext_b64
if fpath:
with open(fpath, "wb") as f:
f.write(ciphertext)
# in case of NFC fname is b64 encoded backup itself
return fname, backup_key_hex, node
return doit
# useful fixtures related to multisig
from test_multisig import (import_ms_wallet, make_multisig, offer_ms_import, fake_ms_txn,
make_ms_address, clear_ms, make_myself_wallet)

View File

@ -0,0 +1,2 @@
Õo{; ]íó®ªDf#Nôbê¶ŠÖ¯è¥b‡¹„¾Û~·eŽÏáXæ˜~ÁRßcuíK =)Pºªc¿;q™´N+õvàR|Ø"!™ …v¾×±J<C2B1>ƒ
Ð…lyABбµäñV \~1,}º±ç‰j×Ô§

View File

@ -0,0 +1 @@
RAJwoІ: Ш<C2A0>o+<2B>САф ї~нССvysTрпЗ<D0BF>Ќ<EFBFBD>ЙwТ<77>ЏЦ'15<31>q0~ёo<D191>Ов<D09E><D0B2>lшѓ IЕ,gѓН) 1PVЙ2L<>И-ў<>њу}<7D>ЛgRа<52>З`O{ЎsLє*Ф<><D0A4>ПхЖ--9<ф7В*WdЎg

View File

@ -2,7 +2,7 @@
#
# Ephemeral Seeds tests
#
import pytest, time, re
import pytest, time, re, os, shutil
from constants import simulator_fixed_xpub
from ckcc.protocol import CCProtocolPacker
@ -15,6 +15,7 @@ def truncate_seed_words(words):
words = words.split(" ")
return ' '.join(w[0:4] for w in words)
def seed_story_to_words(story: str):
# filter those that starts with space, number and colon --> actual words
words = [
@ -24,6 +25,17 @@ def seed_story_to_words(story: str):
]
return words
@pytest.fixture
def ephemeral_seed_disabled(cap_menu):
def doit():
time.sleep(0.1)
menu = cap_menu()
# no ephemeral seed chosen (yet)
assert "[" not in menu[0]
return doit
@pytest.fixture
def get_seed_value_ux(goto_home, pick_menu_item, need_keypress, cap_story, nfc_read_text):
def doit(nfc=False):
@ -64,6 +76,7 @@ def get_identity_story(goto_home, pick_menu_item, cap_story):
return story
return doit
@pytest.fixture
def goto_eph_seed_menu(goto_home, pick_menu_item, cap_story, need_keypress):
def doit():
@ -78,10 +91,58 @@ def goto_eph_seed_menu(goto_home, pick_menu_item, cap_story, need_keypress):
need_keypress("4") # understand consequences
return doit
@pytest.fixture
def verify_ephemeral_secret_ui(cap_story, need_keypress, cap_menu, dev, goto_home, pick_menu_item,
fake_txn, try_sign, goto_eph_seed_menu, reset_seed_words,
get_identity_story, get_seed_value_ux):
def doit(mnemonic=None, xpub=None):
time.sleep(0.3)
_, 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
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
if mnemonic:
seed_words = get_seed_value_ux()
assert mnemonic == seed_words
e_master_xpub = dev.send_recv(CCProtocolPacker.get_xpub(), timeout=5000)
assert e_master_xpub != simulator_fixed_xpub
if xpub:
assert e_master_xpub == xpub
psbt = fake_txn(2, 2, master_xpub=e_master_xpub, segwit_in=True)
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}]"
reset_seed_words()
goto_eph_seed_menu()
menu = cap_menu()
assert menu[0] != f"[{ident_xfp}]"
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):
def test_ephemeral_seed_generate(num_words, pick_menu_item, goto_home, cap_story, need_keypress,
reset_seed_words, goto_eph_seed_menu, dice, ephemeral_seed_disabled,
verify_ephemeral_secret_ui):
reset_seed_words()
try:
@ -90,11 +151,8 @@ def test_ephemeral_seed_generate(num_words, cap_menu, pick_menu_item, goto_home,
time.sleep(.1)
goto_eph_seed_menu()
menu = cap_menu()
# no ephemeral seed chosen (yet)
assert len(menu) == 2
pick_menu_item("Generate Seed")
ephemeral_seed_disabled()
pick_menu_item("Generate Words")
if not dice:
pick_menu_item(f"{num_words} Words")
time.sleep(0.1)
@ -103,6 +161,7 @@ def test_ephemeral_seed_generate(num_words, cap_menu, pick_menu_item, goto_home,
for ch in '123456yy':
need_keypress(ch)
time.sleep(0.2)
title, story = cap_story()
assert f"Record these {num_words} secret words!" in story
assert "Press (6) to skip word quiz" in story
@ -115,49 +174,16 @@ def test_ephemeral_seed_generate(num_words, cap_menu, pick_menu_item, goto_home,
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
verify_ephemeral_secret_ui(mnemonic=e_seed_words)
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, True])
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
):
def test_ephemeral_seed_import_words(nfc, truncated, num_words, cap_menu, pick_menu_item, goto_home,
cap_story, need_keypress, reset_seed_words, goto_eph_seed_menu,
word_menu_entry, nfc_write_text, verify_ephemeral_secret_ui,
ephemeral_seed_disabled, get_seed_value_ux):
if truncated and not nfc: return
wordlists = {
@ -174,11 +200,8 @@ def test_ephemeral_seed_import(nfc, num_words, cap_menu, pick_menu_item, goto_ho
time.sleep(.1)
goto_eph_seed_menu()
menu = cap_menu()
# no ephemeral seed chosen (yet)
assert len(menu) == 2
pick_menu_item("Import Seed")
ephemeral_seed_disabled()
pick_menu_item("Import Words")
if not nfc:
pick_menu_item(f"{num_words} Words")
@ -199,38 +222,226 @@ def test_ephemeral_seed_import(nfc, num_words, cap_menu, pick_menu_item, goto_ho
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
menu = cap_menu()
assert menu[0] == "Ready To Sign" # returned to main menu
seed_words = get_seed_value_ux()
assert words == " ".join(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(2, 2, 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}]"
verify_ephemeral_secret_ui(mnemonic=words.split(" "))
nfc_seed = get_seed_value_ux(nfc=True) # export seed via NFC (always truncated)
seed_words = get_seed_value_ux()
assert " ".join(nfc_seed) == truncate_seed_words(seed_words)
@pytest.mark.parametrize("way", ["sd", "vdisk", "nfc"])
@pytest.mark.parametrize('retry', range(3))
@pytest.mark.parametrize("testnet", [True, False])
def test_ephemeral_seed_import_tapsigner(way, retry, testnet, pick_menu_item, cap_story, enter_hex,
need_keypress, reset_seed_words, goto_eph_seed_menu,
verify_ephemeral_secret_ui, ephemeral_seed_disabled,
nfc_write_text, tapsigner_encrypted_backup):
reset_seed_words()
fname, backup_key_hex, node = tapsigner_encrypted_backup(way, testnet=testnet)
try:
goto_eph_seed_menu()
except:
time.sleep(.1)
goto_eph_seed_menu()
ephemeral_seed_disabled()
pick_menu_item("Tapsigner Backup")
time.sleep(0.1)
_, story = cap_story()
if way == "sd":
if "Press (1) to import TAPSIGNER encrypted backup file from SD Card" in story:
need_keypress("1")
elif way == "nfc":
if "press (3) to import via NFC" not in story:
pytest.xfail("NFC disabled")
else:
need_keypress("3")
time.sleep(0.2)
nfc_write_text(fname)
time.sleep(0.3)
else:
# virtual disk
if "press (2) to import from Virtual Disk" not in story:
pytest.xfail("Vdisk disabled")
else:
need_keypress("2")
if way != "nfc":
time.sleep(0.1)
_, story = cap_story()
assert "Pick TAPSIGNER encrypted backup file" in story
need_keypress("y")
pick_menu_item(fname)
time.sleep(0.1)
_, story = cap_story()
assert "your TAPSIGNER" in story
assert "back of the card" in story
need_keypress("y") # yes I have backup key
enter_hex(backup_key_hex)
verify_ephemeral_secret_ui(xpub=node.hwif())
@pytest.mark.parametrize("fail", ["wrong_key", "key_len", "plaintext", "garbage"])
def test_ephemeral_seed_import_tapsigner_fail(cap_menu, pick_menu_item, goto_home, cap_story, fail,
need_keypress, reset_seed_words, enter_hex,
tapsigner_encrypted_backup, goto_eph_seed_menu,
microsd_path, ephemeral_seed_disabled):
reset_seed_words()
fail_msg = "Decryption failed - wrong key?"
fname, backup_key_hex, node = tapsigner_encrypted_backup("sd", testnet=False)
if fail == "plaintext":
with open(microsd_path(fname), "w") as f:
f.write(node.hwif(True) + "\n")
if fail == "garbage":
with open(microsd_path(fname), "wb") as f:
f.write(os.urandom(152))
try:
goto_eph_seed_menu()
except:
time.sleep(.1)
goto_eph_seed_menu()
ephemeral_seed_disabled()
pick_menu_item("Tapsigner Backup")
time.sleep(0.1)
_, story = cap_story()
if "Press (1) to import TAPSIGNER encrypted backup file from SD Card" in story:
need_keypress("1")
time.sleep(0.1)
_, story = cap_story()
assert "Pick TAPSIGNER encrypted backup file" in story
need_keypress("y")
pick_menu_item(fname)
time.sleep(0.1)
_, story = cap_story()
assert "Press OK to continue X to cancel." in story
need_keypress("y") # yes I have backup key
if fail == "wrong_key":
backup_key_hex = os.urandom(16).hex()
if fail == "key_len":
backup_key_hex = os.urandom(15).hex()
fail_msg = "'Backup Key' length != 32"
enter_hex(backup_key_hex)
time.sleep(0.3)
title, story = cap_story()
assert title == "FAILURE"
assert fail_msg in story
need_keypress("x")
need_keypress("x")
@pytest.mark.parametrize("data", [
(
"backup-4VMI3-2023-02-15T1645.aes",
"cb5bec9ddea4e85558bb54f41dcb1d2e",
"xpub661MyMwAqRbcFkTtUfByC6u46vJtdw6xFHUFhjc2AvA16BJCUPoeuwQcthN6yshHR34WZBT5gsHYVtha2QD9j9QozJf9ENeHS6TDgSAFBeX"
),
(
"backup-O4MZA-2023-02-15T2250.aes",
"578efa5d6803e3c314a98a87d499ce97",
"xpub661MyMwAqRbcGBeMu9h1B222hQmc4XkXasbN4F3mDGTWRJ11UQ5orWv41FPVK7stXsS9UtR5DBTArBvcsHPiCE2E1PAdqq1UQiQTYmrEEaa"
),
])
def test_ephemeral_seed_import_tapsigner_real(data, cap_menu, pick_menu_item, goto_home, cap_story,
need_keypress, reset_seed_words, enter_hex, microsd_path,
goto_eph_seed_menu, verify_ephemeral_secret_ui,
ephemeral_seed_disabled):
fname, backup_key_hex, pub = data
fpath = microsd_path(fname)
shutil.copy(f"data/{fname}", fpath)
reset_seed_words()
try:
goto_eph_seed_menu()
except:
time.sleep(.1)
goto_eph_seed_menu()
ephemeral_seed_disabled()
pick_menu_item("Tapsigner Backup")
time.sleep(0.1)
_, story = cap_story()
if "Press (1) to import TAPSIGNER encrypted backup file from SD Card" in story:
need_keypress("1")
time.sleep(0.1)
_, story = cap_story()
assert "Pick TAPSIGNER encrypted backup file" in story
need_keypress("y")
pick_menu_item(fname)
time.sleep(0.1)
_, story = cap_story()
assert "Press OK to continue X to cancel." in story
need_keypress("y") # yes I have backup key
enter_hex(backup_key_hex)
verify_ephemeral_secret_ui(xpub=pub)
os.unlink(fpath)
@pytest.mark.parametrize("way", ["sd", "vdisk", "nfc"])
@pytest.mark.parametrize('retry', range(3))
@pytest.mark.parametrize("testnet", [True, False])
def test_ephemeral_seed_import_xprv(way, retry, testnet, cap_menu, pick_menu_item, goto_home,
cap_story, need_keypress, reset_seed_words, goto_eph_seed_menu,
nfc_write_text, enter_hex, microsd_path, virtdisk_path,
verify_ephemeral_secret_ui, ephemeral_seed_disabled):
reset_seed_words()
fname = "ek.txt"
from pycoin.key.BIP32Node import BIP32Node
node = BIP32Node.from_master_secret(os.urandom(32), netcode="XTN" if testnet else "BTC")
ek = node.hwif(as_private=True) + '\n'
if way =="sd":
fpath = microsd_path(fname)
elif way == "vdisk":
fpath = virtdisk_path(fname)
if way != "nfc":
with open(fpath, "w") as f:
f.write(ek)
if testnet:
assert "tprv" in ek
else:
assert "xprv" in ek
try:
goto_eph_seed_menu()
except:
time.sleep(.1)
goto_eph_seed_menu()
ephemeral_seed_disabled()
pick_menu_item("Import XPRV")
time.sleep(0.1)
_, story = cap_story()
if way == "sd":
if "Press (1) to import extended private key file from SD Card" in story:
need_keypress("1")
elif way == "nfc":
if "press (3) to import via NFC" not in story:
pytest.xfail("NFC disabled")
else:
need_keypress("3")
time.sleep(0.2)
nfc_write_text(ek)
time.sleep(0.3)
else:
# virtual disk
if "press (2) to import from Virtual Disk" not in story:
pytest.xfail("Vdisk disabled")
else:
need_keypress("2")
if way != "nfc":
time.sleep(0.1)
_, story = cap_story()
assert "Select file containing the extended private key" in story
need_keypress("y")
pick_menu_item(fname)
verify_ephemeral_secret_ui(xpub=node.hwif())
# EOF

View File

@ -463,11 +463,11 @@ def test_new_wallet(nwords, goto_home, pick_menu_item, cap_story, need_keypress,
@pytest.mark.parametrize('multiple_runs', range(3))
@pytest.mark.parametrize('nfc', [True, False])
@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc"])
@pytest.mark.parametrize('testnet', [True, False])
def test_import_prv(nfc, testnet, pick_menu_item, cap_story, need_keypress, unit_test, cap_menu, word_menu_entry, get_secrets,
microsd_path, multiple_runs, reset_seed_words, nfc_write_text, settings_set):
def test_import_prv(way, testnet, pick_menu_item, cap_story, need_keypress, unit_test, cap_menu,
word_menu_entry, get_secrets, microsd_path, multiple_runs, reset_seed_words,
nfc_write_text, settings_set, virtdisk_path):
if testnet:
netcode = "XTN"
settings_set('chain', 'XTN')
@ -485,31 +485,44 @@ def test_import_prv(nfc, testnet, pick_menu_item, cap_story, need_keypress, unit
else:
assert "xprv" in prv
if not nfc:
fname = 'test-%d.txt' % os.getpid()
path = microsd_path(fname)
with open(path, 'wt') as f:
fname = 'test-%d.txt' % os.getpid()
if way =="sd":
fpath = microsd_path(fname)
elif way == "vdisk":
fpath = virtdisk_path(fname)
if way != "nfc":
with open(fpath, "w") as f:
f.write(prv)
print("Created: %s" % path)
m = cap_menu()
assert m[0] == 'New Seed Words'
pick_menu_item('Import Existing')
pick_menu_item('Import XPRV')
title, body = cap_story()
assert "press (3) to import via NFC" in body
assert "Press (1) to import extended private key file from SD Card" in body
if nfc:
need_keypress("3")
nfc_write_text(prv)
time.sleep(0.5)
time.sleep(0.1)
_, story = cap_story()
if way == "sd":
if "Press (1) to import extended private key file from SD Card" in story:
need_keypress("1")
elif way == "nfc":
if "press (3) to import via NFC" not in story:
pytest.skip("NFC disabled")
else:
need_keypress("3")
time.sleep(0.2)
nfc_write_text(prv)
time.sleep(0.3)
else:
need_keypress("1")
time.sleep(0.2)
title, body = cap_story()
assert 'Select file' in body
need_keypress('y')
time.sleep(.01)
# virtual disk
if "press (2) to import from Virtual Disk" not in story:
pytest.skip("Vdisk disabled")
else:
need_keypress("2")
if way != "nfc":
time.sleep(0.1)
_, story = cap_story()
assert "Select file containing the extended private key" in story
need_keypress("y")
pick_menu_item(fname)
unit_test('devtest/abort_ux.py')
@ -522,6 +535,67 @@ def test_import_prv(nfc, testnet, pick_menu_item, cap_story, need_keypress, unit
reset_seed_words()
@pytest.mark.parametrize("way", ["sd", "vdisk", "nfc"])
@pytest.mark.parametrize('retry', range(3))
@pytest.mark.parametrize("testnet", [True, False])
def test_seed_import_tapsigner(way, retry, testnet, cap_menu, pick_menu_item, goto_home, cap_story,
need_keypress, reset_seed_words, dev, try_sign, enter_hex, unit_test,
settings_set, get_secrets, tapsigner_encrypted_backup, nfc_write_text):
fname, backup_key_hex, node = tapsigner_encrypted_backup(way, testnet=testnet)
if testnet:
settings_set('chain', 'XTN')
else:
settings_set('chain', 'XTN')
unit_test('devtest/clear_seed.py')
m = cap_menu()
assert m[0] == 'New Seed Words'
pick_menu_item('Import Existing')
pick_menu_item("Tapsigner Backup")
time.sleep(0.1)
_, story = cap_story()
if way == "sd":
if "Press (1) to import TAPSIGNER encrypted backup file from SD Card" in story:
need_keypress("1")
elif way == "nfc":
if "press (3) to import via NFC" not in story:
pytest.skip("NFC disabled")
else:
need_keypress("3")
time.sleep(0.2)
nfc_write_text(fname)
time.sleep(0.3)
else:
# virtual disk
if "press (2) to import from Virtual Disk" not in story:
pytest.skip("Vdisk disabled")
else:
need_keypress("2")
if way != "nfc":
time.sleep(0.1)
_, story = cap_story()
assert "Pick TAPSIGNER encrypted backup file" in story
need_keypress("y")
pick_menu_item(fname)
time.sleep(0.1)
_, story = cap_story()
assert "your TAPSIGNER" in story
assert "back of the card" in story
need_keypress("y") # yes I have backup key
enter_hex(backup_key_hex)
unit_test('devtest/abort_ux.py')
v = get_secrets()
assert v['xpub'] == node.hwif()
assert v['xprv'] == node.hwif(as_private=True)
reset_seed_words()
@pytest.mark.parametrize('target', ['baby', 'struggle', 'youth'])
@pytest.mark.parametrize('version', range(8))
def test_bip39_pick_words(target, version, goto_home, pick_menu_item, cap_story, need_keypress,