305 lines
12 KiB
Python
305 lines
12 KiB
Python
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
|
|
# and is covered by GPLv3 license found in COPYING.
|
|
#
|
|
# paper.py - generate paper wallets, based on random values (not linked to wallet)
|
|
#
|
|
import ujson
|
|
from ubinascii import hexlify as b2a_hex
|
|
from utils import imported
|
|
from public_constants import AF_CLASSIC, AF_P2WPKH
|
|
from ux import ux_show_story, ux_dramatic_pause
|
|
from files import CardSlot, CardMissingError, needs_microsd
|
|
from actions import file_picker
|
|
from menu import MenuSystem, MenuItem
|
|
|
|
background_msg = '''\
|
|
Coldcard will pick a random private key (which has no relation to your seed words), \
|
|
and record the corresponding payment address and private key (WIF) into a text file, \
|
|
creating a so-called "paper wallet".
|
|
{can_qr}
|
|
|
|
Another option is to roll a D6 die many times to generate the key.
|
|
|
|
CAUTION: Paper wallets carry MANY RISKS and should only be used for SMALL AMOUNTS.'''
|
|
|
|
no_templates_msg = '''\
|
|
You don't have any PDF templates to choose from, but plain text wallet files \
|
|
can still be made. Visit the Coldcard website to get some interesting templates.\
|
|
'''
|
|
|
|
SECP256K1_ORDER = b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xba\xae\xdc\xe6\xaf\x48\xa0\x3b\xbf\xd2\x5e\x8c\xd0\x36\x41\x41"
|
|
|
|
# Aprox. time of this feature release (Nov 20/2019) so no need to scan
|
|
# blockchain earlier than this during "importmulti"
|
|
FEATURE_RELEASE_TIME = const(1574277000)
|
|
|
|
# These very-specific text values are matched on the Coldcard; cannot be changed.
|
|
class placeholders:
|
|
addr = b'ADDRESS_XXXXXXXXXXXXXXXXXXXXXXXXXXXXX' # 37 long
|
|
privkey = b'PRIVKEY_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' # 51 long
|
|
|
|
# rather than Tokyo, I chose Chiba Prefecture in ShiftJIS encoding...
|
|
header = b'%PDF-1.3\n%\x90\xe7\x97t\x8c\xa7 Coldcard Paper Wallet Template\n'
|
|
|
|
def template_taster(fn):
|
|
# check if file looks like our special PDF templates... must have right header bits
|
|
hdr = open(fn, 'rb').read(len(placeholders.header))
|
|
return hdr == placeholders.header
|
|
|
|
class PaperWalletMaker:
|
|
def __init__(self, my_menu):
|
|
self.my_menu = my_menu
|
|
self.template_fn = None
|
|
self.is_segwit = False
|
|
|
|
async def pick_template(self, *a):
|
|
fn = await file_picker(suffix='.pdf', min_size=20000, taster=template_taster,
|
|
none_msg=no_templates_msg)
|
|
self.template_fn = fn
|
|
|
|
self.update_menu()
|
|
|
|
def addr_format_chooser(self, *a):
|
|
# simple bool choice
|
|
def set(idx, text):
|
|
self.is_segwit = bool(idx)
|
|
self.update_menu()
|
|
return int(self.is_segwit), ['Classic P2PKH', 'Segwit P2WPKH'], set
|
|
|
|
def update_menu(self):
|
|
# Reconstruct the menu contents based on our state.
|
|
self.my_menu.replace_items([
|
|
MenuItem("Don't make PDF" if not self.template_fn else 'Making PDF',
|
|
f=self.pick_template),
|
|
MenuItem('Classic P2PKH' if not self.is_segwit else 'Segwit P2WPKH',
|
|
chooser=self.addr_format_chooser),
|
|
MenuItem('Use Dice', f=self.use_dice),
|
|
MenuItem('GENERATE WALLET', f=self.doit),
|
|
], keep_position=True)
|
|
|
|
async def doit(self, *a, have_key=None):
|
|
# make the wallet.
|
|
from glob import dis, VD
|
|
|
|
try:
|
|
import ngu
|
|
from msgsign import write_sig_file
|
|
from chains import current_chain
|
|
from serializations import hash160
|
|
from stash import blank_object
|
|
|
|
if not have_key:
|
|
# get some random bytes
|
|
await ux_dramatic_pause("Picking key...", 2)
|
|
pair = ngu.secp256k1.keypair()
|
|
else:
|
|
# caller must range check this already: 0 < privkey < order
|
|
# - actually libsecp256k1 will check it again anyway
|
|
pair = ngu.secp256k1.keypair(have_key)
|
|
|
|
# pull out binary versions (serialized) as we need
|
|
privkey = pair.privkey()
|
|
pubkey = pair.pubkey().to_bytes(False) # always compressed style
|
|
|
|
dis.fullscreen("Rendering...")
|
|
|
|
# make payment address
|
|
digest = hash160(pubkey)
|
|
ch = current_chain()
|
|
if self.is_segwit:
|
|
addr = ngu.codecs.segwit_encode(ch.bech32_hrp, 0, digest)
|
|
else:
|
|
addr = ngu.codecs.b58_encode(ch.b58_addr + digest)
|
|
|
|
wif = ngu.codecs.b58_encode(ch.b58_privkey + privkey + b'\x01')
|
|
|
|
with imported('uqr') as uqr:
|
|
# make the QR's now, since it's slow
|
|
is_alnum = self.is_segwit
|
|
qr_addr = uqr.make(addr if not is_alnum else addr.upper(),
|
|
min_version=4, max_version=4,
|
|
encoding=(uqr.Mode_ALPHANUMERIC if is_alnum else 0))
|
|
|
|
qr_wif = uqr.make(wif, min_version=4, max_version=4, encoding=uqr.Mode_BYTE)
|
|
|
|
# Use address as filename. clearly will be unique, but perhaps a bit
|
|
# awkward to work with.
|
|
basename = addr
|
|
force_vdisk = False
|
|
if VD:
|
|
prompt = "Press (1) to save paper wallet file to SD Card"
|
|
escape = "1"
|
|
if VD is not None:
|
|
prompt += ", press (2) to save to VDisk"
|
|
escape += "2"
|
|
prompt += "."
|
|
ch = await ux_show_story(prompt, escape=escape)
|
|
if ch == "2":
|
|
force_vdisk = True
|
|
elif ch == '1':
|
|
force_vdisk = False
|
|
else:
|
|
return
|
|
dis.fullscreen("Saving...")
|
|
with CardSlot(force_vdisk=force_vdisk) as card:
|
|
fname, nice_txt = card.pick_filename(basename +
|
|
('-note.txt' if self.template_fn else '.txt'))
|
|
sig_cont = []
|
|
with card.open(fname, 'wt+') as fp:
|
|
self.make_txt(fp, addr, wif, privkey, qr_addr, qr_wif)
|
|
fp.seek(0)
|
|
contents0 = fp.read()
|
|
|
|
h = ngu.hash.sha256s(contents0.encode())
|
|
sig_cont.append((h, fname))
|
|
if self.template_fn:
|
|
fname, nice_pdf = card.pick_filename(basename + '.pdf')
|
|
|
|
with open(fname, 'wb+') as fp:
|
|
self.make_pdf(fp, addr, wif, qr_addr, qr_wif)
|
|
fp.seek(0)
|
|
contents1 = fp.read()
|
|
h = ngu.hash.sha256s(contents1)
|
|
sig_cont.append((h, fname))
|
|
else:
|
|
nice_pdf = ''
|
|
|
|
nice_sig = write_sig_file(sig_cont, pk=privkey, sig_name=basename,
|
|
addr_fmt=AF_P2WPKH if self.is_segwit else AF_CLASSIC)
|
|
|
|
# Half-hearted attempt to cleanup secrets-contaminated memory
|
|
# - better would be force user to reboot
|
|
# - and yet, we just output the WIF to SDCard anyway
|
|
blank_object(privkey)
|
|
blank_object(wif)
|
|
del qr_wif
|
|
|
|
except CardMissingError:
|
|
await needs_microsd()
|
|
return
|
|
except Exception as e:
|
|
from utils import problem_file_line
|
|
await ux_show_story('Failed to write!\n\n'+problem_file_line(e))
|
|
return
|
|
|
|
story = "Done! Created file(s):\n\n%s" % nice_txt
|
|
if nice_pdf:
|
|
story += "\n\n%s" % nice_pdf
|
|
story += "\n\n%s" % nice_sig
|
|
await ux_show_story(story)
|
|
|
|
async def use_dice(self, *a):
|
|
# Use lots of (D6) dice rolls to create privkey entropy.
|
|
privkey = b''
|
|
with imported('seed') as seed:
|
|
count, privkey = await seed.add_dice_rolls(0, privkey, True, enforce=True)
|
|
if count == 0: return
|
|
|
|
if privkey >= SECP256K1_ORDER or privkey == bytes(32):
|
|
# lottery won! but not going to waste bytes here preparing to celebrate
|
|
return
|
|
|
|
return await self.doit(have_key=privkey)
|
|
|
|
|
|
def make_txt(self, fp, addr, wif, privkey, qr_addr=None, qr_wif=None):
|
|
# Generate the "simple" text file version, includes private key.
|
|
from descriptor import append_checksum
|
|
|
|
fp.write('Coldcard Generated Paper Wallet\n\n')
|
|
|
|
fp.write('Deposit address:\n\n %s\n\n' % addr)
|
|
fp.write('Private key (WIF=Wallet Import Format):\n\n %s\n\n' % wif)
|
|
fp.write('Private key (Hex, 32 bytes):\n\n %s\n\n' % b2a_hex(privkey).decode('ascii'))
|
|
fp.write('Bitcoin Core command:\n\n')
|
|
|
|
# new hotness: output descriptors
|
|
desc = ('wpkh(%s)' if self.is_segwit else 'pkh(%s)') % wif
|
|
multi = ujson.dumps(dict(timestamp=FEATURE_RELEASE_TIME, desc=append_checksum(desc)))
|
|
fp.write(" bitcoin-cli importmulti '[%s]'\n\n" % multi)
|
|
fp.write('# OR (more compatible, but slower)\n\n bitcoin-cli importprivkey "%s"\n\n' % wif)
|
|
|
|
if qr_addr and qr_wif:
|
|
fp.write('\n\n--- QR Codes --- (requires UTF-8, unicode, white background)\n\n\n\n')
|
|
|
|
for idx, (qr, val) in enumerate([(qr_addr, addr), (qr_wif, wif)]):
|
|
fp.write(('Private key' if idx else 'Deposit address') + ':\n\n')
|
|
|
|
w = qr.width()
|
|
for y in range(w):
|
|
fp.write(' ')
|
|
ln = ''.join('\u2588\u2588' if qr.get(x,y) else ' ' for x in range(w))
|
|
fp.write(ln)
|
|
fp.write('\n')
|
|
|
|
fp.write('\n %s\n\n\n\n' % val)
|
|
|
|
fp.write('\n\n\n')
|
|
|
|
def insert_qr_hex(self, out_fp, qr, width):
|
|
# render QR as binary data: 1 bit per pixel 33x33
|
|
# - aways 8:1 expansion ratio here
|
|
assert qr.width() == width == 33 # only version==4 supported
|
|
for y in range(width):
|
|
ln = b''.join(b'00' if qr.get(x,y) else b'FF' for x in range(width))
|
|
ln += b'\n'
|
|
out_fp.write(ln * 8)
|
|
|
|
def make_pdf(self, out_fp, addr, wif, qr_addr, qr_wif):
|
|
qr_armed, qr_skip = False, False
|
|
addr = addr.encode('ascii')
|
|
wif = wif.encode('ascii')
|
|
|
|
with open(self.template_fn, 'rb') as inp:
|
|
for ln in inp:
|
|
if qr_skip:
|
|
if ln == b'endstream\n':
|
|
qr_skip = False
|
|
else:
|
|
continue
|
|
|
|
if b'Coldcard Paper Wallet Template' in ln:
|
|
# remove ' Template\n' part at end .. so we won't offer this
|
|
# file as a template, next round.
|
|
ln = ln.replace(b' Template', b'')
|
|
elif ln == b'stream\n':
|
|
qr_armed = True
|
|
elif qr_armed:
|
|
if ln[0:6] == b'51523A': # 'QR:' in hex
|
|
# it's the first line of QR hex data
|
|
# - QR:addr vs QR:pk, in hex..
|
|
is_addr = (ln[0:14] == b'51523A61646472')
|
|
self.insert_qr_hex(out_fp, qr_addr if is_addr else qr_wif, (len(ln)-1)//2)
|
|
qr_skip = True
|
|
continue
|
|
else:
|
|
qr_armed = False
|
|
|
|
# replace these text values if they occur
|
|
if b'XXXXXXXXXX' in ln:
|
|
ln = ln.replace(placeholders.addr, addr)
|
|
ln = ln.replace(placeholders.privkey, wif)
|
|
|
|
# typical case: echo the line back out
|
|
out_fp.write(ln)
|
|
|
|
async def make_paper_wallet(*a):
|
|
|
|
msg = background_msg.format(can_qr='\nIf you have a special PDF template file, '
|
|
'it can also make a pretty version of the same data.')
|
|
|
|
if await ux_show_story(msg) != 'y':
|
|
return
|
|
|
|
# show a menu with some settings, and a GO button
|
|
|
|
menu = MenuSystem([])
|
|
rv = PaperWalletMaker(menu)
|
|
|
|
rv.update_menu()
|
|
|
|
return menu
|
|
|
|
|
|
# EOF
|