/
actions.py
2231 lines (1738 loc) · 69.2 KB
/
actions.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
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# actions.py
#
# Every function here is called directly by a menu item. They should all be async.
#
import ckcc, pyb, version, uasyncio, sys
from uhashlib import sha256
from uasyncio import sleep_ms
from ubinascii import hexlify as b2a_hex
from utils import imported, pretty_short_delay, problem_file_line, import_prompt_builder
from utils import xfp2str, decrypt_tapsigner_backup, B2A, addr_fmt_label
from ux import ux_show_story, the_ux, ux_confirm, ux_dramatic_pause, ux_aborted
from ux import ux_enter_bip32_index, ux_input_text
from export import make_json_wallet, make_summary_file, make_descriptor_wallet_export
from export import make_bitcoin_core_wallet, generate_wasabi_wallet, generate_generic_export
from export import generate_unchained_export, generate_electrum_wallet
from files import CardSlot, CardMissingError, needs_microsd
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR
from glob import settings
from pincodes import pa
from menu import start_chooser
from version import MAX_TXN_LEN
CLEAR_PIN = '999999-999999'
async def start_selftest(*args):
if len(args) and not version.is_factory_mode:
# called from inside menu, not directly
# - mk4 doesn't damage settings, only earlier marks
if not await ux_confirm('''Selftest may destroy settings on other profiles (not seeds). Requires MicroSD card and might have other consequences. Recommended only for factory.'''):
return await ux_aborted()
with imported('selftest') as st:
await st.start_selftest()
settings.save()
async def needs_primary():
# Standard msg shown if action can't be done w/o main PIN
await ux_show_story("Only the holder of the main PIN (not the secondary) can perform this function. Please start over with the main PIN.")
async def show_bag_number(*a):
import callgate
bn = callgate.get_bag_number() or 'UNBAGGED!'
await ux_show_story('''\
Your new Coldcard should have arrived SEALED in a bag with the above number. Please take a moment to confirm the number and look for any signs of tampering.
\n
Take pictures and contact support@coinkite if you have concerns.''', title=bn)
async def accept_terms(*a):
# do nothing if they have accepted the terms once (ever), otherwise
# force them to read message...
if settings.get('terms_ok'):
return
while 1:
ch = await ux_show_story("""\
By using this product, you are accepting our Terms of Sale and Use.
Read the full document at:
https://
coldcardwallet
.com/legal
Press OK to accept terms and continue.""", escape='7')
if ch == 'y':
break
await show_bag_number()
# Note fact they accepted the terms. Annoying to do more than once.
settings.set('terms_ok', 1)
settings.save()
async def view_ident(*a):
# show the XPUB, and other ident on screen
import callgate, stash
tpl = '''\
Master Key Fingerprint:
{xfp}
USB Serial Number:
{serial}
Extended Master Key:
{xpub}
'''
my_xfp = settings.get('xfp', 0)
xpub = settings.get('xpub', None)
msg = tpl.format(xpub=(xpub or '(none yet)'),
xfp=xfp2str(my_xfp),
serial=version.serial_number())
if pa.is_secondary:
msg += '\n(Secondary wallet)\n'
if stash.bip39_passphrase:
msg += '\nBIP-39 passphrase is in effect.\n'
elif pa.tmp_value:
msg += '\nTemporary seed is in effect.\n'
bn = callgate.get_bag_number()
if bn:
msg += '\nShipping Bag:\n %s\n' % bn
if xpub:
msg += '\nPress (3) to show QR code of xpub.'
ch = await ux_show_story(msg, escape=('3' if xpub else None))
if ch == '3':
# show the QR
from ux import show_qr_code
await show_qr_code(xpub, False)
async def show_settings_space(*a):
percentage_capacity = int(settings.get_capacity() * 100)
if percentage_capacity < 10:
percentage_capacity = 10
await ux_show_story('Settings storage space in use:\n\n'
' %d%%' % percentage_capacity)
async def show_mcu_keys_left(*a):
import callgate
avail, used, total = callgate.mcu_key_usage()
await ux_show_story('MCU key slots remaining:\n\n %d of %d' % (avail, total))
async def maybe_dev_menu(*a):
from version import is_devmode
if not is_devmode:
ok = await ux_confirm('Developer features could be used to weaken security or release key material.\n\nDo not proceed unless you know what you are doing and why.')
if not ok:
return None
from flow import DevelopersMenu
return DevelopersMenu
async def dev_enable_vcp(*a):
# Enable USB serial port emulation, for devs.
# Mk3 and earlier only.
#
from usb import is_vcp_active
if is_vcp_active():
await ux_show_story("""The USB virtual serial port is already enabled.""")
return
was = pyb.usb_mode()
pyb.usb_mode(None)
if was and 'MSC' in was:
pyb.usb_mode('VCP+MSC')
else:
pyb.usb_mode('VCP+HID')
# allow REPL access
ckcc.vcp_enabled(True)
await ux_show_story("""\
The USB virtual serial port has now been enabled. Use a real computer to connect to it.""")
async def dev_enable_disk(*a):
# Enable disk emulation, which allows them to change code.
# Mk3 and earlier only.
#
cur = pyb.usb_mode()
if cur and 'MSC' in cur:
await ux_show_story("""The USB disk emulation is already enabled.""")
return
# serial port and disk (but no HID-based USB protocol)
pyb.usb_mode(None)
pyb.usb_mode('VCP+MSC')
await ux_show_story("""\
The disk emulation has now been enabled. Your code can go into /lib. \
Keep tmp files and other junk out!""")
async def dev_enable_protocol(*a):
# Turn off disk emulation. Keep VCP enabled, since they are still devs.
# Mk3 and earlier
cur = pyb.usb_mode()
if cur and 'HID' in cur:
await ux_show_story('Coldcard USB protocol is already enabled (HID mode)')
return
if settings.get('du', 0):
await ux_show_story('USB disabled in settings.')
return
# might need to reset stuff?
from usb import enable_usb
# reset and re-enable
pyb.usb_mode(None)
enable_usb()
# enable REPL
ckcc.vcp_enabled(True)
await ux_show_story('Back to normal USB mode.')
async def microsd_upgrade(menu, label, item):
# Upgrade vis MicroSD card
# - search for a particular file
# - verify it lightly
# - erase serial flash
# - copy it over (slow)
# - reboot into bootloader, which finishes install
from glob import dis, PSRAM
from files import dfu_parse
from utils import check_firmware_hdr
from sigheader import FW_HEADER_OFFSET, FW_HEADER_SIZE, FW_MAX_LENGTH_MK4
force_vdisk = item.arg
fn = await file_picker('Pick firmware image to use (.DFU)', suffix='.dfu',
min_size=0x7800, max_size=FW_MAX_LENGTH_MK4,
force_vdisk=force_vdisk)
if not fn: return
failed = None
with CardSlot(force_vdisk=force_vdisk) as card:
with card.open(fn, 'rb') as fp:
offset, size = dfu_parse(fp)
# we also put a copy of special signed heaer at the end of the flash
# read just the signature header
hdr = bytearray(FW_HEADER_SIZE)
fp.seek(offset + FW_HEADER_OFFSET)
rv = fp.readinto(hdr)
assert rv == FW_HEADER_SIZE
# check header values
failed = check_firmware_hdr(hdr, size)
if not failed:
# copy binary into PSRAM
fp.seek(offset)
dis.fullscreen("Loading...")
buf = bytearray(0x20000)
pos = 0
while pos < size:
dis.progress_bar_show(pos/size)
here = fp.readinto(buf)
if not here: break
PSRAM.write(pos, buf)
pos += here
if failed:
await ux_show_story(failed, title='Sorry!')
return
# continue process...
from auth import FirmwareUpgradeRequest
m = FirmwareUpgradeRequest(hdr, size, psram_offset=0)
the_ux.push(m)
async def start_dfu(*a):
from callgate import enter_dfu
enter_dfu(0)
# NOT REACHED
async def reset_self(*a):
import machine
machine.soft_reset()
# NOT REACHED
async def initial_pin_setup(*a):
# First time they select a PIN of any type.
from login import LoginUX
lll = LoginUX()
title = 'Choose PIN'
ch = await ux_show_story('''\
Pick the main wallet's PIN code now. Be more clever, but an example:
123-4567
It has two parts: prefix (123-) and suffix (-4567). \
Each part must be between 2 to 6 digits long. Total length \
can be as long as 12 digits.
The prefix part determines the anti-phishing words you will \
see each time you login.
Your new PIN protects access to \
this Coldcard device and is not a factor in the wallet's \
seed words or private keys.
THERE IS ABSOLUTELY NO WAY TO RECOVER A FORGOTTEN PIN! Write it down.
''', title=title)
if ch != 'y': return
while 1:
ch = await ux_show_story('''\
There is ABSOLUTELY NO WAY to 'reset the PIN' or 'factory reset' the Coldcard if you forget the PIN.
DO NOT FORGET THE PIN CODE.
Press 6 to prove you read to the end of this message.''', title='WARNING', escape='6')
if ch == 'x': return
if ch == '6': break
# do the actual picking
pin = await lll.get_new_pin(title)
del lll
if pin is None: return
# A new pin is to be set!
from glob import dis
dis.fullscreen("Saving...")
try:
dis.busy_bar(True)
assert pa.is_blank()
pa.change(new_pin=pin)
# check it? kinda, but also get object into normal "logged in" state
pa.setup(pin)
ok = pa.login()
assert ok
# must re-read settings after login, because they are encrypted
# with a key derived from the main secret.
settings.set_key()
settings.load()
except Exception as e:
print("Exception: %s" % e)
finally:
dis.busy_bar(False)
# Allow USB protocol, now that we are auth'ed
from usb import enable_usb
enable_usb()
from menu import MenuSystem
from flow import EmptyWallet
return MenuSystem(EmptyWallet)
async def login_countdown(sec):
# Show a countdown, which may need to
# run for multiple **days**
from glob import dis
from display import FontSmall, FontLarge
from utime import ticks_ms, ticks_diff
# pre-render fixed parts
dis.clear()
y = 0
dis.text(None, y, 'Login countdown in', font=FontSmall); y += 14
dis.text(None, y, 'effect. Must wait:', font=FontSmall); y += 14
y += 5
dis.save()
st = ticks_ms()
while sec > 0:
dis.restore()
dis.text(None, y, pretty_short_delay(sec), font=FontLarge)
dis.show()
dis.busy_bar(1)
# this should be more accurate, errors were accumulating
now = ticks_ms()
dt = 1000 - ticks_diff(now, st)
await sleep_ms(dt)
st = ticks_ms()
sec -= 1
dis.busy_bar(0)
async def block_until_login():
#
# Force user to enter a valid PIN.
# - or accept a bogus one and return T iff mk<4 and "countdown" pin used
#
from login import LoginUX
from ux import AbortInteraction
# do they want a randomized (shuffled) keypad?
rnd_keypad = settings.get('rngk', 0)
# single key that "kills" self if pressed on "words" screen
kill_btn = settings.get('kbtn', None)
rv = None # might already be logged-in if _skip_pin used
while not pa.is_successful():
lll = LoginUX(rnd_keypad, kill_btn)
try:
rv = await lll.try_login(bypass_pin=None)
if rv: break
except AbortInteraction:
# not allowed!
pass
return rv
async def show_nickname(nick):
# Show a nickname for this coldcard (as a personalization)
# - no keys here, just show it until they press anything
from glob import dis
from display import FontLarge, FontTiny, FontSmall
from ux import ux_wait_keyup
dis.clear()
if dis.width(nick, FontLarge) <= dis.WIDTH:
dis.text(None, 21, nick, font=FontLarge)
else:
dis.text(None, 27, nick, font=FontSmall)
dis.show()
await ux_wait_keyup()
async def pick_killkey(*a):
# Setting: kill seed sometimes (requires mk4)
if await ux_show_story('''\
If you press this key while the anti- phishing words are shown during login, \
your seed phrase will be immediately wiped.
Best if this does not match the first number of the second half of your PIN.''') != 'y':
return
from choosers import kill_key_chooser
start_chooser(kill_key_chooser)
async def pick_scramble(*a):
# Setting: scrambled keypad or normal
if await ux_show_story("When entering PIN, randomize the order of the key numbers, "
"so that cameras and shoulder-surfers are defeated.") != 'y':
return
from choosers import scramble_keypad_chooser
start_chooser(scramble_keypad_chooser)
async def pick_nickname(*a):
# from settings menu, enter a nickname
from nvstore import SettingsObject
# Value is not stored with normal settings, it's part of "prelogin" settings
# which are encrypted with zero-key.
s = SettingsObject.prelogin()
nick = s.get('nick', '')
if not nick:
ch = await ux_show_story('''\
You can give this Coldcard a nickname and it will be shown before login.''')
if ch != 'y': return
nn = await ux_input_text(nick, confirm_exit=False)
nn = nn.strip() if nn else None
s.set('nick', nn)
s.save()
del s
async def logout_now(*a):
# wipe memory and lock up
from utils import clean_shutdown
clean_shutdown()
async def login_now(*a):
# wipe memory and reboot
from utils import clean_shutdown
clean_shutdown(2)
async def virgin_help(*a):
await ux_show_story("""\
8 = Down (do it!)
5 = Up
OK = Checkmark
X = Cancel/Back
0 = Go to top
More on our website:
coldcardwallet
.com
""")
async def start_b39_pw(menu, label, item):
if not settings.get('b39skip', False):
ch = await ux_show_story('''\
You may add a passphrase to your BIP-39 seed words. \
This creates an entirely new wallet, for every possible passphrase.
By default, the Coldcard uses an empty string as the passphrase.
On the next menu, you can enter a passphrase by selecting \
individual letters, choosing from the word list (recommended), \
or by typing numbers.
Please write down the fingerprint of all your wallets, so you can \
confirm when you've got the right passphrase. (If you are writing down \
the passphrase as well, it's okay to put them together.) There is no way for \
the Coldcard to know if your password is correct, and if you have it wrong, \
you will be looking at an empty wallet.
Limitations: 100 characters max length, ASCII \
characters 32-126 (0x20-0x7e) only.
OK to start.
X to go back. Or press (2) to hide this message forever.
''', escape='2')
if ch == '2':
settings.set('b39skip', True)
if ch == 'x':
return
import seed
return seed.PassphraseMenu()
async def start_seed_import(menu, label, item):
import seed
return seed.WordNestMenu(item.arg)
def pick_new_seed(menu, label, item):
import seed
return seed.make_new_wallet(item.arg)
def new_from_dice(menu, label, item):
import seed
return seed.new_from_dice(item.arg)
async def convert_ephemeral_to_master(*a):
import seed
from pincodes import pa
from stash import bip39_passphrase
if not pa.tmp_value:
await ux_show_story('You do not have an active temporary seed (including BIP-39 passphrase)'
' right now, so this command does little except forget the seed words.'
' It does not enhance security in any way.')
return
words = settings.get("words", True)
_type = 'BIP-39 passphrase' if bip39_passphrase else 'temporary seed'
msg = 'Convert currently used %s to master seed. Old master seed' % _type
if words or bip39_passphrase:
msg += ' words themselves are erased forever, '
else:
msg += ' is erased forever, '
msg += ('and its settings blanked. This action is destructive '
'and may affect funds, if any, on old master seed. ')
if bip39_passphrase:
msg += ('BIP-39 passphrase '
'is captured during this process and will be in effect '
'going forward, but the passphrase itself is erased '
'and unrecoverable. ')
if not words:
msg += 'The resulting wallet cannot be used with any other passphrase. '
msg += 'A reboot is part of this process. '
msg += 'PIN code, and %s funds are not affected.' % _type
if not await ux_confirm(msg):
return await ux_aborted()
await seed.remember_ephemeral_seed()
settings.save()
await login_now()
async def clear_seed(*a):
# Erase the seed words, and private key from this wallet!
# This is super dangerous for the customer's money.
import seed
if pa.has_duress_pin():
await ux_show_story('Please empty the duress wallet, and clear '
'the duress PIN before clearing main seed.')
return
from trick_pins import tp
if any(tp.get_duress_pins()):
await ux_show_story('You have one or more duress wallets defined '
'under Trick PINs. Please empty them, and clear '
'associated Trick PINs before clearing main seed.')
return
if not await ux_confirm('Wipe seed words and reset wallet. '
'All funds will be lost. '
'You better have a backup of the seed words.'):
return await ux_aborted()
ch = await ux_show_story('''Are you REALLY sure though???\n\n\
This action will certainly cause you to lose all funds associated with this wallet, \
unless you have a backup of the seed words and know how to import them into a \
new wallet.\n\nPress (4) to prove you read to the end of this message and accept all \
consequences.''', escape='4')
if ch != '4':
return await ux_aborted()
seed.clear_seed()
# NOT REACHED -- reset happens
def render_master_secrets(mode, raw, node):
# Render list of words, or XPRV / master secret to text.
import stash, chains
c = chains.current_chain()
qr_alnum = False
if mode == 'words':
import bip39
words = bip39.b2a_words(raw).split(' ')
# This optimization make the QR very nice, and space for
# all the words too
qr = ' '.join(w[0:4] for w in words)
qr_alnum = True
msg = 'Seed words (%d):\n' % len(words)
msg += '\n'.join('%2d: %s' % (i+1, w) for i,w in enumerate(words))
if stash.bip39_passphrase:
msg += '\n\nBIP-39 Passphrase:\n *****'
if node:
msg += '\n\nSeed+Passphrase:\n%s' % c.serialize_private(node)
elif mode == 'xprv':
msg = c.serialize_private(node)
qr = msg
elif mode == 'master':
msg = '%d bytes:\n\n' % len(raw)
qr = str(b2a_hex(raw), 'ascii')
msg += qr
else:
raise ValueError(mode)
return msg, qr, qr_alnum
async def view_seed_words(*a):
import stash
if not await ux_confirm('The next screen will show the seed words'
' (and if defined, your BIP-39 passphrase).'
'\n\nAnyone with knowledge of those words '
'can control all funds in this wallet.'):
return
from glob import dis
dis.fullscreen("Wait...")
dis.busy_bar(True)
# preserve old UI where we show words + passphrase
# instead of just calculated seed + passphrase = extended privkey
# new: calculated xprv is now also shown for BIP39 passphrase wallet
raw = mode = None
if stash.bip39_passphrase:
# get main secret - bypass tmp
with stash.SensitiveValues(bypass_tmp=True) as sv:
if not sv.deltamode:
assert sv.mode == "words"
raw = sv.raw[:]
mode = sv.mode
stash.SensitiveValues.clear_cache()
with stash.SensitiveValues(bypass_tmp=False) as sv:
if sv.deltamode:
# give up and wipe self rather than show true seed values.
import callgate
callgate.fast_wipe()
dis.busy_bar(False)
msg, qr, qr_alnum = render_master_secrets(mode or sv.mode,
raw or sv.raw,
sv.node)
msg += '\n\nPress (1) to view as QR Code.'
while 1:
ch = await ux_show_story(msg, sensitive=True, escape='1')
if ch == '1':
from ux import show_qr_code
await show_qr_code(qr, qr_alnum)
continue
break
stash.blank_object(qr)
stash.blank_object(msg)
stash.blank_object(raw)
async def export_seedqr(*a):
# see standard: <https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md>
import bip39, stash
if not await ux_confirm('The next screen will show the seed words in a QR code.'
'\n\nAnyone with knowledge of those words '
'can control all funds in this wallet.'):
return
from glob import dis
dis.fullscreen("Wait...")
dis.busy_bar(True)
# Note: cannot reach this menu item if no words. If they are tmp, that's cool.
with stash.SensitiveValues(bypass_tmp=False) as sv:
if sv.deltamode:
# give up and wipe self rather than show true seed values.
import callgate
callgate.fast_wipe()
if sv.mode != 'words':
raise ValueError(sv.mode)
words = bip39.b2a_words(sv.raw).split(' ')
dis.busy_bar(False)
qr = ''.join('%04d'% bip39.get_word_index(w) for w in words)
del words
from ux import show_qr_code
await show_qr_code(qr, True)
stash.blank_object(qr)
async def damage_myself():
# called when it's time to disable ourselves due to various
# features related to duress and so on
# - mk2 cannot do this
# - mk4 doesn't call this, done by bootrom
mode = settings.get('cd_mode', 0)
#['Brick', 'Final PIN', 'Test Mode']
if mode == 2:
# test mode, do no damage
return
from glob import dis
dis.fullscreen("Wait...")
dis.busy_bar(True)
if mode == 1:
# leave single attempt; careful!
# - always consume one attempt, regardless
todo = max(1, pa.attempts_left - 1)
else:
# brick ourselves, by consuming all PIN attempts
todo = pa.attempts_left
# do a bunch of failed attempts
pa.setup('hfsp', False)
for i in range(todo):
try:
pa.login()
except:
# expecting EPIN_AUTH_FAIL
pass
# Try to keep UX responsive? But callgate stuff blocks everything,
# so just go as fast as possible.
dis.busy_bar(False)
async def version_migration():
# Handle changes between upgrades, and allow downgrades when possible.
# - long term we generally cannot delete code from here, because we
# never know when a user might skip a bunch of intermediate versions
# Data migration issue:
# - "login countdown" feature now stored elsewhere [mk3]
had_delay = settings.get('lgto', 0)
if had_delay:
from nvstore import SettingsObject
settings.remove_key('lgto')
s = SettingsObject.prelogin()
s.set('lgto', had_delay)
s.save()
del s
# Disable vdisk so it is off by default until re-enabled, after
# version 5.0.6 is installed
settings.remove_key('vdsk')
async def version_migration_prelogin():
# same, but for setting before login
# these have moved into SE2 for Mk4 and so can be removed
for n in [ 'cd_lgto', 'cd_mode', 'cd_pin' ]:
settings.remove_key(n)
async def start_login_sequence():
# Boot up login sequence here.
#
# - easy to brick units here, so catch and ignore errors where possible/appropriate
#
from ux import idle_logout
from glob import dis
import callgate, version
if pa.is_blank():
# Blank devices, with no PIN set all, can continue w/o login
goto_top_menu()
return
# data migration on settings that are used pre-login
try:
await version_migration_prelogin()
except: pass
# maybe show a nickname before we do anything
try:
nickname = settings.get('nick', None)
if nickname:
await show_nickname(nickname)
except: pass
# Allow impatient devs and crazy people to skip the PIN
guess = settings.get('_skip_pin', None)
if guess is not None:
try:
dis.fullscreen("(Skip PIN)")
pa.setup(guess)
pa.login()
except: pass
# If that didn't work, or no skip defined, force
# them to login successfully.
try:
# Get a PIN and try to use it to login
# - does warnings about attempt usage counts
wants_countdown = await block_until_login()
# Do we need to do countdown delay? (real or otherwise)
delay = 0
# Mk4 approach:
# - wiping has already occured if that was picked
# - delay is variable, stored in tc_arg
from trick_pins import tp
delay = tp.was_countdown_pin()
# Maybe they do know the right PIN, but do a delay anyway, because they wanted that
if not delay:
delay = settings.get('lgto', 0)
if delay:
# kill some time, with countdown, and get "the" PIN again for real login
pa.reset()
await login_countdown(delay * (60 if not version.is_devmode else 1))
# keep it simple for Mk4+: just challenge again for any PIN
# - if it's the same countdown pin, it will be accepted and they
# get in (as most trick pins would do)
await block_until_login()
except BaseException as exc:
# Robustness: any logic errors/bugs in above will brick the Coldcard
# even for legit owner, since they can't login. So try to recover, when it's
# safe to do so. Remember the bootrom checks PIN on every access to
# the secret, so "letting" them past this point is harmless if they don't know
# the true pin.
if not pa.is_successful():
raise
sys.print_exception(exc)
# Successful login...
# Must re-read settings after login
dis.fullscreen("Startup...")
settings.set_key()
settings.load(dis)
# handle upgrades/downgrade issues
try:
await version_migration()
except:
pass
# Maybe insist on the "right" microSD being already installed?
try:
from pwsave import MicroSD2FA
MicroSD2FA.enforce_policy()
except BaseException as exc:
# robustness: keep going!
sys.print_exception(exc)
# implement idle timeout now that we are logged-in
from imptask import IMPT
IMPT.start_task('idle', idle_logout())
# Populate xfp/xpub values, if missing.
# - can happen for first-time login of duress wallet
# - may indicate lost settings, which we can easily recover from
# - these values are important to USB protocol
if not (settings.get('xfp', 0) and settings.get('xpub', 0)) and not pa.is_secret_blank():
try:
import stash
# Recalculate xfp/xpub values (depends both on secret and chain)
with stash.SensitiveValues() as sv:
sv.capture_xpub()
except Exception as exc:
# just in case, keep going; we're not useless and this
# is early in boot process
print("XFP save failed: %s" % exc)
# Version warning before HSM is offered
if version.is_edge and not ckcc.is_simulator():
await ux_show_story(
"This preview version of firmware has not yet been qualified and "
"tested to the same standard as normal Coinkite products."
"\n\nIt is recommended only for developers and early adopters for experimental use. "
"DO NOT use for large Bitcoin amounts.", title="Edge Version")
# If HSM policy file is available, offer to start that,
# **before** the USB is even enabled.
# do not offer HSM if wallet is blank -> HSM needs secret
if not pa.is_secret_blank():
try:
import hsm, hsm_ux
if hsm.hsm_policy_available():
settings.put("hsmcmd", True)
ar = await hsm_ux.start_hsm_approval(usb_mode=False, startup_mode=True)
if ar:
await ar.interact()
except: pass
if version.has_nfc and settings.get('nfc', 0):
# Maybe allow NFC now
import nfc
nfc.NFCHandler.startup()
if settings.get('vidsk', 0):
# Maybe start virtual disk
import vdisk
vdisk.VirtDisk()
# Allow USB protocol, now that we are auth'ed
if not settings.get('du', 0):
from usb import enable_usb
enable_usb()
async def restore_main_secret(*a):
from glob import dis
from seed import restore_to_main_secret, in_seed_vault
escape = None
msg = "Restore main wallet and its settings?\n\n"
if not in_seed_vault(pa.tmp_value):
msg += (
"Press OK to forget current temporary seed "
"settings, or press (1) to save & keep "
"those settings if same seed is later restored."
)
escape = "1"
ch = await ux_show_story(msg, escape=escape)
if ch == "x": return
dis.fullscreen("Working...")
ps = True
if escape and (ch == "y"):
ps = False
await restore_to_main_secret(preserve_settings=ps)
goto_top_menu()