WIF Store
This commit is contained in:
parent
c19be4f41e
commit
9b131b2eff
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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),
|
||||
]
|
||||
|
||||
@ -59,6 +59,7 @@ freeze_as_mpy('', [
|
||||
'version.py',
|
||||
'wallet.py',
|
||||
'web2fa.py',
|
||||
'wif.py',
|
||||
'xor_seed.py'
|
||||
], opt=0)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 += "!"
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
315
shared/wif.py
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user