WIF Store

This commit is contained in:
scgbckbone 2026-02-18 22:39:07 +01:00 committed by doc-hex
parent c19be4f41e
commit 9b131b2eff
22 changed files with 1243 additions and 82 deletions

View File

@ -8,6 +8,7 @@ This lists the new changes that have not yet been published in a normal release.
- Requires a carefully crafted PSBT that does not represent a monetary transaction, but instead is demonstrating
control over the keys for a list of UTXO, and commits to a short text message.
- Read more [here](https://github.com/Coldcard/firmware/blob/master/docs/proof-of-reserves-bip-322.md).
- New Feature: WIF Store. Ability to import foreign WIF keys and use them for PSBT signing.
- New Feature: Export [BIP-380](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki) extended key expression.
Navigate to "Advanced/Tools -> Export Wallet -> Key Expression"
- New Feature: Transaction Input Explorer. Shows data about UTXO(s) being spent. Press (2) before approving

View File

@ -16,7 +16,7 @@ from serializations import ser_uint256, SIGHASH_ALL
from ux import ux_show_story, abort_and_goto, ux_dramatic_pause, ux_clear_keys, ux_confirm, the_ux
from ux import show_qr_code, OK, X, abort_and_push, AbortInteraction, ux_input_text, ux_enter_number
from usb import CCBusyError
from utils import (HexWriter, xfp2str, problem_file_line, cleanup_deriv_path, B2A,
from utils import (HexWriter, xfp2str, problem_file_line, cleanup_deriv_path, B2A, node_from_privkey,
show_single_address, keypath_to_str, seconds2human_readable)
from psbt import psbtObject, FatalPSBTIssue, FraudulentChangeOutput
from files import CardSlot, CardMissingError
@ -131,7 +131,7 @@ Press %s to continue, otherwise %s to cancel.''' % (OK, X)
class ApproveMessageSign(UserAuthorizedAction):
def __init__(self, text, subpath, addr_fmt, approved_cb=None,
msg_sign_request=None, only_printable=True):
msg_sign_request=None, only_printable=True, privkey=None):
super().__init__()
is_json = False
@ -146,13 +146,18 @@ class ApproveMessageSign(UserAuthorizedAction):
self.subpath = cleanup_deriv_path(subpath)
self.addr_fmt = chains.parse_addr_fmt_str(addr_fmt)
self.approved_cb = approved_cb
self.privkey = privkey
from glob import dis
dis.fullscreen('Wait...')
with stash.SensitiveValues() as sv:
node = sv.derive_path(self.subpath)
self.address = sv.chain.address(node, self.addr_fmt)
if self.privkey:
node = node_from_privkey(self.privkey)
self.address = chains.current_chain().address(node, self.addr_fmt)
else:
with stash.SensitiveValues() as sv:
node = sv.derive_path(self.subpath)
self.address = sv.chain.address(node, self.addr_fmt)
dis.progress_bar_show(1)
@ -173,7 +178,8 @@ class ApproveMessageSign(UserAuthorizedAction):
else:
# perform signing (progress bar shown)
digest = chains.current_chain().hash_message(self.text.encode())
self.result, _ = sign_message_digest(digest, self.subpath, "Signing...", self.addr_fmt)
self.result, _ = sign_message_digest(digest, self.subpath, "Signing...",
self.addr_fmt, pk=self.privkey)
if self.approved_cb:
# for micro sd case
@ -197,7 +203,7 @@ def sign_msg(text, subpath, addr_fmt):
async def approve_msg_sign(text, subpath, addr_fmt, approved_cb=None,
msg_sign_request=None, kill_menu=False,
only_printable=True):
only_printable=True, privkey=None):
# Ask user if they want to sign some short text message.
UserAuthorizedAction.cleanup()
@ -208,6 +214,7 @@ async def approve_msg_sign(text, subpath, addr_fmt, approved_cb=None,
approved_cb=approved_cb,
msg_sign_request=msg_sign_request,
only_printable=only_printable,
privkey=privkey
)
if kill_menu:
@ -228,8 +235,6 @@ async def sign_txt_file(filename):
async def done(signature, address, text):
# complete. write out result
from glob import dis
orig_path, basename = filename.rsplit('/', 1)
orig_path += '/'
base = basename.rsplit('.', 1)[0]
@ -1734,12 +1739,14 @@ class TXInpExplorer(TXExplorer):
psbt_item = ""
if inp.required_key:
ws = self.user_auth_action.psbt.wif_store
our = [inp.required_key] if isinstance(inp.required_key, bytes) else inp.required_key
psbt_item += "Our key%s:\n\n" % ("s" if len(our) > 1 else "")
for k in our:
pth = inp.subpaths[k]
psbt_item += "%s:\n%s\n\n" % (keypath_to_str(pth, prefix="%s/" % xfp2str(pth[0])),
b2a_hex(k).decode())
ws_note = "\n(WIF Store)" if (ws and k in ws) else ""
psbt_item += "%s:\n%s%s\n\n" % (keypath_to_str(pth, prefix="%s/" % xfp2str(pth[0])),
b2a_hex(k).decode(), ws_note)
M = None
if inp.is_multisig:

View File

@ -59,17 +59,9 @@ def decode_secret(got):
if len(got) in (51, 52):
try:
raw = ngu.codecs.b58_decode(got)
if raw[0] in (0xef, 0x80):
testnet = True if raw[0] == 0xef else False
if len(raw) in (33, 34): # uncompressed pubkey
compressed = False
if len(raw) == 34: # compressed pubkey
assert raw[33] == 0x01
compressed = True
sk = raw[1:33]
kp = ngu.secp256k1.keypair(sk)
return 'wif', (got, kp, compressed, testnet)
from wif import decode_wif
kp, testnet, compressed = decode_wif(got)
return 'wif', (got, kp, compressed, testnet)
except: pass
taste = got.strip().lower()

View File

@ -12,7 +12,7 @@ from menu import MenuItem, MenuSystem
from ubinascii import hexlify as b2a_hex
from ubinascii import b2a_base64
from msgsign import write_sig_file
from utils import xfp2str, swab32
from utils import xfp2str, swab32, node_from_privkey
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
BIP85_PWD_LEN = 21
@ -180,7 +180,7 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
elif s_mode == 'xprv':
# Raw XPRV value.
ch, pk = new_secret[0:32], new_secret[32:64]
master_node = ngu.hdnode.HDNode().from_chaincode_privkey(ch, pk)
master_node = node_from_privkey(pk, ch)
node = master_node
encoded = stash.SecretStash.encode(xprv=master_node)

View File

@ -20,6 +20,7 @@ from paper import make_paper_wallet
from trick_pins import TrickPinMenu
from tapsigner import import_tapsigner_backup_file
from ccc import toggle_ccc_feature, sssp_spending_policy, sssp_feature_menu
from wif import WIFStore
# useful shortcut keys
from charcodes import KEY_QR, KEY_NFC
@ -422,6 +423,7 @@ AdvancedNormalMenu = [
MenuItem("Key Teleport (start)", f=kt_start_rx, predicate=version.has_qr),
MenuItem("Spending Policy", menu=SpendingPolicySubMenu,shortcut='s',predicate=has_real_secret),
MenuItem('Paper Wallets', f=make_paper_wallet),
MenuItem('WIF Store', menu=WIFStore.make_menu),
MenuItem('NFC Tools', predicate=nfc_enabled, menu=NFCToolsMenu, shortcut=KEY_NFC),
MenuItem("Danger Zone", menu=DangerZoneMenu, shortcut='z'),
]
@ -548,6 +550,7 @@ HobbledAdvancedMenu = [
MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu, predicate=sssp_related_keys),
MenuItem('Paper Wallets', f=make_paper_wallet),
MenuItem('NFC Tools', predicate=nfc_enabled, menu=HobbledNFCToolsMenu, shortcut=KEY_NFC),
MenuItem('WIF Store', menu=WIFStore.make_menu, predicate=sssp_related_keys),
MenuItem('Show %s Version' % ("Firmware" if version.has_qwerty else "FW"), f=show_version),
MenuItem("Destroy Seed", f=clear_seed, predicate=has_real_secret),
]

View File

@ -59,6 +59,7 @@ freeze_as_mpy('', [
'version.py',
'wallet.py',
'web2fa.py',
'wif.py',
'xor_seed.py'
], opt=0)

View File

@ -12,7 +12,7 @@ from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
from ux import (ux_show_story, OK, ux_enter_bip32_index, ux_input_text, the_ux,
import_export_prompt, ux_aborted)
from utils import problem_file_line, to_ascii_printable, show_single_address
from utils import problem_file_line, to_ascii_printable, show_single_address, node_from_privkey
from files import CardSlot, CardMissingError, needs_microsd
def rfc_signature_template(msg, addr, sig):
@ -381,7 +381,7 @@ def sign_message_digest(digest, subpath, prompt, addr_fmt=AF_CLASSIC, pk=None):
else:
# if private key is provided, derivation subpath is ignored
# and given private key is used for signing.
node = ngu.hdnode.HDNode().from_chaincode_privkey(bytes(32), pk)
node = node_from_privkey(pk)
dis.progress_sofar(50, 100)
addr = ch.address(node, addr_fmt)

View File

@ -763,6 +763,11 @@ class NFCHandler:
f = lambda x: x.decode()
return await self._nfc_reader(f, 'Unable to find BIP-322 message.')
async def read_wif(self):
# only compressed WIFs allowed
f = lambda x: x.decode() if len(x) >= 51 else None
return await self._nfc_reader(f, 'Unable to find WIF key(s).')
async def _nfc_reader(self, func, fail_msg):
data = await self.start_nfc_rx()
if not data: return

View File

@ -69,6 +69,7 @@ from utils import call_later_ms
# ktrx = (privkey) Key teleport Rx has been started, this will be our keypair
# sssp = (complex) If present, a (single signer) spending-policy is defined (maybe disabled)
# lfr = (string) If present, the reason why Spending Policy blocked last transaction
# wifs = (list) List of tuples (public/private key)
# Stored w/ key=00 for access before login
# _skip_pin = hard code a PIN value (dangerous, only for debug)

View File

@ -6,8 +6,10 @@ import os, chains, ngu, struct, version
from glob import settings
from ucollections import namedtuple
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
from exceptions import UnknownAddressExplained
from utils import problem_file_line, show_single_address
from public_constants import AFC_SCRIPT, AF_P2WPKH_P2SH, AF_P2SH, AF_P2WSH_P2SH, AF_P2TR, AF_P2WSH
# Track many addresses, but in compressed form
# - map from random Bech32/Base58 payment address to (wallet) + keypath
@ -212,20 +214,13 @@ class OwnershipCache:
return doit
@classmethod
def filter(cls, addr, args):
def filter(cls, addr_fmt, args):
# Filter possible candidates!
# - if you start w/ testnet, we'll follow that
from multisig import MultisigWallet
from public_constants import AFC_SCRIPT, AF_P2WPKH_P2SH, AF_P2SH, AF_P2WSH_P2SH
ch = chains.current_chain()
args = args or {}
addr_fmt = ch.possible_address_fmt(addr)
if not addr_fmt:
# might be valid address over on testnet vs mainnet
raise UnknownAddressExplained('That address is not valid on ' + ch.name)
# user has specified specific (named) wallet
named_wal = args.get("wallet", None)
if named_wal:
@ -309,7 +304,13 @@ class OwnershipCache:
dis.fullscreen("Wait...")
matches = OWNERSHIP.filter(addr, args)
ch = chains.current_chain()
addr_fmt = ch.possible_address_fmt(addr)
if not addr_fmt:
# might be valid address over on testnet vs mainnet
raise UnknownAddressExplained('That address is not valid on ' + ch.name)
matches = OWNERSHIP.filter(addr_fmt, args)
# build cache files for both external & internal chain
cachefs = []
@ -336,9 +337,17 @@ class OwnershipCache:
# first arg from_cache=False
return False, wallet, subpath
else:
raise UnknownAddressExplained('Searched %d candidate addresses in %d wallet(s)'
' without finding a match.' % (c, len(matches)))
# nothing found among singlesig & registered multisig wallets
# check WIF store (single sig only)
if addr_fmt not in [AF_P2TR, AF_P2WSH]:
from wif import iter_wif_store_addresses
target_af = AF_P2WPKH_P2SH if addr_fmt == AF_P2SH else addr_fmt
for i, store_addr in iter_wif_store_addresses(ch, target_af):
if store_addr == addr:
return False, "wif", i+1
raise UnknownAddressExplained('Searched %d candidate addresses in %d wallet(s)'
' without finding a match.' % (c, len(matches)))
@classmethod
async def search_ux(cls, addr, args):
@ -351,23 +360,24 @@ class OwnershipCache:
try:
_, wallet, subpath = cls.search(addr, args)
is_ms = isinstance(wallet, MultisigWallet)
sp = wallet.render_path(*subpath)
msg = show_single_address(addr)
msg += '\n\nFound in wallet:\n ' + wallet.name
msg += '\nDerivation path:\n ' + sp
if is_ms:
esc = ""
esc = ""
if wallet == "wif":
msg += '\n\nFound in WIF store at index %d' % subpath
else:
esc = "0"
msg += "\n\nPress (0) to sign message with this key."
sp = wallet.render_path(*subpath)
msg += '\n\nFound in wallet:\n ' + wallet.name
msg += '\nDerivation path:\n ' + sp
if not is_ms:
esc = "0"
msg += "\n\nPress (0) to sign message with this key."
title = "Verified"
if version.has_qwerty:
esc += KEY_QR
title += " Address"
else:
msg += ' (1) for address QR'
msg += ' Press (1) for address QR.'
esc += '1'
title += "!"

View File

@ -6,7 +6,7 @@ import stash, gc, history, sys, ngu, ckcc, chains
from ustruct import unpack_from, unpack, pack
from ubinascii import hexlify as b2a_hex
from utils import xfp2str, B2A, keypath_to_str
from utils import seconds2human_readable, datetime_from_timestamp, datetime_to_str
from utils import seconds2human_readable, datetime_from_timestamp, datetime_to_str, node_from_privkey
from chains import NLOCK_IS_TIME
from uhashlib import sha256
from uio import BytesIO
@ -20,6 +20,7 @@ from serializations import SIGHASH_ALL, SIGHASH_SINGLE, SIGHASH_NONE, SIGHASH_AN
from serializations import ALL_SIGHASH_FLAGS
from opcodes import OP_CHECKMULTISIG, OP_RETURN
from glob import settings
from wif import init_wif_store
from public_constants import (
PSBT_GLOBAL_UNSIGNED_TX, PSBT_GLOBAL_XPUB, PSBT_IN_NON_WITNESS_UTXO, PSBT_IN_WITNESS_UTXO,
@ -259,7 +260,7 @@ class psbtProxy:
self.fd.seek(pos)
return self.fd.read(ll)
def parse_subpaths(self, my_xfp, warnings):
def parse_subpaths(self, my_xfp, parent):
# Reformat self.subpaths into a more useful form for us; return # of them
# that are ours (and track that as self.num_our_keys)
# - works in-place, on self.subpaths
@ -295,8 +296,8 @@ class psbtProxy:
# XFP is unknown because PSBT built from derived XPUB only. Also privacy.
if here[0] == 0:
here[0] = my_xfp
if not any(True for k,_ in warnings if 'XFP' in k):
warnings.append(('Zero XFP',
if not any(True for k,_ in parent.warnings if 'XFP' in k):
parent.warnings.append(('Zero XFP',
'Assuming XFP of zero should be replaced by correct XFP'))
# update in place
@ -304,6 +305,8 @@ class psbtProxy:
if here[0] == my_xfp:
num_ours += 1
elif pk in parent.wif_store:
num_ours += 1
else:
# Address that isn't based on my seed; might be another leg in a p2sh,
# or an input we're not supposed to be able to sign... and that's okay.
@ -402,7 +405,7 @@ class psbtOutputProxy(psbtProxy):
# - full key derivation and validation is done during signing, and critical.
# - we raise fraud alarms, since these are not innocent errors
#
num_ours = self.parse_subpaths(my_xfp, parent.warnings)
num_ours = self.parse_subpaths(my_xfp, parent)
# - must match expected address for this output, coming from unsigned txn
af, addr_or_pubkey, is_segwit = txo.get_address()
@ -574,7 +577,7 @@ class psbtInputProxy(psbtProxy):
'unknown', 'utxo', 'witness_utxo', 'sighash', 'redeem_script', 'witness_script',
'fully_signed', 'is_segwit', 'is_multisig', 'is_p2sh', 'num_our_keys',
'required_key', 'scriptSig', 'amount', 'scriptCode', 'previous_txid',
'prevout_idx', 'sequence', 'req_time_locktime', 'req_height_locktime', 'addr_fmt'
'prevout_idx', 'sequence', 'req_time_locktime', 'req_height_locktime', 'addr_fmt',
)
def __init__(self, fd, idx):
@ -655,7 +658,7 @@ class psbtInputProxy(psbtProxy):
# require path for each addr, check some are ours
# rework the pubkey => subpath mapping
self.parse_subpaths(my_xfp, parent.warnings)
self.parse_subpaths(my_xfp, parent)
if self.part_sigs:
# How complete is the set of signatures so far?
@ -760,7 +763,7 @@ class psbtInputProxy(psbtProxy):
self.amount = utxo.nValue
self.addr_fmt, addr_or_pubkey, addr_is_segwit = utxo.get_address()
if not self.subpaths or self.fully_signed:
if not self.subpaths or self.fully_signed or (not self.num_our_keys):
# without xfp+path we will not be able to sign this input
# - okay if fully signed
# - okay if payjoin or other multi-signer (not multisig) txn
@ -816,6 +819,10 @@ class psbtInputProxy(psbtProxy):
# slight chance of dup xfps, so handle
which_key.add(pubkey)
elif pubkey in psbt.wif_store:
# maybe sset some input value
which_key.add(pubkey)
if not addr_is_segwit and \
len(redeem_script) == 22 and \
redeem_script[0] == 0 and redeem_script[1] == 20:
@ -1022,6 +1029,7 @@ class psbtObject(psbtProxy):
self.xpubs = [] # tuples(xfp_path, xpub)
self.my_xfp = settings.get('xfp', 0)
self.wif_store = init_wif_store()
# details that we discover as we go
self.inputs = None
@ -1760,13 +1768,13 @@ class psbtObject(psbtProxy):
# Important: parse incoming UTXO to build total input value
foreign = []
total_in = 0
from_wif_store = []
prevouts = set()
for i, txi in self.input_iter():
# check for duplicate inputs
k = (txi.prevout.hash, txi.prevout.n)
if k in prevouts:
if k in prevouts:
raise FatalPSBTIssue("Duplicate inputs")
if len(prevouts) < 100:
@ -1796,6 +1804,14 @@ class psbtObject(psbtProxy):
# - also validates redeem_script when present
# - also finds appropriate multisig wallet to be used
inp.determine_my_signing_key(i, utxo, self.my_xfp, self, cosign_xfp)
if inp.required_key and self.wif_store:
is_in = False
for pk in inp.required_key if isinstance(inp.required_key, set) else [inp.required_key]:
if pk in self.wif_store:
is_in = True
if is_in:
from_wif_store.append(i)
# iff to UTXO is segwit, then check it's value, and also
# capture that value, since it's supposed to be immutable
@ -1888,6 +1904,10 @@ class psbtObject(psbtProxy):
'Some input(s) provided were already completely signed by other parties: ' +
seq_to_str(self.presigned_inputs)))
if from_wif_store:
self.warnings.append(("WIF Store", "Some input(s) use key from the WIF store: " +
seq_to_str(from_wif_store)))
if MultisigWallet.disable_checks:
self.warnings.append(('Danger', 'Some multisig checks are disabled.'))
@ -2117,7 +2137,11 @@ class psbtObject(psbtProxy):
# need to consider a set of possible keys, since xfp may not be unique
for which_key in inp.required_key:
# get node required
node = self.check_pubkey_at_path(sv, inp.subpaths[which_key], which_key)
if which_key in self.wif_store:
node = node_from_privkey(self.wif_store[which_key])
else:
node = self.check_pubkey_at_path(sv, inp.subpaths[which_key], which_key)
if node:
break
else:
@ -2126,27 +2150,27 @@ class psbtObject(psbtProxy):
else:
# single pubkey <=> single key
which_key = inp.required_key
assert not inp.added_sigs, "already done??"
assert which_key in inp.subpaths, 'unk key'
if inp.subpaths[which_key][0] != my_xfp:
# we don't have the key for this subkey
# (redundant, required_key wouldn't be set)
continue
# get node required
skp = keypath_to_str(inp.subpaths[which_key])
node = sv.derive_path(skp, register=False)
if which_key in self.wif_store:
node = node_from_privkey(self.wif_store[which_key])
else:
# get node required
skp = keypath_to_str(inp.subpaths[which_key])
node = sv.derive_path(skp, register=False)
# expensive test, but works... and important
pu = node.pubkey()
assert pu == which_key, \
"Path (%s) led to wrong pubkey for input#%d"%(skp, in_idx)
# expensive test, but works... and important
pu = node.pubkey()
# track wallet usage
OWNERSHIP.note_subpath_used(inp.subpaths[which_key])
assert pu == which_key, \
"Path (%s) led to wrong pubkey for input#%d"%(skp, in_idx)
# track wallet usage
OWNERSHIP.note_subpath_used(inp.subpaths[which_key])
if not inp.is_segwit:
# Hash by serializing/blanking various subparts of the transaction

View File

@ -12,7 +12,7 @@
#
import ngu, uctypes, gc, bip39, utime
from uhashlib import sha256
from utils import swab32, call_later_ms, B2A
from utils import swab32, call_later_ms, B2A, node_from_privkey
SEED_LEN_OPTS = [12, 18, 24]
@ -104,7 +104,7 @@ class SecretStash:
ch, pk = secret[1:33], secret[33:65]
assert not _bip39pw
hd.from_chaincode_privkey(ch, pk)
hd = node_from_privkey(pk, ch)
return 'xprv', ch+pk, hd
elif marker & 0x80:
@ -403,8 +403,7 @@ class SensitiveValues:
self.register(cc)
self.register(pk)
rv = ngu.hdnode.HDNode()
rv.from_chaincode_privkey(cc, pk)
rv = node_from_privkey(pk, cc)
self.register(rv)
return rv, p

View File

@ -12,6 +12,7 @@ from menu import MenuSystem, MenuItem
from ux import ux_show_story, ux_confirm, ux_dramatic_pause, ux_enter_number, the_ux
from stash import SecretStash
from drv_entro import bip85_derive
from utils import node_from_privkey
# see from mk4-bootloader/se2.h
NUM_TRICKS = const(14)
@ -897,9 +898,8 @@ Wallet is XPRV-based and derived from a fixed path.''' % pin
assert s.tc_flags == flags
if flags & TC_XPRV_WALLET:
node = ngu.hdnode.HDNode()
ch, pk = s.xdata[0:32], s.xdata[32:64]
node.from_chaincode_privkey(ch, pk)
node = node_from_privkey(pk, ch)
title, msg, *_ = render_master_secrets('xprv', None, node)
elif flags & TC_WORD_WALLET:

View File

@ -788,4 +788,11 @@ def extract_cosigner(data, af_str):
# emulate coldcard export xpubs
return {"xfp": xfp, af_str: ek, key_deriv: deriv}
def node_from_privkey(privkey, chain_code=bytes(32)):
return ngu.hdnode.HDNode().from_chaincode_privkey(chain_code, privkey)
def node_from_pubkey(pubkey, chain_code=bytes(32)):
return ngu.hdnode.HDNode().from_chaincode_pubkey(chain_code, pubkey)
# EOF

315
shared/wif.py Normal file
View File

@ -0,0 +1,315 @@
# (c) Copyright 2026 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
import chains, ngu, version
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
from ux import ux_show_story, ux_confirm, the_ux, import_export_prompt, ux_input_text, show_qr_code
from menu import MenuSystem, MenuItem
from utils import problem_file_line, show_single_address, node_from_pubkey
from files import CardSlot
from glob import settings
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
from public_constants import AF_P2WPKH
from msgsign import msg_signing_done
def decode_wif(wif):
raw = ngu.codecs.b58_decode(wif)
assert raw[0] in (0xef, 0x80)
testnet = True if raw[0] == 0xef else False
assert len(raw) in (33, 34)
compressed = False
if len(raw) == 34: # compressed pubkey
assert raw[33] == 0x01
compressed = True
sk = raw[1:33]
kp = ngu.secp256k1.keypair(sk) # catches wrong private keys
return kp, testnet, compressed
def iter_wif_store_addresses(chain, addr_fmt):
# nothing found among singlesig & registered multisig wallets
# check WIF store
wifs = settings.get("wifs", [])
if not wifs: return
for i, (pk, sk) in enumerate(wifs):
node = node_from_pubkey(a2b_hex(pk))
yield i, chain.address(node, addr_fmt)
class WIFStore(MenuSystem):
MAX_ITEMS = 30
def __init__(self):
items = self.construct()
super().__init__(items)
@classmethod
async def make_menu(cls, *a):
if not settings.get("wifs", None):
intro = ("Individual private keys, encoded as WIF (Wallet Import Format) keys"
" can be imported and used for signing. Any PSBT that uses a WIF stored here"
" will be signed as normal, but warning is shown."
" Remove all imported keys to disable WIF store signing")
ch = await ux_show_story(intro, title="WIF Store")
if ch != 'y': return
return cls()
def update_contents(self):
tmp = self.construct()
self.replace_items(tmp)
def construct(self):
from glob import dis
from seed import not_hobbled_mode
dis.fullscreen("Wait...")
ch = chains.current_chain()
wifs = settings.get('wifs', [])
items = []
if len(wifs) < self.MAX_ITEMS:
items.append(MenuItem('Import WIF', f=self.import_wif, predicate=not_hobbled_mode))
a_items = []
export_all = []
for i, (pk, sk) in enumerate(wifs):
wif = ngu.codecs.b58_encode(ch.b58_privkey + a2b_hex(sk) + b'\x01')
export_all.append(wif)
submenu = [
MenuItem("Detail", f=self.detail, arg=(wif,pk,sk)),
MenuItem("Addresses", f=self.show_addr_step1, arg=pk),
MenuItem("Sign MSG", f=self.sign_msg_step1, arg=sk),
MenuItem('Delete', f=self.delete, arg=(i, pk), predicate=not_hobbled_mode),
]
# cannot use truncate_address here, as it does nto fit on Mk4 (because padded numbering)
clen = 12 if version.has_qwerty else 5
a_items.append(MenuItem("%2d: %s" % (i+1, wif[0:clen] + '' + wif[-clen:]),
menu=MenuSystem(submenu)))
if a_items:
items += a_items
if len(a_items) > 1:
items.append(MenuItem("Export All", f=self.export_all, arg=export_all))
items.append(MenuItem("Clear All", f=self.clear_all, predicate=not_hobbled_mode))
else:
items.append(MenuItem("(none yet)"))
return items
async def detail(self, a, b, item):
wif, pk, sk = item.arg
msg = "%s\n\nPrivkey:\n%s\n\nPubkey:\n%s" % (wif, sk, pk)
from export import export_contents
title = "WIF"
await export_contents(title, wif, "wif.txt", None, None,
force_prompt=True, intro=msg, ux_title=title)
async def show_addr_step1(self, a, b, item):
pubkey = a2b_hex(item.arg)
rv = [
MenuItem(chains.addr_fmt_label(af), f=self.show_addr_step2, arg=(pubkey, af))
for af in chains.SINGLESIG_AF
]
the_ux.push(MenuSystem(rv))
async def show_addr_step2(self, a, b, item):
from glob import NFC
pubkey, af = item.arg
node = node_from_pubkey(pubkey)
addr = chains.current_chain().address(node, af)
msg = show_single_address(addr) + "\n\n"
escape = ""
# Q only hint keys
if not version.has_qwerty:
msg += "Press (1) to show address QR code."
escape += "1"
if NFC:
msg += "(3) to share via NFC."
escape += "3"
title = chains.addr_fmt_label(af) if version.has_qwerty else None
while True:
ch = await ux_show_story(msg, title=title, escape=escape,
hint_icons=KEY_QR+(KEY_NFC if NFC else ''))
if ch == "x": return
if ch in "1"+KEY_QR:
await show_qr_code(addr, is_alnum=af == AF_P2WPKH)
elif NFC and (ch in "3"+KEY_NFC):
await NFC.share_text(addr)
async def sign_msg_step1(self, a, b, item):
privkey = a2b_hex(item.arg)
rv = [
MenuItem(chains.addr_fmt_label(af), f=self.sign_msg_step2, arg=(privkey, af))
for af in chains.SINGLESIG_AF
]
the_ux.push(MenuSystem(rv))
async def sign_msg_step2(self, a, b, item):
from glob import NFC
from actions import file_picker
from auth import approve_msg_sign
ch = await import_export_prompt("message", is_import=True, force_prompt=True,
key0="to input message manually",
no_qr=not version.has_qwerty)
if ch == KEY_CANCEL:
return
elif ch == "0":
msg = await ux_input_text("", confirm_exit=False)
elif ch == KEY_NFC:
msg = await NFC.read_bip322_msg()
elif ch == KEY_QR:
from ux_q1 import QRScannerInteraction
msg = await QRScannerInteraction().scan_text('Scan message from a QR code')
else:
fn = await file_picker(suffix='.txt')
if not fn: return
with CardSlot(readonly=True, **ch) as card:
with open(fn, 'rt') as fd:
msg = fd.read()
if not msg: return
privkey, af = item.arg
await approve_msg_sign(msg, "", af, privkey=privkey, approved_cb=msg_signing_done)
async def delete(self, a, b, item):
# no confirm, stakes are low
if not await ux_confirm("Delete WIF key?"):
return
idx, pubkey = item.arg
wifs = settings.get('wifs', {})
if not wifs: return
try:
item = wifs[idx]
assert item[0] == pubkey
del wifs[idx]
settings.set('wifs', wifs)
settings.save()
except IndexError: pass
the_ux.pop() # pop submenu
self.update_contents()
async def clear_all(self, *a):
if await ux_confirm("Remove all saved WIF keys?", confirm_key='4'):
settings.remove_key("wifs")
settings.save()
self.update_contents()
async def export_all(self, a, b, item):
wifs = item.arg
from export import export_contents
title = "WIF Store"
await export_contents(title, "\n".join(wifs), "wif_store.txt",
None, None, force_prompt=True, ux_title=title)
async def import_wif(self, *a):
from glob import NFC, dis
from actions import file_picker
label = "WIF private key"
ch = await import_export_prompt(label, is_import=True, key0="to input WIF manually")
if ch == KEY_CANCEL:
return
elif ch == KEY_NFC:
got = await NFC.read_wif()
elif ch == KEY_QR:
from ux_q1 import QRScannerInteraction
got = await QRScannerInteraction().scan_text(label)
elif ch == "0":
got = await ux_input_text("", confirm_exit=False, max_len=52) # compressed WIF key str length is 52
else:
# pick a likely-looking file: just looking at size and extension
fn = await file_picker(suffix='.txt', min_size=51, max_size=2000,
none_msg="Must contain WIF(s)", **ch)
if not fn: return
with CardSlot(readonly=True, **ch) as card:
with open(fn, 'rt') as fd:
got = fd.read().strip()
if not got:
return
dis.fullscreen("Wait...")
# allow both newlines and commas as separators
if "\n" in got:
wifs = got.split("\n")
elif "," in got:
wifs = got.split(",")
else:
# just one wif
wifs = [got]
saved = settings.get("wifs", [])
len_saved = len(saved)
if (len_saved + len(wifs)) > self.MAX_ITEMS:
await ux_show_story("Max %d items allowed in WIF Store.\n\nAttempted to import %d keys,"
" while remaining WIF store capacity is only %d. Please, make room"
" first." % (self.MAX_ITEMS, len(wifs), self.MAX_ITEMS - len_saved),
title="Failure")
return
try:
for wif in wifs:
try:
wif = wif.strip()
kp, testnet, compressed = decode_wif(wif)
except Exception:
raise ValueError("wif decode")
assert compressed, "compressed only"
assert testnet == (chains.current_chain().ctype != "BTC"), "chain"
sk = b2a_hex(kp.privkey()).decode()
pk = b2a_hex(kp.pubkey().to_bytes()).decode()
item = (pk, sk)
if item not in saved: # ignore duplicates
saved.append(item)
if len_saved < len(saved):
settings.set('wifs', saved)
settings.save()
self.update_contents()
except Exception as e:
await ux_show_story('Failed to import WIF.\n\n%s\n%s' % (e, problem_file_line(e)),
title="Failure")
def init_wif_store():
# stored as hex strings, need load to bytes
wifs = settings.get('wifs', [])
if not wifs: return {}
res = {}
for pk, sk in wifs:
res[a2b_hex(pk)] = a2b_hex(sk)
return res
# EOF

View File

@ -16,6 +16,7 @@ from charcodes import *
from core_fixtures import _need_keypress, _sim_exec, _cap_story, _cap_menu, _cap_screen, _sim_eval
from core_fixtures import _press_select, _pick_menu_item, _enter_complex, _dev_hw_label
from txn import render_address
from bbqr import split_qrs
# lock down randomness
@ -2907,6 +2908,113 @@ def set_deltamode(sim_exec):
doit(False)
@pytest.fixture
def import_wif_to_store(goto_home, pick_menu_item, cap_story, press_select, cap_menu,
microsd_path, virtdisk_path, is_q1, scan_a_qr, need_keypress,
garbage_collector, press_nfc, nfc_write_text, enter_complex):
def doit(wif_lst, way="sd", sep="\n", early_exit=False):
home = True
try:
m = cap_menu()
if m[0] == "Import WIF":
home = False
except: pass
if home:
goto_home()
pick_menu_item("Advanced/Tools")
pick_menu_item("WIF Store")
time.sleep(.1)
title, story = cap_story()
if title == "WIF Store":
press_select()
menu = cap_menu()
assert menu[0] == "Import WIF"
num_items = sum(1 for i in menu if "" in i)
pick_menu_item("Import WIF")
time.sleep(.1)
title, story = cap_story()
conts = sep.join(wif_lst)
if way == "nfc":
if f"press {KEY_NFC if is_q1 else '(3)'} to import via NFC" not in story:
pytest.xfail("NFC disabled")
else:
press_nfc()
time.sleep(0.2)
nfc_write_text(conts)
time.sleep(0.3)
elif way == "qr":
if not is_q1:
raise pytest.xfail("Mk4 no QR")
assert f"{KEY_QR} to scan QR code" in story
need_keypress(KEY_QR)
if sep == "\n" or len(wif_lst) > 3:
# use BBQr
actual_vers, parts = split_qrs(conts, 'U', max_version=10)
random.shuffle(parts)
for p in parts:
scan_a_qr(p)
time.sleep(2.0 / len(parts))
else:
scan_a_qr(conts)
time.sleep(1)
elif way == "input":
assert "(0) to input WIF manually" in story
need_keypress("0")
enter_complex(conts, apply=False, b39pass=False)
else:
if way == "sd":
assert "Press (1) to import WIF private key from SD Card" in story
to_press = "1"
path_f = microsd_path
else:
if "press (2) to import from Virtual Disk" not in story:
raise pytest.xfail("Vdisk disabled")
else:
to_press = "2"
path_f = virtdisk_path
fname = "wif.txt"
fpath = path_f(fname)
with open(fpath, "w") as f:
f.write(conts)
need_keypress(to_press)
try:
pick_menu_item(fname)
except: pass
if early_exit:
return
time.sleep(.2)
new_menu = cap_menu()
# assert new_menu[0] == "Import WIF"
new_num_items = sum(1 for i in new_menu if "" in i)
assert new_num_items == (num_items + len(wif_lst))
if "Clear All" in new_menu[-1]:
new_menu = new_menu[:-1]
if "Export All" in new_menu[-1]:
new_menu = new_menu[:-1]
len_added = len(wif_lst)
for i, mi in enumerate(new_menu[-len_added:]):
left, right = mi.split("")
left = left.split(" ")[-1] # ignore numbering
assert wif_lst[i].startswith(left)
assert wif_lst[i].endswith(right)
return doit
# useful fixtures
from test_backup import backup_system

View File

@ -18,6 +18,7 @@
#
import pytest, time, os, pdb
from bip32 import BIP32Node
from base58 import encode_base58_checksum
from constants import simulator_fixed_words, simulator_fixed_xprv
from test_ephemeral import SEEDVAULT_TEST_DATA, WORDLISTS
from test_ephemeral import confirm_tmp_seed, verify_ephemeral_secret_ui
@ -111,6 +112,7 @@ def test_menu_contents(set_hobble, pick_menu_item, cap_menu, en_okeys, en_notes,
if en_okeys:
adv_expect.add('Temporary Seed')
adv_expect.add('WIF Store')
m = cap_menu()
assert set(m) == adv_expect, "Adv menu wrong"
@ -394,6 +396,38 @@ def test_h_tempseeds(mode, set_hobble, pick_menu_item, cap_menu, settings_set, i
press_select()
@pytest.mark.parametrize('en_okeys', [ True, False])
def test_h_wif_store(en_okeys, set_hobble, settings_remove, import_wif_to_store, goto_home,
cap_menu, pick_menu_item):
settings_remove("wifs")
wif_list = [
encode_base58_checksum(bytes([239]) + os.urandom(32) + b'\x01')
for _ in range(3)
]
import_wif_to_store(wif_list)
goto_home()
set_hobble(True, {'okeys'} if en_okeys else {})
pick_menu_item("Advanced/Tools")
if en_okeys:
pick_menu_item("WIF Store")
time.sleep(.1)
menu = cap_menu()
# check it is read-only
assert "Import WIF" not in menu
assert "Clear All" not in menu
pick_menu_item(menu[0])
time.sleep(.1)
menu = cap_menu()
assert "Delete" not in menu
else:
assert "WIF Store" not in cap_menu()
@pytest.mark.parametrize('en_okeys', [ True, False])
def test_h_usbcmds(en_okeys, set_hobble, dev):
# test various usb commands are blocked during hobble

View File

@ -6,6 +6,7 @@ import pytest, time, os, itertools, hashlib, json
from bip32 import BIP32Node
from msg import verify_message, RFC_SIGNATURE_TEMPLATE, sign_message, parse_signed_message
from base64 import b64encode, b64decode
from base58 import encode_base58_checksum
from ckcc_protocol.protocol import CCProtocolPacker, CCProtoError, CCUserRefused
from ckcc_protocol.constants import *
from constants import addr_fmt_names, msg_sign_unmap_addr_fmt
@ -1040,4 +1041,65 @@ def test_verify_scanned_signed_msg(msg, scan_a_qr, need_keypress, goto_home, cap
assert "Good signature by address" in story
assert addr == addr_from_display_format(story.split("\n")[-1])
@pytest.mark.parametrize("way,af", [
("sd", "P2SH-Segwit"),
("input", "Segwit P2WPKH"),
("nfc", "Classic P2PKH")
])
def test_sign_msg_with_wif_store_key(way, af, settings_remove, import_wif_to_store, cap_menu,
pick_menu_item, cap_story, need_keypress, press_nfc,
enter_complex, garbage_collector, microsd_path, nfc_write_text,
verify_msg_sign_story, msg_sign_export, press_select, goto_home):
settings_remove("wifs")
msg = "Coinkite"
n = BIP32Node.from_master_secret(os.urandom(32))
privkey = n.node.private_key
import_wif_to_store([encode_base58_checksum(bytes([239]) + bytes(privkey) + b'\x01')])
menu = cap_menu()
assert len(menu) == 2
pick_menu_item(menu[1])
pick_menu_item("Sign MSG")
pick_menu_item(af)
if way == "input":
need_keypress("0")
enter_complex(msg, apply=False, b39pass=False)
elif way == "sd":
name = "msg_to_sign.txt"
pth = microsd_path(name)
with open(pth, "w") as f:
f.write(msg)
need_keypress("1")
pick_menu_item(name)
elif way == "nfc":
press_nfc()
time.sleep(0.2)
nfc_write_text(msg)
time.sleep(0.3)
else:
raise NotImplementedError
time.sleep(.1)
title, story = cap_story()
addr_fmt = {"P2SH-Segwit": "p2sh-p2wpkh",
"Segwit P2WPKH": "p2wpkh",
"Classic P2PKH": "p2pkh"}[af]
target_addr = n.address(addr_fmt=addr_fmt)
verify_msg_sign_story(story, msg, "m", addr=target_addr)
press_select()
res = msg_sign_export(way if way != "input" else "sd")
assert target_addr in res
pmsg, addr, sig = parse_signed_message(res)
assert pmsg == msg
assert verify_message(addr, sig, msg) is True
goto_home()
# EOF

View File

@ -4275,4 +4275,68 @@ def test_txin_explorer_our_sig(dev, fake_ms_txn, start_sign, settings_set, clear
start_sign(psbt)
txin_explorer(num_ins, [(af, inp_amount, 0, "XTN", (M,N), None, None, False, [my_xfp])])
@pytest.mark.parametrize("addr_fmt", ["p2wsh", "p2sh-p2wsh", "p2sh"])
def test_wif_store(addr_fmt, dev, fake_ms_txn, start_sign, settings_set, clear_ms,
cap_story, pytestconfig, import_ms_wallet, end_sign, settings_remove):
# TODO This test MUST be run with --psbt2 flag on and off
clear_ms()
settings_remove("wifs")
M, N = 3, 5
if addr_fmt == AF_P2SH:
dd = "m/45h"
elif addr_fmt == AF_P2WSH:
dd = "m/48h/1h/0h/2h"
else:
dd = "m/48h/1h/0h/1h"
def path_mapper(idx):
kk = str_to_path(dd)
return kk + [0,0]
keys = import_ms_wallet(M, N, name='wif_store', accept=True, netcode="XTN",
descriptor=True, addr_fmt=addr_fmt, common=dd)
psbt = fake_ms_txn(1, 1, M, keys, inp_af=unmap_addr_fmt[addr_fmt],
path_mapper=path_mapper, psbt_v2=pytestconfig.getoption('psbt2'))
# sign with master key first - nothing in WIF store
# without warning
# one signature from master added
start_sign(psbt)
title, story = cap_story()
assert "warning" not in story
signed = end_sign()
po = BasicPSBT().parse(signed)
assert len(po.inputs[0].part_sigs) == 1
# add privkey from 0th & 1st node to WIF store
der_node0 = keys[0][1].subkey_for_path(dd[2:] + "/0/0")
sk0 = bytes(der_node0.node.private_key).hex()
pk0 = der_node0.node.private_key.K.sec().hex()
der_node1 = keys[1][1].subkey_for_path(dd[2:] + "/0/0")
sk1 = bytes(der_node1.node.private_key).hex()
pk1 = der_node1.node.private_key.K.sec().hex()
settings_set("wifs", [(pk0,sk0), (pk1,sk1)])
# ofe of the private keys will be used for signing
# only one as we cannot sign with 2 keys in one sitting
start_sign(signed)
title, story = cap_story()
assert "warning" in story
assert "WIF store" in story
signed = end_sign()
po = BasicPSBT().parse(signed)
assert len(po.inputs[0].part_sigs) == 2
# sign with other key - keys that already have signatures are ignored
# that is why we can proceed with this iterative method
start_sign(signed, finalize=True)
title, story = cap_story()
assert "warning" in story
assert "WIF store" in story
end_sign(finalize=True)
# EOF

View File

@ -2,7 +2,7 @@
#
# Address ownership tests.
#
import pytest, time, io, csv
import pytest, time, io, csv, os
from txn import fake_address
from base58 import encode_base58_checksum
from bech32 import encode as bech32_encode
@ -699,4 +699,58 @@ def test_named_wallet_search(wname, valid, method, clear_ms, import_ms_wallet, i
assert "1 wallet(s)" in story
assert 'without finding a match' in story
@pytest.mark.parametrize("addr_fmt", ["p2wpkh", "p2sh-p2wpkh", "p2pkh"])
@pytest.mark.parametrize("idx", [1, 3])
def test_wif_store(addr_fmt, idx, is_q1, goto_home, pick_menu_item, scan_a_qr, cap_story, need_keypress,
src_root_dir, sim_root_dir, nfc_write, settings_remove, import_wif_to_store,
load_shared_mod):
settings_remove("wifs")
n = BIP32Node.from_master_secret(os.urandom(32))
privkey = n.node.private_key
addr = n.address(addr_fmt=addr_fmt)
wif = encode_base58_checksum(bytes([239]) + bytes(privkey) + b'\x01')
wif1 = encode_base58_checksum(bytes([239]) + os.urandom(32) + b'\x01')
wif2 = encode_base58_checksum(bytes([239]) + os.urandom(32) + b'\x01')
if idx == 1:
wif_list = [wif, wif1, wif2]
else:
wif_list = [wif1, wif2, wif]
import_wif_to_store(wif_list)
goto_home()
if is_q1:
pick_menu_item('Scan Any QR Code')
scan_a_qr(addr)
time.sleep(1)
title, story = cap_story()
assert addr == addr_from_display_format(story.split("\n\n")[0])
assert '(1) to verify ownership' in story
need_keypress('1')
else:
cc_ndef = load_shared_mod('cc_ndef', f'{src_root_dir}/shared/ndef.py')
n = cc_ndef.ndefMaker()
n.add_text(addr)
ccfile = n.bytes()
pick_menu_item('Advanced/Tools')
pick_menu_item('NFC Tools')
pick_menu_item('Verify Address')
with open(f'{sim_root_dir}/debug/nfc-addr.ndef', 'wb') as f:
f.write(ccfile)
nfc_write(ccfile)
time.sleep(1)
title, story = cap_story()
assert addr == addr_from_display_format(story.split("\n\n")[0])
assert f"Found in WIF store at index {idx}" in story
# EOF

View File

@ -3674,4 +3674,152 @@ def test_txid_qr(fake_txn, start_sign, cap_story, press_cancel, press_select):
assert "(6) for QR Code of TXID" in story
press_cancel()
@pytest.mark.parametrize("num_ins", [1, 5])
@pytest.mark.parametrize("addr_fmt", ["p2pkh", "p2wpkh", "p2sh-p2wpkh"])
def test_wif_store_signing(num_ins, addr_fmt, fake_txn, goto_home, pick_menu_item, need_keypress,
start_sign, end_sign, cap_menu, cap_story, press_cancel, settings_remove,
press_select, import_wif_to_store):
settings_remove("wifs")
wrap = False
if addr_fmt == "p2pkh":
sw = False
elif addr_fmt == "p2wpkh":
sw = True
elif addr_fmt == "p2sh-p2wpkh":
wrap = True
sw = True
else:
raise ValueError
node = BIP32Node.from_master_secret(os.urandom(32))
psbt = fake_txn(num_ins, 1, segwit_in=sw, wrapped=wrap, master_xpub=node.hwif())
wifs = []
privkeys = []
for i in range(num_ins):
n = node.subkey_for_path("0/%d" % i)
sk = n.node.private_key
privkeys.append(sk)
wifs.append(n.node.private_key.wif(testnet=True))
import_wif_to_store(wifs)
menu = cap_menu()
assert menu[0] == "Import WIF"
start_sign(psbt, finalize=True)
time.sleep(.1)
title, story = cap_story()
assert "warning" in story
if num_ins == 1:
assert "WIF store: 0" in story
else:
assert f"WIF store: {', '.join([str(i) for i in range(num_ins)])}" in story
end_sign(finalize=True)
@pytest.mark.parametrize("der_paths", [True, False])
@pytest.mark.parametrize("complete", [True, False])
def test_wif_store_multi(der_paths, complete, fake_txn, start_sign, end_sign, cap_story, settings_set):
wifs = []
hack = None
if der_paths:
def hack(psbt):
new_paths = {}
for k, v in psbt.inputs[0].bip32_paths.items():
new_paths[k] = b"\x01" * 8 # garbage (do not use zero xfp here)
psbt.inputs[0].bip32_paths = new_paths
node = BIP32Node.from_master_secret(os.urandom(32))
psbt = fake_txn(1, 1, segwit_in=True, master_xpub=node.hwif(), psbt_v2=True, outvals=[1E8*3],
psbt_hacker=hack)
po = BasicPSBT().parse(psbt)
n = node.subkey_for_path("0/0")
sk = bytes(n.node.private_key).hex()
pk = n.node.private_key.K.sec().hex()
wifs.append((pk, sk))
node = BIP32Node.from_master_secret(os.urandom(32))
psbt = fake_txn(1, 1, segwit_in=False, master_xpub=node.hwif(), psbt_v2=True, psbt_hacker=hack)
tmp = BasicPSBT().parse(psbt)
po.inputs += tmp.inputs
po.input_count += 1
n = node.subkey_for_path("0/0")
sk = bytes(n.node.private_key).hex()
pk = n.node.private_key.K.sec().hex()
wifs.append((pk, sk))
node = BIP32Node.from_master_secret(os.urandom(32))
psbt = fake_txn(1, 1, segwit_in=True, wrapped=True, master_xpub=node.hwif(), psbt_v2=True,
psbt_hacker=hack)
tmp = BasicPSBT().parse(psbt)
po.inputs += tmp.inputs
po.input_count += 1
n = node.subkey_for_path("0/0")
sk = bytes(n.node.private_key).hex()
pk = n.node.private_key.K.sec().hex()
wifs.append((pk, sk))
# pretend we have those imported
if not complete:
wifs = wifs[:-1]
settings_set("wifs", wifs)
start_sign(po.as_bytes(), finalize=complete)
title, story = cap_story()
assert "warning" in story
if complete:
assert "WIF store: 0, 1, 2" in story
else:
assert "WIF store: 0, 1" in story
assert "Limited Signing" in story
end_sign(finalize=complete)
def test_wif_store_with_master(fake_txn, start_sign, end_sign, cap_story, settings_set):
# signs both master key and keys from WIF store
wifs = []
node = BIP32Node.from_master_secret(os.urandom(32))
psbt = fake_txn(1, 1, segwit_in=True, master_xpub=node.hwif(), psbt_v2=True, outvals=[1E8*3])
po = BasicPSBT().parse(psbt)
n = node.subkey_for_path("0/0")
sk = bytes(n.node.private_key).hex()
pk = n.node.private_key.K.sec().hex()
wifs.append((pk, sk))
node = BIP32Node.from_master_secret(os.urandom(32))
psbt = fake_txn(1, 1, segwit_in=False, master_xpub=node.hwif(), psbt_v2=True)
tmp = BasicPSBT().parse(psbt)
po.inputs += tmp.inputs
po.input_count += 1
n = node.subkey_for_path("0/0")
sk = bytes(n.node.private_key).hex()
pk = n.node.private_key.K.sec().hex()
wifs.append((pk, sk))
# add simulator input
psbt = fake_txn(1, 1, segwit_in=True, psbt_v2=True)
tmp = BasicPSBT().parse(psbt)
po.inputs += tmp.inputs
po.input_count += 1
settings_set("wifs", wifs)
# convert to v0 PSBT just for fun
start_sign(po.to_v0(), finalize=True)
title, story = cap_story()
assert "warning" in story
assert "WIF store: 0, 1" in story
end_sign(finalize=True)
# EOF

View File

@ -1,11 +1,12 @@
# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
import pytest, time, os, re, hashlib, shutil
from helpers import xfp2str, prandom
from charcodes import KEY_DOWN, KEY_QR, KEY_NFC, KEY_DELETE, KEY_CANCEL
from helpers import xfp2str, prandom, addr_from_display_format
from charcodes import KEY_DOWN, KEY_QR, KEY_NFC, KEY_DELETE, KEY_UP
from constants import AF_CLASSIC, simulator_fixed_words, simulator_fixed_xfp
from mnemonic import Mnemonic
from bip32 import BIP32Node
from bip32 import BIP32Node, PrivateKey
from base58 import encode_base58_checksum
mnem = Mnemonic('english')
wordlist = mnem.wordlist
@ -1229,6 +1230,331 @@ def test_file_picker_suffixes(pick_menu_item, goto_home, cap_story, microsd_wipe
microsd_wipe()
@pytest.mark.parametrize("num_wifs", [1, 11])
@pytest.mark.parametrize("separator", ["\n", ','])
@pytest.mark.parametrize("way", ["sd", "nfc", "qr", "vdisk"])
def test_wif_store_import(num_wifs, separator, way, import_wif_to_store, skip_if_useless_way,
settings_remove, goto_home):
skip_if_useless_way(way)
settings_remove("wifs")
wif_list = [
encode_base58_checksum(bytes([239]) + os.urandom(32) + b'\x01')
for _ in range(num_wifs)
]
import_wif_to_store(wif_list, way=way, sep=separator)
goto_home()
def test_wif_store_import_manual(import_wif_to_store, settings_remove, goto_home):
settings_remove("wifs")
wif_list = [
encode_base58_checksum(bytes([239]) + os.urandom(32) + b'\x01')
]
import_wif_to_store(wif_list, way="input")
goto_home()
@pytest.mark.parametrize("wif,err,way", [
("cWALDjUu1tszsCBMjBjL4mhYj2wHUWYDR8Q8aSjLKzjkWaXMLRaY", "wif decode", "sd"), # curve order
("cMahea7zqjxrtgAbB7LSGbcQUr1uX1ojuat9jZodMN87J7g8rY9t", "wif decode", "nfc"), # zero
("Ky2BtsR8qRN91PjktxaTQWMgJZUWSBJLjwip642vvoNyH1PeEpUP", "chain", "qr"), # mainnet key on testnet
("91zb4oYGEvwEroihAbkdeoBpLSKnZYMdD1CPhfQD76fxrfNSp5J", "compressed only", "sd"), # uncompressed pk
("cPPBMnQzGV4QAqD2HNPamprjvnmv6dQ2oysHCUVSRv2yXkVvWVtX", "wif decode", "nfc"), # wrong csum
("cPPBMnQzGV4QAqD2HNPamprjvnmv6dQ2oysHCUVSRv2yXkVvWVtX;cN7M6sNzn4LGBxAozsmphxjuxVNaHcLre7Nm163qM3DpY3BZog1v", "wif decode", "sd"), # wrong separator
])
def test_wif_store_import_fail(way, wif, err, import_wif_to_store, skip_if_useless_way,
settings_remove, press_select, cap_story, use_testnet, settings_get):
skip_if_useless_way(way)
use_testnet()
settings_remove("wifs")
import_wif_to_store([wif], way=way, early_exit=True)
time.sleep(.1)
title, story = cap_story()
assert "Failed to import WIF" in story
assert err in story
press_select()
assert not settings_get("wifs")
@pytest.mark.parametrize("netcode", ["XTN", "BTC"])
def test_wif_store_detail(netcode, import_wif_to_store, use_mainnet, cap_menu, pick_menu_item,
cap_story, need_keypress, settings_remove, cap_screen_qr, is_q1,
press_cancel, nfc_is_enabled, press_nfc, nfc_read_text, goto_home):
goto_home()
if netcode == "BTC":
use_mainnet()
settings_remove("wifs")
prefix = bytes([128]) if netcode == "BTC" else bytes([239])
privkeys = [PrivateKey.parse(os.urandom(32)) for _ in range(5)]
wif_list = [
encode_base58_checksum(prefix + bytes(sk) + b'\x01')
for sk in privkeys
]
import_wif_to_store(wif_list)
time.sleep(.1)
menu = cap_menu()
target_mi = []
for mi in menu:
if "" in mi:
target_mi.append(mi)
assert len(target_mi) == len(wif_list)
for mi, wif, sk in zip(target_mi, wif_list, privkeys):
mi_split = mi.split(" ")[-1].split("")
assert len(mi_split) == 2
assert mi_split[0] in wif
assert mi_split[1] in wif
pick_menu_item(mi)
time.sleep(.1)
menu = cap_menu()
assert menu[0] == "Detail"
assert menu[1] == "Addresses"
assert menu[2] == "Sign MSG"
assert menu[3] == "Delete"
pick_menu_item("Detail")
title, story = cap_story()
assert title == "WIF"
split_story = story.split("\n\n")
story_wif = split_story[0]
story_sk = split_story[1].split("\n")[-1]
story_pk = split_story[2].split("\n")[-1]
assert f'{KEY_QR if is_q1 else "(4)"} to show QR code' in story
need_keypress(KEY_QR if is_q1 else "4")
time.sleep(.1)
wif_qr = cap_screen_qr().decode()
press_cancel()
if nfc_is_enabled():
assert f"{KEY_NFC if is_q1 else '(3)'} to share via NFC" in story
press_nfc()
time.sleep(0.3)
nfc_wif = nfc_read_text()
time.sleep(0.3)
press_cancel()
assert nfc_wif == wif
assert story_wif == wif == wif_qr
assert story_sk == bytes(sk).hex()
assert story_pk == sk.K.sec().hex()
press_cancel() # exit Detail
press_cancel() # exit WIF submenu
@pytest.mark.parametrize("netcode", ["XTN", "BTC"])
def test_wif_store_addresses(netcode, import_wif_to_store, use_mainnet, cap_menu, pick_menu_item,
cap_story, need_keypress, settings_remove, cap_screen_qr, is_q1,
nfc_is_enabled, press_nfc, nfc_read_text, goto_home, press_cancel):
goto_home()
if netcode == "BTC":
use_mainnet()
settings_remove("wifs")
prefix = bytes([128]) if netcode == "BTC" else bytes([239])
n = BIP32Node.from_master_secret(os.urandom(32))
privkey = n.node.private_key
wif_list = [
encode_base58_checksum(prefix + bytes(privkey) + b'\x01')
]
import_wif_to_store(wif_list)
time.sleep(.1)
menu = cap_menu()
assert len(menu) == 2
pick_menu_item(menu[1])
pick_menu_item("Addresses")
for mi, af in [("P2SH-Segwit", "p2sh-p2wpkh"), ("Segwit P2WPKH", "p2wpkh"), ("Classic P2PKH", "p2pkh")]:
pick_menu_item(mi)
time.sleep(.1)
title, story = cap_story()
if is_q1:
# Q has title as it needs hint keys
assert title == mi
target_addr = n.address(addr_fmt=af, netcode=netcode)
addr = addr_from_display_format(story.split("\n\n")[0])
assert addr == target_addr
if not is_q1:
assert "Press (1) to show address QR code." in story
need_keypress(KEY_QR if is_q1 else "1")
time.sleep(.1)
qr_addr = cap_screen_qr().decode()
if af == "p2wpkh":
qr_addr = qr_addr.lower()
press_cancel()
assert qr_addr == target_addr
if nfc_is_enabled():
if not is_q1:
assert "(3) to share via NFC." in story
press_nfc()
time.sleep(0.3)
nfc_addr = nfc_read_text()
time.sleep(0.3)
press_cancel()
assert nfc_addr == target_addr
press_cancel()
press_cancel()
press_cancel()
def test_wif_store_clear_all(import_wif_to_store, press_select, cap_story, settings_get,
need_keypress, cap_menu, settings_remove, is_q1, goto_home):
goto_home()
settings_remove("wifs")
wif_list = [
encode_base58_checksum(bytes([239]) + os.urandom(32) + b'\x01')
for _ in range(30) # MAX
]
import_wif_to_store(wif_list)
time.sleep(.1)
menu = cap_menu()
assert "Import WIF" not in menu # WIF store is full
assert "Clear All" in menu
need_keypress(KEY_UP if is_q1 else "5")
time.sleep(.1)
press_select() # on Clear All
time.sleep(.1)
title, story = cap_story()
assert "Remove all saved WIF keys?" in story
assert "(4)" in story
press_select() # does not work & gets you back to menu
assert len(settings_get("wifs")) == 30
press_select() # on Clear All
time.sleep(.1)
need_keypress("4")
time.sleep(.1)
menu = cap_menu()
assert len(menu) == 2
assert "(none yet)" in menu
assert "Import WIF" in menu
assert not settings_get("wifs")
def test_wif_store_capacity(import_wif_to_store, settings_remove, press_select, cap_story,
settings_get, cap_menu, pick_menu_item, need_keypress):
settings_remove("wifs")
wif_list = [
encode_base58_checksum(bytes([239]) + os.urandom(32) + b'\x01')
for _ in range(40) # MAX+1
]
import_wif_to_store(wif_list[:31], early_exit=True)
time.sleep(.1)
title, story = cap_story()
assert title == "Failure"
assert "Max 30 items allowed in WIF Store" in story
assert "Attempted to import 31 keys" in story
assert "remaining WIF store capacity is only 30"
press_select()
assert not settings_get("wifs")
# import 29 keys
import_wif_to_store(wif_list[:29])
assert len(settings_get("wifs", [])) == 29
import_wif_to_store(wif_list[-2:], early_exit=True)
time.sleep(.1)
title, story = cap_story()
assert title == "Failure"
assert "Max 30 items allowed in WIF Store" in story
assert "Attempted to import 2 keys" in story
assert "remaining WIF store capacity is only 1"
press_select()
assert len(settings_get("wifs", [])) == 29
import_wif_to_store(wif_list[-1:])
assert len(settings_get("wifs", [])) == 30
menu = cap_menu()
assert "Import WIF" not in menu
# remove random key to make space
# pick key at current menu item position
press_select()
time.sleep(.1)
pick_menu_item("Delete")
time.sleep(.1)
title, story = cap_story()
assert "Delete WIF key?" in story
press_select()
time.sleep(.1)
menu = cap_menu()
assert "Import WIF" in menu
def test_wif_store_import_duplicate(settings_remove, import_wif_to_store, settings_get, cap_menu,
goto_home):
goto_home()
settings_remove("wifs")
wif_list = [
encode_base58_checksum(bytes([239]) + os.urandom(32) + b'\x01')
for _ in range(4)
]
import_wif_to_store(wif_list)
b4 = cap_menu()
assert len(settings_get("wifs")) == 4
import_wif_to_store(wif_list, early_exit=True)
assert len(settings_get("wifs")) == 4
assert len(b4) == len(cap_menu())
import_wif_to_store(wif_list[:1], early_exit=True)
assert len(b4) == len(cap_menu())
assert len(settings_get("wifs")) == 4
@pytest.mark.parametrize("way", ["qr", "sd", "nfc"])
def test_wif_store_export_all(way, goto_home, settings_remove, import_wif_to_store, pick_menu_item,
load_export):
goto_home()
settings_remove("wifs")
wif_list = [
encode_base58_checksum(bytes([239]) + os.urandom(32) + b'\x01')
for _ in range(6) # 6*52 chars so it can be shown on mk4 too
]
import_wif_to_store(wif_list)
time.sleep(.1)
pick_menu_item("Export All")
conts = load_export(way, "WIF Store", is_json=False, sig_check=False)
assert wif_list == conts.split("\n")
@pytest.mark.onetime
def test_dump_menutree(sim_execfile):
# saves to ../unix/work/menudump.txt