You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1506 lines
56 KiB
1506 lines
56 KiB
# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. <hello@foundationdevices.com>
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
#
|
|
# SPDX-FileCopyrightText: 2018 Coinkite, Inc. <coldcardwallet.com>
|
|
# SPDX-License-Identifier: GPL-3.0-only
|
|
#
|
|
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
|
|
# and is covered by GPLv3 license found in COPYING.
|
|
#
|
|
# actions.py
|
|
#
|
|
# Every function here is called directly by a menu item. They should all be async.
|
|
#
|
|
|
|
import pyb
|
|
import version
|
|
from files import CardMissingError, CardSlot
|
|
# import main
|
|
from uasyncio import sleep_ms
|
|
from common import settings, system, noise
|
|
from utils import imported, pretty_short_delay, xfp2str,swab32
|
|
from ux import (the_ux, ux_confirm, ux_dramatic_pause, ux_enter_pin,
|
|
ux_enter_number, ux_enter_text, ux_scan_qr_code, ux_shutdown,
|
|
ux_show_story, ux_show_story_sequence, ux_show_text_as_ur, ux_show_word_list)
|
|
from se_commands import *
|
|
import trezorcrypto
|
|
|
|
async def test_normal_menu():
|
|
goto_top_menu()
|
|
|
|
|
|
async def start_selftest(*args):
|
|
|
|
if len(args) and not version.is_factory_mode():
|
|
# called from inside menu, not directly
|
|
if not await ux_confirm('''Selftest destroys settings on other profiles (not seeds). Requires microSD card and might have other consequences. Recommended only for factory.'''):
|
|
return
|
|
|
|
with imported('selftest') as st:
|
|
await st.start_selftest()
|
|
|
|
settings.save()
|
|
|
|
|
|
async def needs_microsd():
|
|
# Standard msg shown if no SD card detected when we need one.
|
|
await ux_show_story("Please insert a microSD card before attempting this operation.")
|
|
|
|
|
|
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 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("""\
|
|
Welcome to Passport! Congratulations for taking the first step towards sovereignty and ownership of your Bitcoin.
|
|
|
|
Please accept our Terms of Use. You can read the full terms at:
|
|
|
|
foundationdevices.com/passport-terms""", left_btn='SHUTDOWN', right_btn='CONTINUE', scroll_label='MORE')
|
|
|
|
print('accept_terms() ch={}'.format(ch))
|
|
if ch == 'y':
|
|
accepted_terms = await ux_confirm('I confirm that I have read and accept the Terms of Use.', negative_btn='BACK', positive_btn='I ACCEPT')
|
|
if accepted_terms:
|
|
# Note fact they accepted the terms. Annoying to ask user more than once.
|
|
settings.set('terms_ok', 1)
|
|
settings.save()
|
|
break
|
|
|
|
elif ch == 'x':
|
|
# We only return from here if the user chose to not shutdown
|
|
await ux_shutdown()
|
|
|
|
|
|
async def view_ident(*a):
|
|
# show the XPUB, and other ident on screen
|
|
from common import settings
|
|
import stash
|
|
|
|
tpl = '''\
|
|
Master Key Fingerprint:
|
|
|
|
{xfp}
|
|
|
|
Fingerprint as LE32:
|
|
|
|
{xfp_le}
|
|
|
|
Extended Master Key:
|
|
|
|
{xpub}
|
|
'''
|
|
my_xfp = settings.get('xfp', 0)
|
|
my_xfp_le = swab32(my_xfp)
|
|
msg = tpl.format(xpub=settings.get('xpub', '(none yet)'),
|
|
xfp=xfp2str(my_xfp), xfp_le=xfp2str(my_xfp_le),
|
|
serial=version.serial_number())
|
|
|
|
if stash.bip39_passphrase:
|
|
msg += '\nBIP39 passphrase is in effect.\n'
|
|
|
|
await ux_show_story(msg, center=True)
|
|
|
|
|
|
async def maybe_dev_menu(*a):
|
|
from common 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 microsd_upgrade(*a):
|
|
# 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
|
|
|
|
fn = await file_picker('Pick firmware image to use (.BIN)')
|
|
|
|
if not fn:
|
|
return
|
|
|
|
failed = None
|
|
|
|
with CardSlot() as card:
|
|
with open(fn, 'rb') as fp:
|
|
from common import sf, dis
|
|
import os
|
|
|
|
offset = 0
|
|
s = os.stat(fn)
|
|
size = s[6]
|
|
|
|
# we also put a copy of special signed header at the end of the flash
|
|
# from sigheader import FW_HEADER_OFFSET, FW_HEADER_SIZE
|
|
|
|
# # 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
|
|
|
|
# copy binary into serial flash
|
|
fp.seek(offset)
|
|
|
|
buf = bytearray(256) # must be flash page size
|
|
pos = 0
|
|
update_display = 0
|
|
while pos <= size:
|
|
# print('pos = {}'.format(pos))
|
|
# Update progress bar every 50 flash pages
|
|
if update_display % 50 == 0:
|
|
dis.fullscreen("Preparing Update...", percent=pos/size)
|
|
update_display += 1
|
|
|
|
here = fp.readinto(buf)
|
|
if not here:
|
|
break
|
|
|
|
if pos % 4096 == 0:
|
|
# erase here
|
|
sf.sector_erase(pos)
|
|
while sf.is_busy():
|
|
await sleep_ms(10)
|
|
|
|
sf.write(pos, buf)
|
|
|
|
# full page write: 0.6 to 3ms
|
|
while sf.is_busy():
|
|
await sleep_ms(1)
|
|
|
|
pos += here
|
|
|
|
|
|
if failed:
|
|
await ux_show_story(failed, title='Sorry!')
|
|
return
|
|
|
|
# continue process...
|
|
print("RESTARTING!")
|
|
|
|
# Show final progress bar at 100% and change message
|
|
dis.fullscreen("Restarting...", percent=1)
|
|
await sleep_ms(1000)
|
|
|
|
import machine
|
|
machine.reset()
|
|
|
|
async def reset_self(*a):
|
|
import machine
|
|
machine.soft_reset()
|
|
# NOT REACHED
|
|
|
|
|
|
# TODO: Convert this to a state machine
|
|
async def initial_pin_setup(*a):
|
|
from common import pa, dis, settings, loop
|
|
|
|
# First time they select a PIN of any type.
|
|
title = 'Choose PIN'
|
|
|
|
while 1:
|
|
ch = await ux_show_story('''\
|
|
Passport uses two PINs. Each PIN must be 2 to 6 digits long.
|
|
|
|
The first is your Security Code. This PIN is used to verify your Passport has not been swapped or tampered with.
|
|
|
|
The second is your Login PIN. This PIN allows you to unlock and use Passport.
|
|
''', title=title, scroll_label='MORE')
|
|
if ch == 'y':
|
|
|
|
while 1:
|
|
ch = await ux_show_story('''\
|
|
There is no way to recover a lost PIN or factory reset your Passport.
|
|
|
|
Please write down your PINs somewhere safe or store them in a password manager.''', title='WARNING', scroll_label='MORE')
|
|
|
|
if ch != 'y':
|
|
break
|
|
|
|
# do the actual picking
|
|
from login_ux import EnterNewPinUX
|
|
new_pin_ux = EnterNewPinUX()
|
|
await new_pin_ux.show()
|
|
pin = new_pin_ux.pin
|
|
print('pin = {}'.format(pin))
|
|
|
|
if pin is None:
|
|
return
|
|
|
|
# New pin is being saved
|
|
dis.fullscreen("Saving...")
|
|
|
|
try:
|
|
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: {}".format(e))
|
|
|
|
from menu import MenuSystem
|
|
from flow import NoWalletMenu
|
|
return MenuSystem(NoWalletMenu)
|
|
|
|
|
|
async def login_countdown(minutes):
|
|
# show a countdown, which may need to
|
|
# run for multiple **days**
|
|
from common import dis
|
|
from display import FontSmall, FontLarge
|
|
|
|
sec = minutes * 60
|
|
while sec:
|
|
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.text(None, y, pretty_short_delay(sec), font=FontLarge)
|
|
|
|
dis.show()
|
|
dis.busy_bar(1)
|
|
await sleep_ms(1000)
|
|
|
|
sec -= 1
|
|
|
|
dis.busy_bar(0)
|
|
|
|
|
|
async def block_until_login(*a):
|
|
#
|
|
# Force user to enter a valid PIN.
|
|
#
|
|
from login_ux import LoginUX
|
|
from common import pa, loop, settings, dis
|
|
|
|
print('pa.is_successful() = {}'.format(pa.is_successful()))
|
|
while not pa.is_successful():
|
|
login_ux = LoginUX()
|
|
|
|
try:
|
|
await login_ux.show()
|
|
except Exception as e:
|
|
print('ERROR when logging in: {}'.format(e))
|
|
# not allowed!
|
|
pass
|
|
|
|
settings.set_key()
|
|
settings.load()
|
|
|
|
# Apply screen brightness
|
|
dis.set_brightness(settings.get('screen_brightness', 100))
|
|
|
|
print('!!!!LOGGED IN!!!!')
|
|
|
|
|
|
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 start_seed_import(menu, label, item):
|
|
import seed
|
|
return seed.WordNestMenu(item.arg)
|
|
|
|
|
|
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 BIP39 seed words. \
|
|
This creates an entirely new wallet, for every possible passphrase.
|
|
|
|
By default, Passport 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 \
|
|
Passport 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.
|
|
''')
|
|
if ch == '2':
|
|
settings.set('b39skip', True)
|
|
if ch == 'x':
|
|
return
|
|
|
|
import seed
|
|
return seed.PassphraseMenu()
|
|
|
|
|
|
async def create_new_wallet(*a):
|
|
from ubinascii import hexlify as b2a_hex
|
|
import seed
|
|
wallet_seed_bytes = seed.create_new_wallet_seed()
|
|
print('wallet_seed_bytes = {}'.format(b2a_hex(wallet_seed_bytes)))
|
|
|
|
mnemonic_str = trezorcrypto.bip39.from_data(wallet_seed_bytes)
|
|
print('mnemonic = {}'.format(mnemonic_str))
|
|
mnemonic_words = mnemonic_str.split(' ')
|
|
|
|
# Show new wallet seed words to user
|
|
msg = 'Seed words (%d):\n' % len(mnemonic_words)
|
|
msg += '\n'.join('%2d: %s' % (i+1, w) for i, w in enumerate(mnemonic_words))
|
|
|
|
trezor_seed = trezorcrypto.bip39.seed(mnemonic_str, '')
|
|
print('trezor_seed = {}'.format(b2a_hex(trezor_seed)))
|
|
|
|
result = await ux_show_story(msg, sensitive=True, title="New Seed", right_btn='DONE')
|
|
if result == 'y':
|
|
# TODO: Quiz user on all words in random order to ensure they remember them all
|
|
|
|
# Set the seed into the SE
|
|
seed.save_wallet_seed(wallet_seed_bytes)
|
|
|
|
goto_top_menu()
|
|
|
|
|
|
async def import_wallet(menu, label, item):
|
|
from foundation import bip39
|
|
from ubinascii import hexlify as b2a_hex
|
|
import seed
|
|
|
|
entropy = bytearray(33) # Includes and extra byte for the checksum bits
|
|
|
|
result = ux_show_story('''On the next screen you'll be able to restore your seed using predictive text input. If you'd like to enter "car" for example, please type 2-2-7 and select "car" from the dropdown.''')
|
|
if result == 'x':
|
|
return
|
|
|
|
fake_it = False
|
|
if fake_it:
|
|
# mnemonic = 'circle ecology lazy world fuel plate column priority crouch midnight scorpion cute defense enforce mention display dove review churn term canvas donate square broken'
|
|
mnemonic = 'park minute parrot ketchup river vital gravity wagon peanut inform craft amount erosion regular rent attack rubber then auto visa upon either fresh other'
|
|
# mnemonic = 'fabric humor guess asset day palace wealth spare trend seek focus empower hair advance myself defy grain inhale market noodle right need joke scatter'
|
|
else:
|
|
from seed_phrase_ux import SeedEntryUX
|
|
seed_phrase_entry = SeedEntryUX(seed_len=item.arg)
|
|
await seed_phrase_entry.show()
|
|
if not seed_phrase_entry.is_seed_valid:
|
|
return
|
|
|
|
# Seed is valid, so go ahead and convert the mnemonic to seed bits and save it
|
|
mnemonic = ' '.join(seed_phrase_entry.words)
|
|
|
|
print('mnemonic = {}'.format(mnemonic))
|
|
bip = bip39() # TODO: Can't we have static methods?
|
|
bip.mnemonic_to_entropy(mnemonic, entropy)
|
|
entropy = entropy[:32] # Trim off the checksum byte
|
|
print('entropy = {}'.format(b2a_hex(entropy)))
|
|
|
|
seed.save_wallet_seed(entropy)
|
|
print('Wallet was imported successfully!')
|
|
|
|
# TODO: Show post-creation story
|
|
|
|
goto_top_menu()
|
|
|
|
|
|
async def convert_bip39_to_bip32(*a):
|
|
import seed
|
|
import stash
|
|
|
|
if not await ux_confirm('''This operation computes the extended master private key using your BIP39 seed words and passphrase, and then saves the resulting value (xprv) as the wallet secret.
|
|
|
|
The seed words themselves are erased forever, but effectively there is no other change. If a BIP39 passphrase is currently in effect, its value is captured during this process and will be 'in effect' going forward, but the passphrase itself is erased and unrecoverable. The resulting wallet cannot be used with any other passphrase.
|
|
|
|
A reboot is part of this process. PIN code, and funds are not affected.
|
|
''', negative_btn='BACK', positive_btn='LOCK DOWN'):
|
|
return
|
|
|
|
print('bip={}'.format(stash.bip39_passphrase))
|
|
if not stash.bip39_passphrase:
|
|
if not await ux_confirm('''You do not have a BIP39 passphrase set right now, so this command does little except forget the seed words. It does not enhance security.'''):
|
|
return
|
|
|
|
await seed.remember_bip39_passphrase()
|
|
|
|
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
|
|
from common import pa
|
|
|
|
if not await ux_confirm('''Are you sure you want to erase the current wallet? All funds will be lost if not backed up.'''):
|
|
return
|
|
|
|
confirmed = await ux_confirm('''Without a proper backup, this action will cause you to lose all funds associated with this wallet.\n
|
|
Are you sure you read this message and understand the risks?''')
|
|
if not confirmed:
|
|
return
|
|
|
|
seed.clear_seed()
|
|
# NOT REACHED -- reset happens
|
|
|
|
async def clear_seed_no_reset(*a):
|
|
# Erase the seed words, and private key from this wallet!
|
|
# This is super dangerous for the customer's money.
|
|
import seed
|
|
from common import pa
|
|
|
|
if not await ux_confirm('''Are you sure you want to erase the current wallet? All funds will be lost if not backed up.'''):
|
|
return
|
|
|
|
confirmed = await ux_confirm('''Without a proper backup, this action will cause you to lose all funds associated with this wallet.\n
|
|
Are you sure you read this message and understand the risks?''')
|
|
if not confirmed:
|
|
return
|
|
|
|
seed.clear_seed(False)
|
|
# NOT REACHED -- reset happens
|
|
|
|
|
|
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 BIP39 passphrase).\n\n' +
|
|
'Anyone who knows these words can control all funds in this wallet.\n\n' +
|
|
'Do you want to display this sensitive information?'):
|
|
return
|
|
|
|
try:
|
|
with stash.SensitiveValues() as sv:
|
|
assert sv.mode == 'words' # protected by menu item predicate
|
|
|
|
words = trezorcrypto.bip39.from_data(sv.raw).split(' ')
|
|
|
|
msg = 'Seed words (%d):\n' % len(words)
|
|
msg += '\n'.join('%2d: %s' % (i+1, w) for i, w in enumerate(words))
|
|
|
|
pw = stash.bip39_passphrase
|
|
if pw:
|
|
msg += '\n\nBIP39 Passphrase:\n%s' % stash.bip39_passphrase
|
|
|
|
await ux_show_story(msg, sensitive=True, right_btn='DONE')
|
|
|
|
stash.blank_object(msg)
|
|
except:
|
|
# Unable to read seed!
|
|
await ux_show_story('Unable to retrieve seed.')
|
|
|
|
async def start_login_sequence():
|
|
# Boot up login sequence here.
|
|
#
|
|
from common import pa, settings, dis, loop
|
|
|
|
|
|
# if pa.is_blank():
|
|
# # Blank devices, with no PIN set all, can continue w/o login
|
|
|
|
# # Do green-light set immediately after firmware upgrade
|
|
# if version.is_fresh_version():
|
|
# pa.greenlight_firmware()
|
|
# dis.show()
|
|
|
|
# goto_top_menu()
|
|
# return
|
|
|
|
# # 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.
|
|
print('start_login_sequence 1')
|
|
while not pa.is_successful():
|
|
print('start_login_sequence 2')
|
|
# always get a PIN and login first
|
|
await block_until_login()
|
|
|
|
# print('start_login_sequence 3')
|
|
|
|
# # Must re-read settings after login
|
|
# settings.set_key()
|
|
# print('start_login_sequence 4')
|
|
# settings.load()
|
|
# print('start_login_sequence 5')
|
|
|
|
# # implement "login countdown" feature
|
|
# delay = settings.get('lgto', 0)
|
|
# if delay:
|
|
# pa.reset()
|
|
# await login_countdown(delay)
|
|
# await block_until_login()
|
|
|
|
# # Do green-light set immediately after firmware upgrade
|
|
# if version.is_fresh_version():
|
|
# pa.greenlight_firmware()
|
|
# dis.show()
|
|
|
|
# # Populate xfp/xpub values, if missing.
|
|
# # - can happen for first-time login of d-u-r-e-s-s 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)
|
|
|
|
# # Allow USB protocol, now that we are auth'ed
|
|
# # from usb import enable_usb
|
|
# # enable_usb(loop, False)
|
|
|
|
|
|
|
|
def goto_top_menu():
|
|
# Start/restart menu system
|
|
from menu import MenuSystem
|
|
from flow import NoPINMenu, MainMenu, NoWalletMenu
|
|
from common import pa
|
|
|
|
|
|
# if version.is_factory_mode():
|
|
# m = MenuSystem(???, title='Factory')
|
|
# elif pa.is_blank():
|
|
# # let them play a little before picking a PIN first time
|
|
# m = MenuSystem(
|
|
# NoPINMenu, should_cont=lambda: pa.is_blank(), title='Setup')
|
|
# else:
|
|
# assert pa.is_successful(), "nonblank but wrong pin"
|
|
|
|
|
|
m = MenuSystem(NoWalletMenu if pa.is_secret_blank() else MainMenu)
|
|
|
|
the_ux.reset(m)
|
|
|
|
return m
|
|
|
|
|
|
SENSITIVE_NOT_SECRET = '''
|
|
|
|
The file created is sensitive--in terms of privacy--but should not \
|
|
compromise your funds directly.'''
|
|
|
|
PICK_ACCOUNT = '''\n\nPress 1 to enter a non-zero account number.'''
|
|
|
|
|
|
async def dump_summary(*A):
|
|
# save addresses, and some other public details into a file
|
|
if not await ux_confirm('''\
|
|
Saves a text file to microSD with a summary of the *public* details \
|
|
of your wallet. For example, this gives the XPUB (extended public key) \
|
|
that you will need to import other wallet software to track balance.''' + SENSITIVE_NOT_SECRET):
|
|
return
|
|
|
|
# pick a semi-random file name, save it.
|
|
with imported('backups') as bk:
|
|
await bk.make_summary_file()
|
|
|
|
|
|
def electrum_export_story(background=False):
|
|
# saves memory being in a function
|
|
return ('''\
|
|
This saves a skeleton Electrum wallet file onto the microSD card. \
|
|
You can then open that file in Electrum without ever connecting this Passport to a computer.\n
|
|
'''
|
|
+ (background or 'Choose an address type for the wallet on the next screen.'+PICK_ACCOUNT)
|
|
+ SENSITIVE_NOT_SECRET)
|
|
|
|
|
|
async def electrum_skeleton(*a):
|
|
# save xpub, and some other public details into a file: NOT MULTISIG
|
|
|
|
ch = await ux_show_story(electrum_export_story())
|
|
|
|
account_num = 0
|
|
if ch == '1':
|
|
account_num = await ux_enter_number('Account Number:', 9999)
|
|
elif ch != 'y':
|
|
return
|
|
|
|
# pick segwit or classic derivation+such
|
|
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
|
|
from menu import MenuSystem, MenuItem
|
|
|
|
# Ordering and terminology from similar screen in Electrum. I prefer
|
|
# 'classic' instead of 'legacy' personally.
|
|
rv = []
|
|
|
|
rv.append(MenuItem("Legacy (P2PKH)", f=electrum_skeleton_step2,
|
|
arg=(AF_CLASSIC, account_num)))
|
|
rv.append(MenuItem("P2SH-Segwit", f=electrum_skeleton_step2,
|
|
arg=(AF_P2WPKH_P2SH, account_num)))
|
|
rv.append(MenuItem("Native Segwit", f=electrum_skeleton_step2,
|
|
arg=(AF_P2WPKH, account_num)))
|
|
|
|
return MenuSystem(rv, title="Electrum")
|
|
|
|
|
|
async def xpub_qr(*a):
|
|
# Create and show a QR code that BlueWallet can import
|
|
|
|
# pick segwit or classic derivation+such
|
|
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
|
|
from menu import MenuSystem, MenuItem
|
|
|
|
# TODO: Insert a step to choose a different account_num
|
|
account_num = 0
|
|
|
|
# Ordering and terminology from similar screen in Electrum. I prefer
|
|
# 'classic' instead of 'legacy' personally.
|
|
rv = []
|
|
|
|
rv.append(MenuItem("Native Segwit (zpub)", f=xpub_qr_step2,
|
|
arg=(AF_P2WPKH, account_num)))
|
|
|
|
return MenuSystem(rv, title="BlueWallet")
|
|
|
|
|
|
async def xpub_qr_step2(_1, _2, item):
|
|
from ubinascii import hexlify
|
|
import ujson
|
|
|
|
addr_fmt, account_num = item.arg
|
|
|
|
with imported('backups') as bk:
|
|
wallet = bk.generate_electrum_wallet(addr_fmt, account_num)
|
|
print('wallet={}'.format(wallet))
|
|
# xpub = wallet['keystore']['xpub']
|
|
|
|
# msg = '''{"keystore": {"ckcc_xpub": "xpub661MyMwAqRbcEd36dwxWycMGRYR9kioqmtd5XScTXxXWcDBNWf9svbcTSJw1nFLQRUFnbvFuEiB4QqygXakhZ3Jx3hh1pnV5uWCCwAk3kAK", "xpub": "zpub6qUao2NtyxySY7tDdSS13Chc3TqMrTF7jxCGExBUD9daszd5ibBME2t359in7m8TiToTSeHGTgCaVMNwKqrRdydyp68jyQ2owZy2UvVCh76", "label": "Coldcard Import 6FCC570C", "ckcc_xfp": 207080559, "type": "hardware", "hw_type": "coldcard", "derivation": "m/84'/0'/0'"}, "wallet_type": "standard", "use_encryption": false, "seed_version": 17}'''
|
|
|
|
encoded_msg = ujson.dumps(wallet).encode('ascii')
|
|
print('encoded_msg={}'.format(encoded_msg))
|
|
hex_msg = hexlify(encoded_msg)
|
|
str_msg = hex_msg.decode('ascii')
|
|
|
|
await ux_show_text_as_ur(title='BlueWallet', qr_text=str_msg)
|
|
|
|
|
|
async def bitcoin_core_skeleton(*A):
|
|
# save output descriptors into a file
|
|
# - user has no choice, it's going to be bech32 with m/84'/{coin_type}'/0' path
|
|
|
|
ch = await ux_show_story('''\
|
|
This saves a command onto the microSD card that includes the public keys. \
|
|
You can then run that command in Bitcoin Core without ever connecting this Passport to a computer.\
|
|
''' + PICK_ACCOUNT + SENSITIVE_NOT_SECRET)
|
|
|
|
account_num = 0
|
|
if ch == '1':
|
|
account_num = await ux_enter_number('Account Number:', 9999)
|
|
elif ch != 'y':
|
|
return
|
|
|
|
# no choices to be made, just do it.
|
|
with imported('backups') as bk:
|
|
await bk.make_bitcoin_core_wallet(account_num)
|
|
|
|
|
|
async def electrum_skeleton_step2(_1, _2, item):
|
|
# pick a semi-random file name, render and save it.
|
|
with imported('backups') as bk:
|
|
addr_fmt, account_num = item.arg
|
|
await bk.make_json_wallet('Electrum wallet', lambda: bk.generate_electrum_wallet(addr_fmt, account_num))
|
|
|
|
|
|
async def wasabi_skeleton(*A):
|
|
# save xpub, and some other public details into a file
|
|
# - user has no choice, it's going to be bech32 with m/84'/0'/0' path
|
|
|
|
if await ux_show_story('''\
|
|
This saves a skeleton Wasabi wallet file onto the microSD card. \
|
|
You can then open that file in Wasabi without ever connecting this Passport to a computer.\
|
|
''' + SENSITIVE_NOT_SECRET) != 'y':
|
|
return
|
|
|
|
# no choices to be made, just do it.
|
|
with imported('backups') as bk:
|
|
await bk.make_json_wallet('Wasabi wallet', lambda: bk.generate_wasabi_wallet(), 'new-wasabi.json')
|
|
|
|
|
|
async def backup_everything(*A):
|
|
# save everything, using a password, into single encrypted file, typically on SD
|
|
with imported('backups') as bk:
|
|
await bk.make_complete_backup()
|
|
|
|
|
|
async def verify_backup(*A):
|
|
# check most recent backup is "good"
|
|
# read 7z header, and measure checksums
|
|
|
|
with imported('backups') as bk:
|
|
|
|
fn = await file_picker('Select file containing the backup to be verified. No password will be required.', suffix='.7z', max_size=bk.MAX_BACKUP_FILE_SIZE)
|
|
|
|
if fn:
|
|
# do a limited CRC-check over encrypted file
|
|
await bk.verify_backup_file(fn)
|
|
|
|
|
|
def import_from_dice(*a):
|
|
import seed
|
|
return seed.import_from_dice()
|
|
|
|
|
|
async def import_xprv(*A):
|
|
# read an XPRV from a text file and use it.
|
|
import chains
|
|
import ure
|
|
from common import pa
|
|
from stash import SecretStash
|
|
from ubinascii import hexlify as b2a_hex
|
|
from backups import restore_from_dict
|
|
|
|
assert pa.is_secret_blank() # "must not have secret"
|
|
|
|
def contains_xprv(fname):
|
|
# just check if likely to be valid; not full check
|
|
try:
|
|
with open(fname, 'rt') as fd:
|
|
for ln in fd:
|
|
# match tprv and xprv, plus y/zprv etc
|
|
if 'prv' in ln:
|
|
return True
|
|
return False
|
|
except OSError:
|
|
# directories?
|
|
return False
|
|
|
|
# pick a likely-looking file.
|
|
fn = await file_picker('Select file containing the XPRV to be imported.',
|
|
min_size=50, max_size=2000, taster=contains_xprv)
|
|
|
|
if not fn:
|
|
return
|
|
|
|
node, chain, addr_fmt = None, None, None
|
|
|
|
# open file and do it
|
|
pat = ure.compile(r'.prv[A-Za-z0-9]+')
|
|
with CardSlot() as card:
|
|
with open(fn, 'rt') as fd:
|
|
for ln in fd.readlines():
|
|
if 'prv' not in ln:
|
|
continue
|
|
|
|
found = pat.search(ln)
|
|
if not found:
|
|
continue
|
|
|
|
found = found.group(0)
|
|
|
|
for ch in chains.AllChains:
|
|
for kk in ch.slip132:
|
|
if found[0] == ch.slip132[kk].hint:
|
|
try:
|
|
node = trezorcrypto.bip32.deserialize(found,
|
|
ch.slip132[kk].pub, ch.slip132[kk].priv)
|
|
chain = ch
|
|
addr_fmt = kk
|
|
break
|
|
except ValueError:
|
|
pass
|
|
if node:
|
|
break
|
|
|
|
if not node:
|
|
# unable
|
|
await ux_show_story('''\
|
|
Sorry, wasn't able to find an extended private key to import. It should be at \
|
|
the start of a line, and probably starts with "xprv".''', title="FAILED")
|
|
return
|
|
|
|
# encode it in our style
|
|
d = dict(chain=chain.ctype, raw_secret=b2a_hex(
|
|
SecretStash.encode(xprv=node)))
|
|
|
|
# This function was added by coinkite
|
|
# TODO: Important enough to add blank() back into trezor?
|
|
# node.blank()
|
|
|
|
# TODO: capture the address format implied by SLIP32 version bytes
|
|
# addr_fmt =
|
|
|
|
# restore as if it was a backup (code reuse)
|
|
await restore_from_dict(d)
|
|
|
|
# not reached; will do reset.
|
|
|
|
EMPTY_RESTORE_MSG = '''\
|
|
Before restoring from a backup, you must erase the current wallet. \
|
|
Please make sure your current wallet is backed up.\n\n\
|
|
Visit the advanced settings and choose 'Erase Wallet'.'''
|
|
|
|
|
|
async def restore_everything(*A):
|
|
from common import pa
|
|
|
|
if not pa.is_secret_blank():
|
|
await ux_show_story(EMPTY_RESTORE_MSG)
|
|
return
|
|
|
|
# restore everything, using a password, from single encrypted 7z file
|
|
fn = await file_picker('Select file containing the backup to be restored, and '
|
|
'then enter the password.', suffix='.7z', max_size=10000)
|
|
|
|
if fn:
|
|
with imported('backups') as bk:
|
|
await bk.restore_complete(fn)
|
|
|
|
|
|
async def restore_everything_cleartext(*A):
|
|
# Asssume no password on backup file; devs and crazy people only
|
|
from common import pa
|
|
|
|
if not pa.is_secret_blank():
|
|
await ux_show_story(EMPTY_RESTORE_MSG)
|
|
return
|
|
|
|
# restore everything, using NO password, from single text file, like would be wrapped in 7z
|
|
fn = await file_picker('Select the cleartext file containing the backup to be restored.',
|
|
suffix='.txt', max_size=10000)
|
|
|
|
if fn:
|
|
with imported('backups') as bk:
|
|
prob = await bk.restore_complete_doit(fn, [])
|
|
if prob:
|
|
await ux_show_story(prob, title='FAILED')
|
|
|
|
|
|
# async def wipe_filesystem(*A):
|
|
# if not await ux_confirm('''\
|
|
# Erase internal filesystem and rebuild it. Resets contents of internal flash area \
|
|
# used for code patches. Does not affect funds, settings or seed words. \
|
|
# Does not affect SD card, if any.'''):
|
|
# return
|
|
|
|
# from files import wipe_flash_filesystem
|
|
|
|
# wipe_flash_filesystem()
|
|
|
|
|
|
async def wipe_sd_card(*A):
|
|
if not await ux_confirm('''\
|
|
Erases and reformats microSD card. This is not a secure erase but more of a quick format.'''):
|
|
return
|
|
|
|
from files import wipe_microsd_card
|
|
wipe_microsd_card()
|
|
|
|
|
|
async def list_files(*A):
|
|
# list files, don't do anything with them?
|
|
fn = await file_picker('List files on microSD')
|
|
return
|
|
|
|
|
|
async def file_picker(msg, suffix=None, min_size=None, max_size=None, taster=None, choices=None, none_msg=None):
|
|
# present a menu w/ a list of files... to be read
|
|
# - optionally, enforce a max size, and provide a "tasting" function
|
|
# - if msg==None, don't prompt, just do the search and return list
|
|
# - if choices is provided; skip search process
|
|
# - escape: allow these chars to skip picking process
|
|
from menu import MenuSystem, MenuItem
|
|
import uos
|
|
from utils import get_filesize
|
|
|
|
if choices is None:
|
|
choices = []
|
|
try:
|
|
with CardSlot() as card:
|
|
sofar = set()
|
|
|
|
for path in card.get_paths():
|
|
files = uos.ilistdir(path)
|
|
for fn, ftype, *var in files:
|
|
print("fn={} ftype={} var={}".format(fn, ftype, var))
|
|
if ftype == 0x4000:
|
|
# ignore subdirs
|
|
continue
|
|
|
|
if suffix and not fn.lower().endswith(suffix):
|
|
# wrong suffix
|
|
continue
|
|
|
|
if fn[0] == '.':
|
|
continue
|
|
|
|
full_fname = path + '/' + fn
|
|
|
|
# Conside file size
|
|
# sigh, OS/filesystem variations
|
|
file_size = var[1] if len(
|
|
var) == 2 else get_filesize(full_fname)
|
|
|
|
if min_size is not None and file_size < min_size:
|
|
continue
|
|
|
|
if max_size is not None and file_size > max_size:
|
|
continue
|
|
|
|
if taster is not None:
|
|
try:
|
|
yummy = taster(full_fname)
|
|
except IOError:
|
|
# print("fail: %s" % full_fname)
|
|
yummy = False
|
|
|
|
if not yummy:
|
|
continue
|
|
|
|
label = fn
|
|
while label in sofar:
|
|
# just the file name isn't unique enough sometimes?
|
|
# - shouldn't happen anymore now that we dno't support internal FS
|
|
# - unless we do muliple paths
|
|
label += path.split('/')[-1] + '/' + fn
|
|
|
|
sofar.add(label)
|
|
choices.append((label, path, fn))
|
|
|
|
except CardMissingError:
|
|
# don't show anything if we're just gathering data
|
|
if msg is not None:
|
|
await needs_microsd()
|
|
return None
|
|
|
|
if msg is None:
|
|
return choices
|
|
|
|
if not choices:
|
|
msg = none_msg or 'Passport is unable to find the correct file on your microSD card. '
|
|
|
|
if not none_msg:
|
|
if suffix:
|
|
msg += 'The filename must end in "%s". ' % suffix
|
|
|
|
msg += '\n\nPlease check the files on your microSD card and try again. '
|
|
|
|
await ux_show_story(msg)
|
|
return
|
|
|
|
# tell them they need to pick; can quit here too, but that's obvious.
|
|
if len(choices) != 1:
|
|
msg += '\n\nThere are %d files to pick from.' % len(choices)
|
|
else:
|
|
msg += '\n\nThere is only one file to pick from.'
|
|
|
|
ch = await ux_show_story(msg)
|
|
if ch == 'x':
|
|
return
|
|
|
|
picked = []
|
|
|
|
async def clicked(_1, _2, item):
|
|
picked.append('/'.join(item.arg))
|
|
the_ux.pop()
|
|
|
|
items = [MenuItem(label, f=clicked, arg=(path, fn))
|
|
for label, path, fn in choices]
|
|
|
|
if 0:
|
|
# don't like; and now showing count on previous page
|
|
if len(choices) == 1:
|
|
# if only one choice, we could make the choice for them ... except very confusing
|
|
items.append(MenuItem(' (one file)', f=None))
|
|
else:
|
|
items.append(MenuItem(' (%d files)' % len(choices), f=None))
|
|
|
|
menu = MenuSystem(items, title='Select a File')
|
|
the_ux.push(menu)
|
|
|
|
await menu.interact()
|
|
|
|
return picked[0] if picked else None
|
|
|
|
|
|
async def sign_tx_from_sd(*a):
|
|
# Top menu choice of top menu! Signing!
|
|
# - check if any signable in SD card, if so do it
|
|
# - if nothing, then talk about USB connection
|
|
from public_constants import MAX_TXN_LEN
|
|
|
|
def is_psbt(filename):
|
|
print("filename=" + filename)
|
|
if '-signed' in filename.lower():
|
|
return False
|
|
|
|
with open(filename, 'rb') as fd:
|
|
taste = fd.read(10)
|
|
if taste[0:5] == b'psbt\xff':
|
|
return True
|
|
if taste[0:10] == b'70736274ff': # hex-encoded
|
|
return True
|
|
if taste[0:6] == b'cHNidP': # base64-encoded
|
|
return True
|
|
return False
|
|
|
|
choices = await file_picker(None, suffix='psbt', min_size=50,
|
|
max_size=MAX_TXN_LEN, taster=is_psbt)
|
|
|
|
if not choices:
|
|
await ux_show_story("""\
|
|
Please copy an unsigned PSBT transaction onto your microSD card and insert into Passport.
|
|
""")
|
|
return
|
|
|
|
if len(choices) == 1:
|
|
# skip the menu
|
|
label, path, fn = choices[0]
|
|
input_psbt = path + '/' + fn
|
|
else:
|
|
input_psbt = await file_picker('Choose PSBT file to be signed.', choices=choices)
|
|
if not input_psbt:
|
|
return
|
|
|
|
# start the process
|
|
from auth import sign_psbt_file
|
|
|
|
await sign_psbt_file(input_psbt)
|
|
|
|
|
|
async def sign_message_on_sd(*a):
|
|
# Menu item: choose a file to be signed (as a short text message)
|
|
#
|
|
def is_signable(filename):
|
|
if '-signed' in filename.lower():
|
|
return False
|
|
with open(filename, 'rt') as fd:
|
|
lines = fd.readlines()
|
|
return (1 <= len(lines) <= 5)
|
|
|
|
fn = await file_picker('Choose text file to be signed.',
|
|
suffix='txt', min_size=2,
|
|
max_size=500, taster=is_signable,
|
|
none_msg='No suitable files found. Must be one line of text, in a .TXT file, optionally followed by a subkey derivation path on a second line.')
|
|
|
|
if not fn:
|
|
return
|
|
|
|
# start the process
|
|
from auth import sign_txt_file
|
|
await sign_txt_file(fn)
|
|
|
|
|
|
async def change_pin(*a):
|
|
# Help user change pins with appropriate warnings.
|
|
from login_ux import ChangePINUX
|
|
|
|
change_pin_ux = ChangePINUX()
|
|
await change_pin_ux.show()
|
|
|
|
|
|
# Reset pin to blank (all zeroes)
|
|
async def set_blank_pin(*a):
|
|
from common import pa
|
|
|
|
args = {}
|
|
old_pin_1 = await ux_enter_pin(title='Enter Old PIN', heading='Security Code')
|
|
old_pin_2 = await ux_enter_pin(title='Enter Old PIN', heading='Login PIN')
|
|
old_pin = old_pin_1 + old_pin_2
|
|
args['old_pin'] = old_pin.encode()
|
|
blank_pin = [32] * 0
|
|
args['new_pin'] = bytearray(blank_pin)
|
|
try:
|
|
pa.change(**args)
|
|
except Exception as e:
|
|
print('Exception: {}'.format(e))
|
|
|
|
|
|
async def show_version(*a):
|
|
# show firmware, bootload versions.
|
|
from common import settings
|
|
import callgate
|
|
import version
|
|
from ubinascii import hexlify as b2a_hex
|
|
|
|
built, rel, *_ = version.get_mpy_version()
|
|
bl = callgate.get_bootloader_version()[0]
|
|
chk = str(b2a_hex(callgate.get_firmware_hash(0))[-8:], 'ascii')
|
|
|
|
msg = '''\
|
|
Passport Firmware
|
|
{rel}
|
|
{built}
|
|
|
|
Bootloader:
|
|
{bl}
|
|
{chk}
|
|
|
|
Serial:
|
|
{ser}
|
|
|
|
Hardware:
|
|
{hw}
|
|
'''
|
|
|
|
await ux_show_story(msg.format(rel=rel, built=built, bl=bl, chk=chk,
|
|
ser=version.serial_number(), hw=version.hw_label))
|
|
|
|
|
|
async def set_firmware_highwater(*a):
|
|
# rarely? used command
|
|
import callgate
|
|
|
|
have = version.get_mpy_version()[0]
|
|
ts = version.get_header_value('timestamp')
|
|
|
|
hw = callgate.get_firmware_highwater()
|
|
|
|
if hw == ts:
|
|
await ux_show_story('''Current version (%s) already marked as high-water mark.''' % have)
|
|
return
|
|
|
|
ok = await ux_confirm('''Mark current version (%s) as the minimum, and prevent any downgrades below this version.
|
|
|
|
Rarely needed as critical security updates will set this automatically.''' % have)
|
|
|
|
if not ok:
|
|
return
|
|
|
|
rv = callgate.set_firmware_highwater(ts)
|
|
|
|
# add error display here? meh.
|
|
|
|
assert rv == 0, "Failed: %r" % rv
|
|
|
|
|
|
async def import_multisig(*a):
|
|
# pick text file from SD card, import as multisig setup file
|
|
|
|
def possible(filename):
|
|
with open(filename, 'rt') as fd:
|
|
for ln in fd:
|
|
if 'pub' in ln:
|
|
return True
|
|
|
|
fn = await file_picker('Pick multisig wallet file to import (.txt)', suffix='.txt',
|
|
min_size=100, max_size=20*200, taster=possible)
|
|
|
|
if not fn:
|
|
return
|
|
|
|
try:
|
|
with CardSlot() as card:
|
|
with open(fn, 'rt') as fp:
|
|
data = fp.read()
|
|
except CardMissingError:
|
|
await needs_microsd()
|
|
return
|
|
|
|
from auth import maybe_enroll_xpub
|
|
try:
|
|
possible_name = (fn.split('/')[-1].split('.'))[0]
|
|
maybe_enroll_xpub(config=data, name=possible_name)
|
|
except Exception as e:
|
|
await ux_show_story('Failed to import.\n\n\n'+str(e))
|
|
|
|
async def sign_tx_from_qr(menu, label, item):
|
|
title = item.arg
|
|
data = await ux_scan_qr_code(title)
|
|
# data = '70736274ff010071020000000131d2b534ed8dccf2546323b58a9fc2b0a28475f522fab0b9eb820b3c7bfe43f20000000000ffffffff021027000000000000160014fb141c2c8020a9242da603b6b1a88b981c9bec33c421010000000000160014395b1557687176af8d96fdf26bd4ecf1b44fb406000000000001011fa086010000000000160014f338b1a82c03f057b6b5200d6642492c3c8742d4220602fce63395dc5ff807b501c16fc6d719277c96936b3727667b0f3d19dd23e3905418fa96b9d0540000800000008000000080000000000000000000002202034bf621fed4dfff08f7b8d8e9b5de5bc556ad3885a30807855617d5bcb3e22c2518fa96b9d0540000800000008000000080010000000000000000'
|
|
|
|
if data != None:
|
|
from auth import sign_psbt_buf
|
|
|
|
# print("data=", data)
|
|
# The data can be a string or may already be a bytes object
|
|
data_buf = data if isinstance(data, bytes) else bytes(data, 'utf8')
|
|
# print("data_buf={}".format(data_buf))
|
|
await sign_psbt_buf(data_buf)
|
|
|
|
async def enter_passphrase(menu, label, item):
|
|
title = item.arg
|
|
passphrase = await ux_enter_text(title, label="Enter a Passphrase")
|
|
|
|
print("Chosen passphrase = {}".format(passphrase))
|
|
|
|
async def enter_seed_phrase(menu, label, item):
|
|
from seed_phrase_ux import SeedEntryUX
|
|
seed_pharase_entry = SeedEntryUX(seed_len=item.arg)
|
|
await seed_pharase_entry.show()
|
|
print('seed words = {}'.format(seed_pharase_entry.words))
|
|
|
|
async def sample_stories(menu, label, item):
|
|
result = await ux_show_story_sequence(
|
|
[
|
|
{'msg': 'Testing 1\nTesting 1\nTesting 1\nTesting 1\nTesting 1\nTesting 1\nTesting 1\nTesting 1\nTesting 1\nTesting 1\n', 'title': 'Story 1'},
|
|
{'msg': 'Testing 2', 'title': 'Story 2'},
|
|
{'msg': 'Testing 3', 'title': 'Story 3', 'right_btn': 'ACTION'}
|
|
]
|
|
)
|
|
if result == 'y':
|
|
# Do some action at the end
|
|
pass
|
|
|
|
# TODO: Go back and reimplement this as a state machine like LoginUX
|
|
async def validate_passport_hw(*a):
|
|
from pincodes import PinAttempt
|
|
|
|
if settings.get('validated_ok'):
|
|
return
|
|
|
|
while True:
|
|
# Show a story with explanatory text regarding validation
|
|
result = await ux_show_story('''\
|
|
First, let's make sure your Passport has not been tampered with during shipping.
|
|
|
|
Navigate to:
|
|
|
|
foundationdevices.com/passport-validation
|
|
|
|
You will be presented with a validation page containing a QR code.
|
|
|
|
On the next screen, scan that QR code. Your Passport will show you 4 Security Words in response.
|
|
|
|
Enter those 4 words in the same order into the web page and click the Validate button.''', title='Validation', left_btn='SHUTDOWN', scroll_label='MORE')
|
|
|
|
if result == 'y':
|
|
|
|
while True:
|
|
# Scan a QR code
|
|
qr_data = await ux_scan_qr_code('Validation')
|
|
|
|
# Generate the 4 validation words
|
|
# 4 words gives 2048^4 possible combinations => 17,592,186,044,416 (~17.5 trillion)
|
|
if qr_data == None:
|
|
|
|
while True:
|
|
result = await ux_show_story('''No QR code was scanned. Do you want to try again, or skip validation?\n\nIMPORTANT: If you skip validation now, there will be no way to do so in the future.''', left_btn='SKIP', right_btn='RETRY')
|
|
if result == 'x':
|
|
# Confirm?
|
|
confirmed = await ux_confirm('Are you sure you want to permanently skip suoply chain validation?')
|
|
if confirmed:
|
|
# 2 means validation was skipped, but we won't show it again
|
|
settings.set('validated_ok', 2)
|
|
settings.save()
|
|
return
|
|
else:
|
|
# Break out of the inner loop and back out the the QR validation scan loop
|
|
break
|
|
|
|
else:
|
|
words = PinAttempt.supply_chain_validation_words(
|
|
qr_data.encode())
|
|
print(words)
|
|
numbered_words = []
|
|
for i in range(len(words)):
|
|
numbered_words.append('{}. {}'.format(i+1, words[i]))
|
|
|
|
result = await ux_show_word_list(
|
|
'Validate',
|
|
numbered_words,
|
|
heading1='Enter these words on',
|
|
heading2='the validation page.',
|
|
left_aligned_center=True,
|
|
left_btn='INVALID',
|
|
right_btn='VALID'
|
|
)
|
|
if result == 'x':
|
|
while True:
|
|
result = await ux_show_story('''\
|
|
If the words did not match, your Passport may have been modified after it was manufactured. Please contact us at support@foundationdevices.com.''', left_btn='SHUTDOWN', right_btn='RETRY')
|
|
if result == 'x':
|
|
await ux_shutdown()
|
|
else:
|
|
# Retry in the outer loop - scan the QR code again
|
|
break
|
|
elif result == 'y':
|
|
# Note that they confirmed the validation words were correct
|
|
settings.set('validated_ok', 1)
|
|
settings.save()
|
|
return
|
|
elif result == 'x':
|
|
await ux_shutdown()
|
|
|
|
|
|
async def test_ur(*a):
|
|
from test import TestUR
|
|
|
|
test = TestUR()
|
|
test.run_tests()
|
|
|
|
|
|
async def test_ur_encoder(_1, _2, item):
|
|
await ux_show_text_as_ur(title='Test UR Encoding', msg='Animated UR Code', qr_text=b'Y\x01\x00\x91n\xc6\\\xf7|\xad\xf5\\\xd7\xf9\xcd\xa1\xa1\x03\x00&\xdd\xd4.\x90[w\xad\xc3nO-<\xcb\xa4O\x7f\x04\xf2\xdeD\xf4-\x84\xc3t\xa0\xe1I\x13o%\xb0\x18RTYa\xd5_\x7fz\x8c\xdem\x0e.\xc4?;-\xcbdJ"\t\xe8\xc9\xe3J\xf5\xc4ty\x84\xa5\xe8s\xc9\xcf_\x96^%\xee)\x03\x9f\xdf\x8c\xa7O\x1cv\x9f\xc0~\xb7\xeb\xae\xc4n\x06\x95\xae\xa6\xcb\xd6\x0b>\xc4\xbb\xff\x1b\x9f\xfe\x8a\x9er@\x12\x93w\xb9\xd3q\x1e\xd3\x8dA/\xbbDB%o\x1eoY^\x0f\xc5\x7f\xedE\x1f\xb0\xa0\x10\x1f\xb7k\x1f\xb1\xe1\xb8\x8c\xfd\xfd\xaa\x94b\x94\xa4}\xe8\xff\xf1s\xf0!\xc0\xe6\xf6[\x05\xc0\xa4\x94\xe5\x07\x91\'\n\x00P\xa7:\xe6\x9bg%PZ.\xc8\xa5y\x14W\xc9\x87m\xd3J\xad\xd1\x92\xa5:\xa0\xdcf\xb5V\xc0\xc2\x15\xc7\xce\xb8$\x8bq|"\x95\x1ee0[V\xa3pn>\x86\xeb\x01\xc8\x03\xbb\xf9\x15\xd8\x0e\xdc\xd6MM')
|
|
|
|
|
|
async def play_snake(*a):
|
|
from snake import snake_game
|
|
await snake_game()
|
|
|
|
|
|
async def play_stacksats(*a):
|
|
from stacksats import stacksats_game
|
|
await stacksats_game()
|
|
|
|
|
|
# Secure Element Test Actions
|
|
async def se_get_version(*a):
|
|
version = bytearray(64)
|
|
system.dispatch(CMD_GET_BOOTLOADER_VERSION, version, 0)
|
|
print('version={}'.format(version))
|
|
ver_str = version[:16].decode('utf8')
|
|
print('ver_str = {}'.format(ver_str))
|
|
|
|
# Hex format for the rest
|
|
import binascii
|
|
s = binascii.hexlify(version[16:]).decode('utf8')
|
|
lines = [s[i:i+16] for i in range(0, len(s), 16)]
|
|
data = '\n'.join(lines)
|
|
await ux_show_story("SE Version\n\n{}\n\n{}".format(ver_str, data))
|
|
|
|
async def se_get_config(*a):
|
|
config = bytearray(128)
|
|
system.dispatch(CMD_GET_SE_CONFIG, config, 0)
|
|
|
|
import binascii
|
|
s = binascii.hexlify(config).decode('utf8')
|
|
lines = [s[i:i+16] for i in range(0, len(s), 16)]
|
|
data = '\n'.join(lines)
|
|
await ux_show_story("SE Config\n\n" + data)
|
|
|
|
async def gen_random(*a):
|
|
from binascii import hexlify
|
|
seed = bytearray(32)
|
|
valid = noise.random_bytes(seed)
|
|
print('Seed = {}'.format(hexlify(seed)))
|
|
|
|
async def show_power_monitor(*a):
|
|
from foundation import Powermon
|
|
powermon = Powermon()
|
|
|
|
for i in range(10):
|
|
(current, voltage) = powermon.read()
|
|
print('current={} voltage={}'.format(current, voltage))
|
|
await sleep_ms(1000)
|
|
|
|
|
|
async def show_board_rev(*a):
|
|
from foundation import Boardrev
|
|
boardrev = Boardrev()
|
|
|
|
rev = boardrev.read()
|
|
print('Board rev={}'.format(rev))
|
|
|
|
async def factory_setup(*a):
|
|
system.dispatch(CMD_FACTORY_SETUP, None, 0);
|
|
|
|
async def erase_rom_secrets(*a):
|
|
confirm = await ux_confirm('Are you sure you want to erase the ROM secrets?\n\nThis will UNPAIR your device from the current Secure Element chip!\n\nYou will need to insert a new chip to recover.')
|
|
if confirm:
|
|
system.erase_rom_secrets()
|
|
|
|
async def erase_user_settings(*a):
|
|
confirm = await ux_confirm('Are you sure you want to erase the User Settings?\n\nWhen restarting, you will be prompted to accept terms again and go through Supply Chain Authentication again.')
|
|
if confirm:
|
|
settings.erase_settings_flash()
|
|
|
|
async def update_xpub(*a):
|
|
import stash
|
|
with stash.SensitiveValues() as sv:
|
|
sv.capture_xpub()
|
|
|
|
async def coming_soon(*a):
|
|
await ux_show_story('This feature is under development. Stay tuned!', title='Coming Soon')
|
|
|
|
async def dump_settings(*a):
|
|
print('Current Settings:\n{}'.format(settings.curr_dict))
|
|
|
|
async def test_ur1(*a):
|
|
from ur1.decode_ur import decode_ur
|
|
from ur1.encode_ur import encode_ur
|
|
|
|
# Encoding
|
|
data = '7e4a61385e2550981b4b5633ab178eb077a30505fbd53f107ec1081e7cf0ca3c0dc0bfea5b8bfb5e6ffc91afd104c3aa756210b5dbc5118fd12c87ee04269815ba6a9968a0d0d3b7a9b631382a36bc70ab626d5670b4b48ff843f4d9a15631aa67c7aaf0ac6ce7e3bff03b2c9643e3375e47493c4e0f8635329d66fdec41b10ce74dcbf25fc15d829e7830c325643a98561f441b40a02e8353493e6afc16192fe99d90d8ca65539af77ddeaccc8943a37563a9ba83675bd5d4da7c60c9a172cf6940cbf0ec8fe04175a629932e3512c5d2aaea3cca3246f40a21ffdc33c3987dc7b880351230eb3759fe3c7dc7b2d3a20a95996ff0b7a0dba834f96beb64c14e3426fb051a936ba41569ab99c0066a6d9c0777a49e49e6cbad24d722a4c7da112432679264b9adc0a8cff9dd1fe0ee9ee2747f6a68537c389a7303a1af23c534ee6392bc17b04cf0fbce7689e66b673a440c04a9454005b0c76664639113458eb7d0902eff04d11138ce2a8ee16a9cd7c8926514efa9bd83ae7a4c139835f0fe0f68c628e0645c8524c30dfc314e825a7aa13224d98e2f7a9d12183a999bb1f28549c99a9072d99c05c24e0c84848c4fc147a094ab7b69e9cbea86952fccf15500fbb234ffe6ee6e6ded515c8016cb017ba36fb931ef276cec4ed22c1aed1495d2df3b3ce66c03f5b9ffa8434bf0e8fb149de94e050b3da178df1f76c00a366cb2801fabdf1a1e90cd3cd45ecb7a930a40b151455f76b726d552f31c21324992da257ff8bde2923dfd5d0d6b87233fae215ffacbecd96249099e7e3427d533db56cdb09c7475b4ce3314e33f43953a7370866cc11d85f00b71b15510b46c4b4fa490c660ddfeda0ceb1b8265995f7071c155ad1b57465fdc0fa81a73f9f19ac4872029d5844c1838f732e803043673e26cbc5b51297a324ff00a2d2d4222bad556b93d27c8e376e3ff8f9d37b3073410708ebb3d4dd7473d27212310b71a3c33a5c8f87f44824640e7f8970f4eda9364195c87a91172b8f085f1773641dde1ce21938746234055bc971ce2325f814e3eec60f781dd4faf52afd5be4a6b38656f7e9739f724cb7ccd4e4d01e802add3dc7b83191f894b3ee0ed752ee514d5ec55'
|
|
|
|
result = encode_ur(data)
|
|
if result == [
|
|
'ur:bytes/1of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/typjqlj2vyu9uf2snqd5k43n4vtcavrh5vzst7748ug8asggre70pj3uphqtl6jm30a4umlujxhazpxr4f6kyy94m0z3rr739jr7uppxnq2m565edzsdp5ah4xmrzwp2x678p2mzd4t8pd953luy8axe59trr2n8c740ptrvul3mlupm9jty8cehter5j0zwp7rr2v5a',
|
|
'ur:bytes/2of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/vm77csd3pnn5mjljtlq4mq570qcvxfty82v9v86yrdq2qt5r2dynu6huzcvjl6vajrvv5e2nntmhmh4vejy58gm4vw5m4qm8t02afknuvry6zuk0d9qvhu8v3lsyzadx9xfjudgjchf2463uegeydaq2y8lacv7rnp7u0wyqx5frp6eht8lrclw8ktf6yz54n9hlpdaq',
|
|
'ur:bytes/3of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/mw5rf7ttadjvzn35ymas2x5ndwjp26dtn8qqv6ndnsrh0fy7f8nvhtfy6u32f376zyjryeujvju6ms9gelua68lqa60wyarldf59xlpcnfes8gd0y0znfmnrj27p0vzv7rauua5fue4kwwjypsz2j32qqkcvwenyvwg3x3vwklgfqthlqng3zwxw928wz65u6lyfyeg5',
|
|
'ur:bytes/4of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/a75mmqaw0fxp8xp47rlq76xx9rsxghy9ynpsmlp3f6p9574pxgjdnr3002w3yxp6nxdmru59f8ye4yrjmxwqtsjwpjzgfrz0c9r6p99t0d57njl2s62jln8325q0hv35llnwumnda4g4eqqkevqhhgm0hyc77fmva38dytq6a52ft5kl8v7wvmqr7kull2zrf0cw37c5',
|
|
'ur:bytes/5of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/nh55upgt8ksh3hclwmqq5dnvk2qpl27lrg0fpnfu630vk75npfqtz529tamtwfk42te3cgfjfxfd5ftllz779y3al4ws66u8yvl6ug2llt97ektzfyyeul35yl2n8k6kekcfcar4kn8rx98r8ape2wnnwzrxesgashcqkud325gtgmztf7jfp3nqmhld5r8trwpxtx2l',
|
|
'ur:bytes/6of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/wpcuz4ddrdt5vh7up75p5ule7xdvfpeq982cgnqc8rmn96qrqsm88cnvh3d4z2t6xf8lqz3d94pz9wk426un6f7gudmw8lu0n5mmxpe5zpcgaweafht5w0f8yy33pdc68se6tj8c0azgy3jqulufwr6wm2fkgx2us753zu4c7zzlzaekg8w7rn3pjwr5vg6q2k7fw88z',
|
|
'ur:bytes/7of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/xf0czn37a3s00qwaf7h49t74he9xkwr9dalfww0hyn9hen2wf5q7sq4d60w8hqcer7y5k0hqa46jaeg56hk92xd0vz4',
|
|
]:
|
|
print('encode_ur() worked!')
|
|
else:
|
|
print('encode_ur() failed!')
|
|
|
|
# Decoding
|
|
workloads = [
|
|
'ur:bytes/7of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/xf0czn37a3s00qwaf7h49t74he9xkwr9dalfww0hyn9hen2wf5q7sq4d60w8hqcer7y5k0hqa46jaeg56hk92xd0vz4',
|
|
'ur:bytes/4of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/a75mmqaw0fxp8xp47rlq76xx9rsxghy9ynpsmlp3f6p9574pxgjdnr3002w3yxp6nxdmru59f8ye4yrjmxwqtsjwpjzgfrz0c9r6p99t0d57njl2s62jln8325q0hv35llnwumnda4g4eqqkevqhhgm0hyc77fmva38dytq6a52ft5kl8v7wvmqr7kull2zrf0cw37c5',
|
|
'ur:bytes/2of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/vm77csd3pnn5mjljtlq4mq570qcvxfty82v9v86yrdq2qt5r2dynu6huzcvjl6vajrvv5e2nntmhmh4vejy58gm4vw5m4qm8t02afknuvry6zuk0d9qvhu8v3lsyzadx9xfjudgjchf2463uegeydaq2y8lacv7rnp7u0wyqx5frp6eht8lrclw8ktf6yz54n9hlpdaq',
|
|
'ur:bytes/5of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/nh55upgt8ksh3hclwmqq5dnvk2qpl27lrg0fpnfu630vk75npfqtz529tamtwfk42te3cgfjfxfd5ftllz779y3al4ws66u8yvl6ug2llt97ektzfyyeul35yl2n8k6kekcfcar4kn8rx98r8ape2wnnwzrxesgashcqkud325gtgmztf7jfp3nqmhld5r8trwpxtx2l',
|
|
'ur:bytes/3of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/mw5rf7ttadjvzn35ymas2x5ndwjp26dtn8qqv6ndnsrh0fy7f8nvhtfy6u32f376zyjryeujvju6ms9gelua68lqa60wyarldf59xlpcnfes8gd0y0znfmnrj27p0vzv7rauua5fue4kwwjypsz2j32qqkcvwenyvwg3x3vwklgfqthlqng3zwxw928wz65u6lyfyeg5',
|
|
'ur:bytes/6of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/wpcuz4ddrdt5vh7up75p5ule7xdvfpeq982cgnqc8rmn96qrqsm88cnvh3d4z2t6xf8lqz3d94pz9wk426un6f7gudmw8lu0n5mmxpe5zpcgaweafht5w0f8yy33pdc68se6tj8c0azgy3jqulufwr6wm2fkgx2us753zu4c7zzlzaekg8w7rn3pjwr5vg6q2k7fw88z',
|
|
'ur:bytes/1of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/typjqlj2vyu9uf2snqd5k43n4vtcavrh5vzst7748ug8asggre70pj3uphqtl6jm30a4umlujxhazpxr4f6kyy94m0z3rr739jr7uppxnq2m565edzsdp5ah4xmrzwp2x678p2mzd4t8pd953luy8axe59trr2n8c740ptrvul3mlupm9jty8cehter5j0zwp7rr2v5a',
|
|
]
|
|
result = decode_ur(workloads)
|
|
# print('test_random_part_order: result = '.format(result))
|
|
if result == '7e4a61385e2550981b4b5633ab178eb077a30505fbd53f107ec1081e7cf0ca3c0dc0bfea5b8bfb5e6ffc91afd104c3aa756210b5dbc5118fd12c87ee04269815ba6a9968a0d0d3b7a9b631382a36bc70ab626d5670b4b48ff843f4d9a15631aa67c7aaf0ac6ce7e3bff03b2c9643e3375e47493c4e0f8635329d66fdec41b10ce74dcbf25fc15d829e7830c325643a98561f441b40a02e8353493e6afc16192fe99d90d8ca65539af77ddeaccc8943a37563a9ba83675bd5d4da7c60c9a172cf6940cbf0ec8fe04175a629932e3512c5d2aaea3cca3246f40a21ffdc33c3987dc7b880351230eb3759fe3c7dc7b2d3a20a95996ff0b7a0dba834f96beb64c14e3426fb051a936ba41569ab99c0066a6d9c0777a49e49e6cbad24d722a4c7da112432679264b9adc0a8cff9dd1fe0ee9ee2747f6a68537c389a7303a1af23c534ee6392bc17b04cf0fbce7689e66b673a440c04a9454005b0c76664639113458eb7d0902eff04d11138ce2a8ee16a9cd7c8926514efa9bd83ae7a4c139835f0fe0f68c628e0645c8524c30dfc314e825a7aa13224d98e2f7a9d12183a999bb1f28549c99a9072d99c05c24e0c84848c4fc147a094ab7b69e9cbea86952fccf15500fbb234ffe6ee6e6ded515c8016cb017ba36fb931ef276cec4ed22c1aed1495d2df3b3ce66c03f5b9ffa8434bf0e8fb149de94e050b3da178df1f76c00a366cb2801fabdf1a1e90cd3cd45ecb7a930a40b151455f76b726d552f31c21324992da257ff8bde2923dfd5d0d6b87233fae215ffacbecd96249099e7e3427d533db56cdb09c7475b4ce3314e33f43953a7370866cc11d85f00b71b15510b46c4b4fa490c660ddfeda0ceb1b8265995f7071c155ad1b57465fdc0fa81a73f9f19ac4872029d5844c1838f732e803043673e26cbc5b51297a324ff00a2d2d4222bad556b93d27c8e376e3ff8f9d37b3073410708ebb3d4dd7473d27212310b71a3c33a5c8f87f44824640e7f8970f4eda9364195c87a91172b8f085f1773641dde1ce21938746234055bc971ce2325f814e3eec60f781dd4faf52afd5be4a6b38656f7e9739f724cb7ccd4e4d01e802add3dc7b83191f894b3ee0ed752ee514d5ec55':
|
|
print('decode_ur() worked!')
|
|
else:
|
|
print('decode_ur() failed!')
|
|
|
|
async def battery_mon(*a):
|
|
from battery_mon import battery_mon
|
|
await battery_mon()
|
|
|