Main seed and ephemeral seed from Tapsigner backup + ephemeral seed from extended private key
This commit is contained in:
parent
d6193d5636
commit
307df0c013
2
.gitignore
vendored
2
.gitignore
vendored
@ -24,3 +24,5 @@ __pycache__/
|
||||
|
||||
.tags
|
||||
pp
|
||||
|
||||
.idea/
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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),
|
||||
]
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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", "")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
2
testing/data/backup-4VMI3-2023-02-15T1645.aes
Normal file
2
testing/data/backup-4VMI3-2023-02-15T1645.aes
Normal file
@ -0,0 +1,2 @@
|
||||
Õo{;]íó®ªDf#Nôbê¶ŠÖ¯è¥b‡¹„¾Û~·eŽÏáXæ˜~€ÁRßc‹uíK=)Pºªc¿;q™´N+õvàR|Ø"!™ …v¾×±J<C2B1>ƒ‹
|
||||
Ð…lyABбµäñV
\~1,}º±ç‰j‚×Ô§
|
||||
1
testing/data/backup-O4MZA-2023-02-15T2250.aes
Normal file
1
testing/data/backup-O4MZA-2023-02-15T2250.aes
Normal file
@ -0,0 +1 @@
|
||||
RAJwoІ: Ш<C2A0>o+<2B>САф ї~нССvysTрпЗ<D0BF>Ќ<EFBFBD>ЙwТ<77>ЏЦ'15<31>q0~ёo<D191>wЇОв<D09E><D0B2>lшѓ
IЕ,gѓН)
1PVЙ2L<>И-ў<>њу}<7D>ЛgRа<52>З`O{ЎsLє*Ф<><D0A4>ПхЖ--9<ф7В*WdЎg
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user