347 lines
9.8 KiB
Python
347 lines
9.8 KiB
Python
# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
|
#
|
|
# Tests for paper-wallet feature
|
|
#
|
|
# Paper wallet features MUST work on both device with and without secrets.
|
|
# This module can and should be run with `-l` and without it.
|
|
#
|
|
import random
|
|
|
|
import pytest, time, os, shutil, re, random, json
|
|
from pycoin.key.Key import Key
|
|
from pycoin.encoding import from_bytes_32
|
|
from binascii import a2b_hex
|
|
from hashlib import sha256
|
|
from ckcc_protocol.constants import *
|
|
|
|
|
|
@pytest.mark.bitcoind
|
|
@pytest.mark.parametrize('mode', [ "classic", 'segwit', 'taproot'])
|
|
@pytest.mark.parametrize('netcode', [ "BTC", "XRT", "XTN"])
|
|
@pytest.mark.parametrize('pdf', [ False, True])
|
|
def test_generate(mode, netcode, pdf, cap_menu, pick_menu_item, goto_home, cap_story, need_keypress,
|
|
dev, microsd_path, verify_detached_signature_file, validate_address, bitcoind,
|
|
settings_set):
|
|
|
|
# test UX and operation of the 'bitcoin core' wallet export
|
|
mx = "Don't make PDF"
|
|
|
|
settings_set("chain", netcode)
|
|
|
|
goto_home()
|
|
pick_menu_item('Advanced/Tools')
|
|
try:
|
|
pick_menu_item('Paper Wallets')
|
|
except:
|
|
raise pytest.skip('Feature absent')
|
|
|
|
time.sleep(0.1)
|
|
title, story = cap_story()
|
|
|
|
assert 'pick a random' in story
|
|
assert 'MANY RISKS' in story
|
|
|
|
need_keypress('y')
|
|
|
|
time.sleep(0.1)
|
|
if mode == 'segwit':
|
|
pick_menu_item('Classic P2PKH')
|
|
pick_menu_item('Segwit P2WPKH')
|
|
time.sleep(0.5)
|
|
|
|
if mode == 'taproot':
|
|
pick_menu_item('Classic P2PKH')
|
|
pick_menu_item('Taproot P2TR')
|
|
time.sleep(0.5)
|
|
|
|
if pdf:
|
|
assert mx in cap_menu()
|
|
shutil.copy('../docs/paperwallet.pdf', microsd_path('paperwallet.pdf'))
|
|
pick_menu_item(mx)
|
|
|
|
time.sleep(0.2)
|
|
title, story = cap_story()
|
|
assert 'Pick PDF' in story
|
|
need_keypress('y')
|
|
|
|
pick_menu_item('paperwallet.pdf')
|
|
|
|
|
|
pick_menu_item('GENERATE WALLET')
|
|
|
|
time.sleep(0.2)
|
|
title, story = cap_story()
|
|
if "Press (1)" in story:
|
|
need_keypress("1")
|
|
time.sleep(0.2)
|
|
title, story = cap_story()
|
|
|
|
assert 'Created file' in story
|
|
|
|
story = [i for i in story.split('\n') if i]
|
|
sig_file = story[-1]
|
|
|
|
if not pdf:
|
|
if mode == "taproot":
|
|
fname = story[-1]
|
|
else:
|
|
fname = story[-2]
|
|
fnames = [fname]
|
|
else:
|
|
if mode == "taproot":
|
|
fname = story[-2]
|
|
pdf_name = story[-1]
|
|
else:
|
|
fname = story[-3]
|
|
pdf_name = story[-2]
|
|
fnames = [fname, pdf_name]
|
|
assert pdf_name.endswith('.pdf')
|
|
|
|
assert fname.endswith('.txt')
|
|
|
|
if mode != 'taproot':
|
|
assert sig_file.endswith(".sig")
|
|
verify_detached_signature_file(fnames, sig_file, "sd",
|
|
addr_fmt=AF_CLASSIC if mode == "classic" else AF_P2WPKH)
|
|
|
|
path = microsd_path(fname)
|
|
_wif = None
|
|
_sk = None
|
|
_addr = None
|
|
_idesc = None
|
|
with open(path, 'rt') as fp:
|
|
hdr = None
|
|
for ln in fp:
|
|
ln = ln.rstrip()
|
|
if not ln: continue
|
|
|
|
if ln[0] != ' ':
|
|
hdr = ln
|
|
continue
|
|
|
|
if '█' in ln:
|
|
continue
|
|
|
|
val = ln.strip()
|
|
if 'Deposit address' in hdr:
|
|
assert val == fname.split('.', 1)[0].split('-', 1)[0]
|
|
_addr = val
|
|
elif hdr == 'Private key:': # for QR case
|
|
assert val == _wif
|
|
elif 'Private key' in hdr and 'WIF=Wallet' in hdr:
|
|
_wif = val
|
|
elif 'Private key' in hdr and 'Hex, 32 bytes' in hdr:
|
|
_sk = val
|
|
elif 'Bitcoin Core command':
|
|
assert _wif in val
|
|
if 'importdescriptors' in val:
|
|
_idesc = val
|
|
assert 'importprivkey' in val or 'importdescriptors' in val
|
|
else:
|
|
print(f'{hdr} => {val}')
|
|
raise ValueError(hdr)
|
|
|
|
os.unlink(path)
|
|
|
|
if netcode != "XRT":
|
|
k1 = Key.from_text(_wif)
|
|
k2 = Key(secret_exponent=from_bytes_32(a2b_hex(_sk)), is_compressed=True)
|
|
assert k2.secret_exponent() == k1.secret_exponent()
|
|
validate_address(_addr, k1)
|
|
# validate address is chain agnostic - which should be changed at some point
|
|
if mode == "segwit":
|
|
if netcode == "BTC":
|
|
assert _addr.startswith("bc1q")
|
|
else:
|
|
assert _addr.startswith("tb1q")
|
|
elif mode == "taproot":
|
|
if netcode == "BTC":
|
|
assert _addr.startswith("bc1p")
|
|
else:
|
|
assert _addr.startswith("tb1p")
|
|
else:
|
|
if netcode == "BTC":
|
|
assert _addr.startswith("1")
|
|
else:
|
|
assert _addr[0] in "mn"
|
|
else:
|
|
if mode == "segwit":
|
|
assert _addr.startswith("bcrt1q")
|
|
elif mode == "taproot":
|
|
assert _addr.startswith("bcrt1p")
|
|
else:
|
|
assert _addr[0] in "mn"
|
|
# bitcoind on regtest
|
|
conn = bitcoind.create_wallet(wallet_name="paper", disable_private_keys=False, blank=True,
|
|
passphrase=None, avoid_reuse=False, descriptors=True)
|
|
desc_obj_s, desc_obj_e = _idesc.find("["), _idesc.find("]") + 1
|
|
desc_obj = json.loads(_idesc[desc_obj_s:desc_obj_e])
|
|
desc = desc_obj[0]["desc"]
|
|
res = conn.importdescriptors(desc_obj)
|
|
assert res[0]["success"]
|
|
assert _addr == conn.deriveaddresses(desc)[0]
|
|
bitcoind.delete_wallet_files()
|
|
|
|
if not pdf: return
|
|
|
|
path = microsd_path(pdf_name)
|
|
with open(path, 'rb') as fp:
|
|
|
|
d = fp.read()
|
|
assert _wif.encode('ascii') in d
|
|
assert _addr.encode('ascii') in d
|
|
|
|
os.unlink(path)
|
|
|
|
|
|
@pytest.mark.parametrize('rolls', [ '123123', '123'*30] )
|
|
def test_dice_generate_failure_num_attempts(rolls, dev, cap_menu, pick_menu_item, goto_home, cap_story, need_keypress,
|
|
microsd_path):
|
|
# verify the math for dice rolling method
|
|
|
|
goto_home()
|
|
pick_menu_item('Advanced/Tools')
|
|
try:
|
|
pick_menu_item('Paper Wallets')
|
|
except:
|
|
raise pytest.skip('Feature absent')
|
|
|
|
time.sleep(0.1)
|
|
title, story = cap_story()
|
|
|
|
assert 'pick a random' in story
|
|
assert 'MANY RISKS' in story
|
|
|
|
need_keypress('y')
|
|
|
|
time.sleep(0.1)
|
|
pick_menu_item('Use Dice')
|
|
|
|
for ch in rolls:
|
|
time.sleep(0.01)
|
|
need_keypress(ch)
|
|
|
|
need_keypress('y')
|
|
time.sleep(0.1)
|
|
title, story = cap_story()
|
|
assert 'Not enough dice rolls!!!' in story
|
|
assert 'For 256-bit security you need at least 99 rolls' in story
|
|
assert 'Press OK to add more dice rolls. X to exit' in story
|
|
need_keypress('x')
|
|
|
|
@pytest.mark.parametrize('rolls', ['123'*34, "1"*99, "64"*50])
|
|
def test_dice_generate_failure_distribution(rolls, dev, cap_menu, pick_menu_item, goto_home, cap_story, need_keypress,
|
|
microsd_path):
|
|
# verify the math for dice rolling method
|
|
|
|
goto_home()
|
|
pick_menu_item('Advanced/Tools')
|
|
try:
|
|
pick_menu_item('Paper Wallets')
|
|
except:
|
|
raise pytest.skip('Feature absent')
|
|
|
|
time.sleep(0.1)
|
|
title, story = cap_story()
|
|
|
|
assert 'pick a random' in story
|
|
assert 'MANY RISKS' in story
|
|
|
|
need_keypress('y')
|
|
|
|
time.sleep(0.1)
|
|
pick_menu_item('Use Dice')
|
|
|
|
for ch in rolls:
|
|
time.sleep(0.01)
|
|
need_keypress(ch)
|
|
|
|
need_keypress('y')
|
|
time.sleep(0.1)
|
|
title, story = cap_story()
|
|
assert 'Distribution of dice rolls is not random' in story
|
|
assert 'Some numbers occurred more than 30% of the time' in story
|
|
# exit
|
|
|
|
@pytest.mark.parametrize('rolls', [
|
|
'123456'*17,
|
|
"".join([str(random.SystemRandom().randint(1,6)) for _ in range(99)]),
|
|
"".join([str(random.SystemRandom().randint(1,6)) for _ in range(99)]),
|
|
])
|
|
@pytest.mark.parametrize('netcode', ["XTN", "BTC"])
|
|
def test_dice_generate(rolls, netcode, dev, cap_menu, pick_menu_item, goto_home,
|
|
cap_story, need_keypress, microsd_path,
|
|
verify_detached_signature_file, settings_set):
|
|
# verify the math for dice rolling method
|
|
|
|
settings_set("chain", netcode)
|
|
|
|
goto_home()
|
|
pick_menu_item('Advanced/Tools')
|
|
try:
|
|
pick_menu_item('Paper Wallets')
|
|
except:
|
|
raise pytest.skip('Feature absent')
|
|
|
|
time.sleep(0.1)
|
|
title, story = cap_story()
|
|
|
|
assert 'pick a random' in story
|
|
assert 'MANY RISKS' in story
|
|
|
|
need_keypress('y')
|
|
|
|
time.sleep(0.1)
|
|
pick_menu_item('Use Dice')
|
|
|
|
for ch in rolls:
|
|
time.sleep(0.01)
|
|
need_keypress(ch)
|
|
|
|
need_keypress('y')
|
|
time.sleep(0.1)
|
|
if len(rolls) < 99:
|
|
title, story = cap_story()
|
|
assert 'need 50' in story
|
|
need_keypress('y')
|
|
|
|
time.sleep(0.4)
|
|
|
|
title, story = cap_story()
|
|
if "Press (1)" in story:
|
|
need_keypress("1")
|
|
time.sleep(0.2)
|
|
title, story = cap_story()
|
|
|
|
assert 'Created file' in story
|
|
|
|
story = [i for i in story.split('\n') if i]
|
|
sig_file = story[-1]
|
|
fname = story[-2]
|
|
|
|
assert sig_file.endswith('.sig')
|
|
assert fname.endswith('.txt')
|
|
_, address = verify_detached_signature_file([fname], sig_file, "sd", addr_fmt=AF_CLASSIC)
|
|
|
|
addr,_ = fname.split('.')
|
|
if '-' in addr:
|
|
# junk in working dir
|
|
addr,_ = addr.split('-')
|
|
|
|
assert addr == address
|
|
|
|
path = microsd_path(fname)
|
|
with open(path, 'rt') as fp:
|
|
hx = re.findall(r'[0-9a-f]{64}', fp.read())
|
|
assert len(hx) == 1
|
|
val, = hx
|
|
|
|
k2 = Key(secret_exponent=from_bytes_32(a2b_hex(val)), is_compressed=True, netcode=netcode)
|
|
assert addr == k2.address()
|
|
|
|
assert val == sha256(rolls.encode('ascii')).hexdigest()
|
|
|
|
os.unlink(path)
|
|
|
|
# EOF
|