forked from Coldcard/firmware
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpaper.py
310 lines (251 loc) · 12.1 KB
/
paper.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
# (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 dice 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('Pick PDF template to use, or X for none.',
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 auth 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\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)
# annoying?
# always have them pick the template, because that's mostly required
#if rv.can_do_qr():
# await rv.pick_template()
rv.update_menu()
return menu
# EOF