hobbled mode support for spending policy
This commit is contained in:
parent
7d20f03639
commit
980bfd9b1c
@ -319,7 +319,7 @@ Press (6) to prove you read to the end of this message.''', title='WARNING', esc
|
||||
if ch == '6': break
|
||||
|
||||
# do the actual picking
|
||||
pin = await lll.get_new_pin(title)
|
||||
pin = await lll.get_new_pin()
|
||||
del lll
|
||||
|
||||
if pin is None: return
|
||||
@ -573,8 +573,11 @@ async def clear_seed(*a):
|
||||
# This is super dangerous for the customer's money.
|
||||
import seed
|
||||
|
||||
if await any_active_duress_ux():
|
||||
return await ux_aborted()
|
||||
# in hobble mode, they cannot reach duress wallets and/or maybe we don't
|
||||
# want to reveal them? So don't block them based on that.
|
||||
if not pa.hobbled_mode:
|
||||
if await any_active_duress_ux():
|
||||
return await ux_aborted()
|
||||
|
||||
if not await ux_confirm('Wipe seed words and reset wallet. '
|
||||
'All funds will be lost. '
|
||||
@ -587,7 +590,7 @@ async def clear_seed(*a):
|
||||
if not await ux_confirm('''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.''', confirm_key='4'):
|
||||
new wallet.''', 'AGAIN...', confirm_key='4'):
|
||||
return await ux_aborted()
|
||||
|
||||
# clear all trick PINs from SE2
|
||||
@ -800,26 +803,41 @@ async def start_login_sequence():
|
||||
|
||||
# If that didn't work, or no skip defined, force
|
||||
# them to login successfully.
|
||||
|
||||
sp_unlock = False
|
||||
try:
|
||||
from trick_pins import tp
|
||||
|
||||
# Get a PIN and try to use it to login
|
||||
# - does warnings about attempt usage counts
|
||||
await block_until_login()
|
||||
|
||||
sp_unlock = tp.was_sp_unlock()
|
||||
if sp_unlock:
|
||||
# Trying to unlock spending policy: ask for main PIN next.
|
||||
while 1:
|
||||
await ux_show_story("Spending Policy Unlock: Please provide Main PIN next.")
|
||||
|
||||
pa.reset()
|
||||
await block_until_login()
|
||||
|
||||
# we don't really know if that was the Main PIN (could easily be the bypass
|
||||
# PIN again) and if it's a duress wallet, that's cool... so just be
|
||||
# sure we got some secret material to work/play with
|
||||
if pa.has_secrets(): break
|
||||
|
||||
# Do we need to do countdown delay? (real or otherwise)
|
||||
# Q/Mk4 approach:
|
||||
# - wiping has already occured if that was picked
|
||||
# - wiping has already occured if that was selected by trick details
|
||||
# - 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
|
||||
# Maybe they do know the right PIN, but always 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 ux_login_countdown(delay * (60 if not version.is_devmode else 1))
|
||||
|
||||
# keep it simple for Mk4+: just challenge again for any PIN
|
||||
@ -847,14 +865,31 @@ async def start_login_sequence():
|
||||
# handle upgrades/downgrade issues
|
||||
try:
|
||||
await version_migration()
|
||||
except:
|
||||
pass
|
||||
except: pass
|
||||
|
||||
# Maybe insist on the "right" microSD being already installed?
|
||||
try:
|
||||
from pwsave import MicroSD2FA
|
||||
MicroSD2FA.enforce_policy()
|
||||
except: pass # robustness: keep going!
|
||||
except: pass
|
||||
|
||||
# apply the hobbling for the spending policy, if appropriate
|
||||
try:
|
||||
from ccc import sssp_spending_policy, sssp_word_challenge
|
||||
|
||||
if sp_unlock and sssp_spending_policy('words'):
|
||||
# challenge them also for first and last seed word! (will reboot on fail)
|
||||
await sssp_word_challenge()
|
||||
|
||||
if sp_unlock:
|
||||
# Disable spending policy going forward; user has to re-enable.
|
||||
pa.hobbled_mode = False
|
||||
sssp_spending_policy('en', change=False)
|
||||
else:
|
||||
# normal entry mode, but might have policy enabled, if so enable it now.
|
||||
pa.hobbled_mode = sssp_spending_policy('en')
|
||||
|
||||
except: pass
|
||||
|
||||
# implement idle timeout now that we are logged-in
|
||||
IMPT.start_task('idle', idle_logout())
|
||||
@ -943,7 +978,7 @@ async def restore_main_secret(*a):
|
||||
goto_top_menu()
|
||||
|
||||
def make_top_menu():
|
||||
from flow import VirginSystem, NormalSystem, EmptyWallet, FactoryMenu
|
||||
from flow import VirginSystem, NormalSystem, EmptyWallet, FactoryMenu, HobbledTopMenu
|
||||
from glob import hsm_active, settings
|
||||
from pincodes import pa
|
||||
|
||||
@ -955,6 +990,9 @@ def make_top_menu():
|
||||
elif pa.is_blank():
|
||||
# let them play a little before picking a PIN first time
|
||||
m = MenuSystem(VirginSystem, should_cont=lambda: pa.is_blank())
|
||||
elif pa.hobbled_mode:
|
||||
# let them do a few things, but not all the things.
|
||||
m = MenuSystem(HobbledTopMenu)
|
||||
else:
|
||||
assert pa.is_successful(), "nonblank but wrong pin"
|
||||
|
||||
@ -2013,7 +2051,7 @@ Write it down.'''
|
||||
while 1:
|
||||
lll.reset()
|
||||
lll.subtitle = "New " + title
|
||||
pin = await lll.get_new_pin(title)
|
||||
pin = await lll.get_new_pin()
|
||||
|
||||
if pin is None:
|
||||
return await ux_aborted()
|
||||
|
||||
291
shared/ccc.py
291
shared/ccc.py
@ -2,6 +2,13 @@
|
||||
#
|
||||
# ccc.py - ColdCard Co-sign feature. Be a leg in a 2-of-3 that is signed based on a policy.
|
||||
#
|
||||
# Rebranding/single-signer addtions:
|
||||
#
|
||||
# - "CCC" will now be branded as "Spending Policy (Co-Sign)" was "ColdCard Cosigning"
|
||||
# - single singer policies will be called "Spending Policy"
|
||||
# - internally: CCC is the multisig stuff, vs SSSP: Single Signer Spending Policy
|
||||
# - "hobbled" refers to less-than full control over Coldcard, even though you have main PIN
|
||||
#
|
||||
import gc, chains, version, ngu, web2fa, bip39, re
|
||||
from chains import NLOCK_IS_TIME
|
||||
from utils import swab32, xfp2str, truncate_address, deserialize_secret, show_single_address
|
||||
@ -886,4 +893,288 @@ async def key_c_challenge(words):
|
||||
m = CCCConfigMenu()
|
||||
the_ux.push(m)
|
||||
|
||||
def sssp_spending_policy(key, default=False, change=None):
|
||||
# This function can be used to check if feature(s) are enabled in
|
||||
# the single-signer policy settings. Might be used while hobbled.
|
||||
# keys:
|
||||
# 'en' = feature enabled; hobble on next boot
|
||||
# 'notes' = allow access to knows
|
||||
# 'words' = add first/last seed words to challenge to unlock
|
||||
# 'okeys' = allow BIP-39 and/or seed vault
|
||||
|
||||
v = settings.get('sssp', dict())
|
||||
|
||||
if key in { 'en', 'notes', 'words', 'okeys' }:
|
||||
# booleans: present or removed from dict
|
||||
if change is not None:
|
||||
if change:
|
||||
v[key] = True
|
||||
else:
|
||||
v.pop(key, None)
|
||||
|
||||
settings.put('sssp', v)
|
||||
settings.save()
|
||||
|
||||
return (key in v) or default
|
||||
|
||||
raise KeyError(key)
|
||||
|
||||
return default
|
||||
|
||||
async def toggle_sssp_feature(*a):
|
||||
from pincodes import pa
|
||||
from actions import goto_top_menu
|
||||
|
||||
if pa.hobbled_mode == 2:
|
||||
# allow exit from test-drive mode, directly into editing settings
|
||||
pa.hobbled_mode = False
|
||||
goto_top_menu()
|
||||
elif settings.get('sssp'):
|
||||
# normal entry into menu system, after the first time
|
||||
assert not pa.hobbled_mode
|
||||
else:
|
||||
en = await sssp_enable()
|
||||
if not en: return
|
||||
|
||||
m = SSSPConfigMenu()
|
||||
the_ux.push(m)
|
||||
|
||||
async def sssp_enable():
|
||||
# enabling the feature
|
||||
# - collect and setup a new trick pin
|
||||
# - set sssp settings w/ something non-empty but still disabled.
|
||||
# - return T if they completed enabling process
|
||||
|
||||
from login import LoginUX
|
||||
from trick_pins import tp
|
||||
from pincodes import pa
|
||||
|
||||
# enable the feature -- not simple!
|
||||
# - pick new (trick pin) that lets you back here.
|
||||
# - collect a policy setup, maybe 2FA enrol too
|
||||
# - lock that down
|
||||
ch = await ux_show_story('''\
|
||||
You can define a "spending policy" which stops you from signing \
|
||||
transactions unless conditions are met.
|
||||
Spending policies can restrict: magnitude (BTC out), \
|
||||
velocity (blocks between txn), address whitelisting, \
|
||||
and/or require confirmation by 2FA phone app.
|
||||
|
||||
When active, your COLDCARD \
|
||||
is locked into a special mode that restricts seed access, backups, settings and other features.
|
||||
|
||||
First step is to define a new PIN code that is used when you want to bypass or \
|
||||
disable this feature.
|
||||
''',
|
||||
title="Spending Policy")
|
||||
|
||||
if ch != 'y':
|
||||
# just a tourist
|
||||
return
|
||||
|
||||
|
||||
# re-use existing PIN if there for some reason
|
||||
new_pin = tp.has_sp_unlock()
|
||||
|
||||
if not new_pin:
|
||||
# all existing PINS
|
||||
have = set(tp.all_tricks())
|
||||
have.add(pa.pin.decode())
|
||||
|
||||
while 1:
|
||||
lll = LoginUX()
|
||||
lll.is_setting = True
|
||||
lll.subtitle = "Spending Policy" + (" Unlock" if version.has_qwerty else '')
|
||||
|
||||
new_pin = await lll.get_new_pin()
|
||||
if new_pin is None:
|
||||
return
|
||||
|
||||
if (new_pin not in have):
|
||||
tp.define_unlock_pin(new_pin)
|
||||
break
|
||||
|
||||
await ux_show_story("That PIN (%s) is already in use. All PIN codes must be unique."
|
||||
% new_pin)
|
||||
|
||||
# all features disabled to to start
|
||||
settings.set('sssp', dict(en=False, pol={}))
|
||||
settings.save()
|
||||
|
||||
# continue into config menu
|
||||
return True
|
||||
|
||||
async def sssp_word_challenge(*a):
|
||||
# Ask for first/last seed word and verify. Return if correct answers given.
|
||||
# Reboots on failure.
|
||||
from stash import SensitiveValues
|
||||
|
||||
with SensitiveValues() as sv:
|
||||
if sv.mode == 'words':
|
||||
words = bip39.b2a_words(sv.raw).split(' ')
|
||||
want_words = words[:1] + words[-1:]
|
||||
assert len(want_words) == 2
|
||||
else:
|
||||
# they are using XPRV or something, skip test entirely
|
||||
return
|
||||
|
||||
got_words = None
|
||||
for retry in range(2):
|
||||
if version.has_qwerty:
|
||||
# see special rendering code for this case in ux_q1.py:ux_draw_words(num_words=2)
|
||||
from ux_q1 import seed_word_entry
|
||||
got_words = await seed_word_entry('First and Last Seed Words', 2, has_checksum=False)
|
||||
else:
|
||||
from seed import WordNestMenu
|
||||
|
||||
# TODO: fix bugs here on Mk4. really not working. XXX
|
||||
|
||||
got_words = None
|
||||
async def check_challenge_cb(words):
|
||||
WordNestMenu.pop_all()
|
||||
got_words = words
|
||||
|
||||
m = WordNestMenu(num_words=2, has_checksum=False, done_cb=check_challenge_cb)
|
||||
the_ux.push(m)
|
||||
await m.interact()
|
||||
|
||||
if got_words == want_words:
|
||||
# success - done
|
||||
return
|
||||
|
||||
await ux_show_story("Sorry, those words are incorrect.")
|
||||
|
||||
# they failed; log them out ... they can just try login again
|
||||
from actions import login_now
|
||||
login_now()
|
||||
|
||||
# NOT-REACHED
|
||||
|
||||
class SSSPCheckedMenuItem(MenuItem):
|
||||
# Show a checkmark if **top level** security setting is defined and not the default
|
||||
# - only works inside SSSPPolicyMenu?
|
||||
# - similar to menu.py:ToggleMenuItem
|
||||
|
||||
def __init__(self, label, polkey, story, **kws):
|
||||
super().__init__(label, **kws)
|
||||
self.polkey = polkey
|
||||
self.story = story
|
||||
|
||||
def is_chosen(self):
|
||||
# should we show a check in menu? check the current SSSP settings
|
||||
return sssp_spending_policy(self.polkey)
|
||||
|
||||
async def activate(self, menu, idx):
|
||||
# do simple toggle on request
|
||||
was = sssp_spending_policy(self.polkey)
|
||||
|
||||
msg = self.story + "\n\n%s?" % ('Disable' if was else 'Enable')
|
||||
|
||||
ch = await ux_show_story(msg)
|
||||
if ch == 'x': return
|
||||
|
||||
sssp_spending_policy(self.polkey, change=(not was))
|
||||
|
||||
|
||||
class SSSPConfigMenu(MenuSystem):
|
||||
def __init__(self):
|
||||
items = self.construct()
|
||||
super().__init__(items)
|
||||
|
||||
def update_contents(self):
|
||||
tmp = self.construct()
|
||||
self.replace_items(tmp)
|
||||
|
||||
def construct(self):
|
||||
from multisig import MultisigWallet, make_ms_wallet_menu
|
||||
|
||||
my_xfp = CCCFeature.get_xfp()
|
||||
items = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem('Spending Policy'), # just a title?
|
||||
MenuItem('Set Policy...'), #, menu=CCCPolicyMenu.be_a_submenu),
|
||||
SSSPCheckedMenuItem('Word Check', 'notes', 'Allow (read-only) access to secure notes and passwords? Otherwise, they are inaccessible.'),
|
||||
SSSPCheckedMenuItem('Allow Notes', 'words', 'To change Spending Policy, addition to special PIN, you must provide the first and last seed words.'),
|
||||
SSSPCheckedMenuItem('Related Keys', 'okeys', 'Allow access to BIP-39 passphrase wallets based on master seed, or Seed Vault (if any). Spending Policy applies too all.'),
|
||||
#MenuItem('Test Word Challenge', f=sssp_word_challenge), # XXX test only?
|
||||
]
|
||||
|
||||
if CCCFeature.last_fail_reason:
|
||||
# xxxxxxxxxxxxxxxx
|
||||
items.insert(1, MenuItem('Last Violation', f=self.debug_last_fail))
|
||||
|
||||
items.append(MenuItem('Remove Policy', f=self.remove_sssp))
|
||||
items.append(MenuItem('Test Drive', f=self.test_drive))
|
||||
items.append(MenuItem('ACTIVATE', f=self.activate_feature))
|
||||
|
||||
return items
|
||||
|
||||
async def activate_feature(self, *a):
|
||||
# Policy is being set in stone now; confirm and switch to hobble mode, etc.
|
||||
from trick_pins import tp
|
||||
|
||||
bypass_pin = tp.has_sp_unlock()
|
||||
|
||||
if not bypass_pin:
|
||||
msg = "You have no Spending Policy bypass PIN defined, so changes to this COLDCARD cannot be made past this point. Only option will be to destroy seed and reload everything."
|
||||
else:
|
||||
msg = "To return to normal unlimited spending mode, you will need to enter the special pin (%s), then the Main PIN" % bypass_pin
|
||||
if sssp_spending_policy('words'):
|
||||
msg += ', followed by the first and last seed words'
|
||||
msg += '.'
|
||||
|
||||
if not await ux_confirm(msg, 'CONTINUE?'):
|
||||
return
|
||||
|
||||
# set it for next login
|
||||
sssp_spending_policy('en', change=True)
|
||||
|
||||
# make it real ... could reboot here instead, but no need.
|
||||
from pincodes import pa
|
||||
from actions import goto_top_menu
|
||||
|
||||
pa.hobbled_mode = True
|
||||
goto_top_menu()
|
||||
|
||||
async def test_drive(self, *a):
|
||||
# allow test drive of feature
|
||||
if not await ux_confirm("See what COLDCARD operation will look like with Spending Policy enabled.", 'CONTINUE?'):
|
||||
return
|
||||
|
||||
from pincodes import pa
|
||||
from actions import goto_top_menu
|
||||
|
||||
pa.hobbled_mode = 2 # Truthy value to indicate they can escape easily
|
||||
goto_top_menu()
|
||||
|
||||
async def debug_last_fail(self, *a):
|
||||
# debug for customers: why did we reject that last txn?
|
||||
pol = CCCFeature.get_policy()
|
||||
bh = pol.get('block_h', None)
|
||||
msg = ''
|
||||
if bh:
|
||||
msg += "CCC height:\n\n%s\n\n" % bh
|
||||
|
||||
msg += 'The most recent policy check failed because of:\n\n%s\n\nPress (4) to clear.' \
|
||||
% CCCFeature.last_fail_reason
|
||||
ch = await ux_show_story(msg, escape='4')
|
||||
|
||||
if ch == '4':
|
||||
CCCFeature.last_fail_reason = ''
|
||||
self.update_contents()
|
||||
|
||||
async def remove_sssp(self, *a):
|
||||
# disable and remove feature
|
||||
if not await ux_confirm('Bypass PIN will be removed, and all spending policy settings forgotten.'):
|
||||
return
|
||||
|
||||
settings.remove_key('sssp')
|
||||
settings.save()
|
||||
|
||||
from trick_pins import tp
|
||||
tp.delete_sp_unlock_pins()
|
||||
|
||||
the_ux.pop()
|
||||
|
||||
|
||||
# EOF
|
||||
|
||||
@ -19,7 +19,7 @@ from countdowns import countdown_chooser
|
||||
from paper import make_paper_wallet
|
||||
from trick_pins import TrickPinMenu
|
||||
from tapsigner import import_tapsigner_backup_file
|
||||
from ccc import toggle_ccc_feature
|
||||
from ccc import toggle_ccc_feature, sssp_spending_policy, toggle_sssp_feature
|
||||
|
||||
# useful shortcut keys
|
||||
from charcodes import KEY_QR, KEY_NFC
|
||||
@ -100,6 +100,15 @@ def hsm_available():
|
||||
# contains hsm feature + can it be used (needs se2 secret and no tmp active)
|
||||
return version.supports_hsm and has_real_secret()
|
||||
|
||||
def qr_and_ms():
|
||||
# has QR scanner, and at least one MS wallet
|
||||
if not version.has_qr: return False
|
||||
return bool(settings.get('multisig', False))
|
||||
|
||||
def is_hobble_testdrive():
|
||||
from pincodes import pa
|
||||
return (pa.hobbled_mode == 2)
|
||||
|
||||
async def goto_home(*a):
|
||||
goto_top_menu()
|
||||
|
||||
@ -376,9 +385,9 @@ AdvancedNormalMenu = [
|
||||
story=("Enable HSM? Enables all user management commands, and other HSM-only USB commands. "
|
||||
"By default these commands are disabled."),
|
||||
predicate=hsm_available),
|
||||
NonDefaultMenuItem('Coldcard Co-Signing', 'ccc', f=toggle_ccc_feature, predicate=is_not_tmp),
|
||||
MenuItem('User Management', menu=make_users_menu,
|
||||
predicate=hsm_available),
|
||||
NonDefaultMenuItem('Spending Policy', 'sssp', f=toggle_sssp_feature, predicate=has_real_secret, shortcut='p'),
|
||||
NonDefaultMenuItem('Spending Policy: Co-Signing', 'ccc', f=toggle_ccc_feature, predicate=is_not_tmp), # XXX mk4 width
|
||||
MenuItem('User Management', menu=make_users_menu, predicate=hsm_available),
|
||||
MenuItem('NFC Tools', predicate=nfc_enabled, menu=NFCToolsMenu, shortcut=KEY_NFC),
|
||||
MenuItem("Danger Zone", menu=DangerZoneMenu, shortcut='z'),
|
||||
]
|
||||
@ -460,3 +469,71 @@ FactoryMenu = [
|
||||
MenuItem("Debug Functions", menu=DebugFunctionsMenu, shortcut='f'),
|
||||
MenuItem("Perform Selftest", f=start_selftest, shortcut='s'),
|
||||
]
|
||||
|
||||
# Special menus for hobbled mode where we have a (single signer) spending policy in effect.
|
||||
# - no access to secrets, backups, firmware up/downgrades.
|
||||
# - secure notes, but readonly; can be disabled completely.
|
||||
# - key teleport, but only for PSBT & multisig purposes.
|
||||
# - can only be enabled after we have secrets, so no need for has_secrets tests here
|
||||
#
|
||||
|
||||
# Slightly limited file menu when hobbled.
|
||||
# - no backup/restore
|
||||
HobbledFileMgmtMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem('Sign Text File', f=sign_message_on_sd),
|
||||
MenuItem('Batch Sign PSBT', f=batch_sign),
|
||||
MenuItem('List Files', f=list_files),
|
||||
MenuItem('Export Wallet', predicate=has_secrets, menu=WalletExportMenu), #dup elsewhere
|
||||
MenuItem('Verify Sig File', f=verify_sig_file),
|
||||
MenuItem('NFC File Share', predicate=nfc_enabled, f=nfc_share_file, shortcut=KEY_NFC),
|
||||
MenuItem('BBQr File Share', predicate=version.has_qr, f=qr_share_file, arg=True),
|
||||
MenuItem('QR File Share', predicate=version.has_qr, f=qr_share_file, shortcut=KEY_QR),
|
||||
MenuItem('Format SD Card', f=wipe_sd_card),
|
||||
MenuItem('Format RAM Disk', predicate=vdisk_enabled, f=wipe_vdisk),
|
||||
]
|
||||
|
||||
# NFC tools when hobbled: not much different.
|
||||
HobbledNFCToolsMenu = [
|
||||
MenuItem('Sign PSBT', f=nfc_sign_psbt),
|
||||
MenuItem('Show Address', f=nfc_show_address),
|
||||
MenuItem('Sign Message', f=nfc_sign_msg),
|
||||
MenuItem('Verify Sig File', f=nfc_sign_verify),
|
||||
MenuItem('Verify Address', f=nfc_address_verify),
|
||||
MenuItem('File Share', f=nfc_share_file),
|
||||
MenuItem('Push Transaction', f=nfc_pushtx_file,
|
||||
predicate=lambda: settings.get("ptxurl", False)),
|
||||
]
|
||||
|
||||
# Very limited advanced menu when hobbled.
|
||||
HobbledAdvancedMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem("File Management", menu=HobbledFileMgmtMenu),
|
||||
MenuItem('Export Wallet', predicate=has_secrets, menu=WalletExportMenu, shortcut='x'), # also inside FileMgmt
|
||||
MenuItem('Teleport Multisig PSBT', predicate=qr_and_ms, f=kt_send_file_psbt),
|
||||
MenuItem("View Identity", f=view_ident),
|
||||
MenuItem('Paper Wallets', f=make_paper_wallet),
|
||||
MenuItem('NFC Tools', predicate=nfc_enabled, menu=HobbledNFCToolsMenu, shortcut=KEY_NFC),
|
||||
MenuItem("Destroy Seed", f=clear_seed),
|
||||
]
|
||||
|
||||
# Main menu when a spending policy (hobbled) is in effect.
|
||||
HobbledTopMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem('Ready To Sign', f=ready2sign, shortcut='r'),
|
||||
MenuItem('Passphrase', menu=start_b39_pw,
|
||||
predicate=lambda: word_based_seed and sssp_spending_policy('okeys'), shortcut='p'),
|
||||
MenuItem('Scan Any QR Code', predicate=version.has_qr,
|
||||
shortcut=KEY_QR, f=scan_any_qr, arg=(False, True)),
|
||||
MenuItem("Address Explorer", menu=address_explore, shortcut='x'),
|
||||
MenuItem('Secure Notes & Passwords', menu=make_notes_menu, shortcut='n',
|
||||
predicate=lambda: settings.get("secnap", False) and sssp_spending_policy('notes')),
|
||||
MenuItem('Type Passwords', f=password_entry, shortcut='t',
|
||||
predicate=lambda: settings.get("emu", False)),
|
||||
MenuItem('Seed Vault', menu=make_seed_vault_menu, shortcut='v',
|
||||
predicate=lambda: settings.master_get('seedvault') and sssp_spending_policy('okeys')),
|
||||
MenuItem('Advanced/Tools', menu=HobbledAdvancedMenu, shortcut='t'),
|
||||
MenuItem('Secure Logout', f=logout_now, predicate=not version.has_battery),
|
||||
MenuItem('EXIT TEST DRIVE', f=toggle_sssp_feature, predicate=is_hobble_testdrive),
|
||||
ShortcutItem(KEY_NFC, predicate=nfc_enabled, menu=HobbledNFCToolsMenu),
|
||||
]
|
||||
|
||||
@ -270,7 +270,7 @@ suffix break point is correct.\n\n'''
|
||||
return await self.interact()
|
||||
|
||||
|
||||
async def get_new_pin(self, title, story=None):
|
||||
async def get_new_pin(self, title=None, story=None):
|
||||
# Do UX flow to get new (or change) PIN. Always does the double-entry thing
|
||||
self.is_setting = True
|
||||
|
||||
|
||||
@ -119,7 +119,7 @@ class ShortcutItem(MenuItem):
|
||||
super().__init__('SHORTCUT', shortcut=key, **kws)
|
||||
|
||||
class NonDefaultMenuItem(MenuItem):
|
||||
# Show a checkmark if setting is defined and not the default ... so know know it's set
|
||||
# Show a checkmark if setting is defined and not the default
|
||||
def __init__(self, label, nvkey, prelogin=False, default_value=None, **kws):
|
||||
super().__init__(label, **kws)
|
||||
self.nvkey = nvkey
|
||||
|
||||
@ -21,6 +21,14 @@ from utils import problem_file_line, url_unquote, wipe_if_deltamode
|
||||
ONE_LINE = CHARS_W-2
|
||||
|
||||
async def make_notes_menu(*a):
|
||||
if pa.hobbled_mode:
|
||||
# Read only version of menu system
|
||||
# - used when spending policy in effect
|
||||
# - must have some notes already, or unreachable
|
||||
assert NoteContent.count()
|
||||
rv = NotesMenu(NotesMenu.construct_readonly())
|
||||
rv.readonly = True
|
||||
return rv
|
||||
|
||||
if not settings.get('secnap', False):
|
||||
# Explain feature, and then enable if interested. Drop them into menu.
|
||||
@ -105,6 +113,8 @@ async def get_a_password(old_value, min_len=0, max_len=128):
|
||||
|
||||
class NotesMenu(MenuSystem):
|
||||
|
||||
readonly = False
|
||||
|
||||
@classmethod
|
||||
def construct(cls):
|
||||
# Dynamic menu with user-defined names of notes shown
|
||||
@ -121,7 +131,8 @@ class NotesMenu(MenuSystem):
|
||||
|
||||
rv = []
|
||||
for note in NoteContent.get_all():
|
||||
rv.append(MenuItem('%d: %s' % (note.idx+1, note.title), menu=note.make_menu))
|
||||
rv.append(MenuItem('%d: %s' % (note.idx+1, note.title),
|
||||
menu=lambda *_: note.make_menu()))
|
||||
|
||||
rv.extend(news)
|
||||
|
||||
@ -134,6 +145,18 @@ class NotesMenu(MenuSystem):
|
||||
|
||||
return rv
|
||||
|
||||
@classmethod
|
||||
def construct_readonly(cls):
|
||||
# When only allowed to view, no export/add new/delete.
|
||||
wipe_if_deltamode()
|
||||
|
||||
rv = []
|
||||
for note in NoteContent.get_all():
|
||||
rv.append(MenuItem('%d: %s' % (note.idx+1, note.title),
|
||||
menu=lambda *_: note.make_menu(readonly=True)))
|
||||
|
||||
return rv
|
||||
|
||||
@classmethod
|
||||
async def export_all(cls, *a):
|
||||
await start_export(NoteContent.get_all())
|
||||
@ -344,25 +367,32 @@ class PasswordContent(NoteContentBase):
|
||||
flds = ['title', 'user', 'password', 'site', 'misc' ]
|
||||
type_label = 'password'
|
||||
|
||||
async def make_menu(self, *a):
|
||||
async def make_menu(self, readonly=False):
|
||||
rv = [MenuItem('"%s"' % self.title, f=self.view)]
|
||||
if self.user:
|
||||
rv.append(MenuItem('↳ %s' % self.user, f=self.view))
|
||||
if self.site:
|
||||
rv.append(MenuItem('↳ %s' % self.site, f=self.view))
|
||||
#if self.misc: rv.append(MenuItem('↳ (notes)', f=self.view))
|
||||
return rv + [
|
||||
rv += [
|
||||
MenuItem('View Password', f=self.view_pw),
|
||||
MenuItem('Send Password', f=self.send_pw, predicate=lambda: settings.get('du', True)),
|
||||
MenuItem('Export', f=self.export),
|
||||
MenuItem('Edit Metadata', f=self.edit),
|
||||
MenuItem('Delete', f=self.delete),
|
||||
MenuItem('Change Password', f=self.change_pw),
|
||||
]
|
||||
if not readonly:
|
||||
rv += [
|
||||
MenuItem('Export', f=self.export),
|
||||
MenuItem('Edit Metadata', f=self.edit),
|
||||
MenuItem('Delete', f=self.delete),
|
||||
MenuItem('Change Password', f=self.change_pw),
|
||||
]
|
||||
rv += [
|
||||
self.sign_misc_menu_item(),
|
||||
ShortcutItem(KEY_QR, f=self.view_qr_menu, arg=self.type_label),
|
||||
ShortcutItem(KEY_NFC, f=self.share_nfc, arg=self.type_label),
|
||||
]
|
||||
|
||||
return rv
|
||||
|
||||
async def view(self, *a):
|
||||
pl = len(self.password)
|
||||
m = ''
|
||||
@ -476,18 +506,24 @@ class NoteContent(NoteContentBase):
|
||||
flds = ['title', 'misc']
|
||||
type_label = 'note'
|
||||
|
||||
async def make_menu(self, *a):
|
||||
async def make_menu(self, readonly=False):
|
||||
# Details and actions for this Note
|
||||
return [
|
||||
rv = [
|
||||
MenuItem('"%s"' % self.title, f=self.view),
|
||||
MenuItem('View Note', f=self.view),
|
||||
MenuItem('Edit', f=self.edit),
|
||||
MenuItem('Delete', f=self.delete),
|
||||
MenuItem('Export', f=self.export),
|
||||
]
|
||||
if not readonly:
|
||||
rv += [
|
||||
MenuItem('Edit', f=self.edit),
|
||||
MenuItem('Delete', f=self.delete),
|
||||
MenuItem('Export', f=self.export),
|
||||
]
|
||||
rv += [
|
||||
self.sign_misc_menu_item(),
|
||||
ShortcutItem(KEY_QR, f=self.view_qr_menu, arg="misc"),
|
||||
ShortcutItem(KEY_NFC, f=self.share_nfc, arg='misc'),
|
||||
]
|
||||
return rv
|
||||
|
||||
async def view(self, *a):
|
||||
ch = await ux_show_story(self.misc, title=self.title, escape=KEY_QR,
|
||||
|
||||
@ -67,6 +67,7 @@ from utils import call_later_ms
|
||||
# msas = multisig address show (do not censor multisig addresses)
|
||||
# ccc = (complex) If present, CCC feature is enabled and key details stored here.
|
||||
# ktrx = (privkey) Key teleport Rx has been started, this will be our keypair
|
||||
# sssp = (complex) If present, spending-policy (single signer) feature are defined
|
||||
|
||||
# Stored w/ key=00 for access before login
|
||||
# _skip_pin = hard code a PIN value (dangerous, only for debug)
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
# pincodes.py - manage PIN code (which map to wallet seeds)
|
||||
#
|
||||
import ustruct, ckcc, version, chains, stash
|
||||
# from ubinascii import hexlify as b2a_hex
|
||||
from callgate import enter_dfu
|
||||
from bip39 import wordlist_en
|
||||
|
||||
@ -127,6 +126,9 @@ class PinAttempt:
|
||||
self.private_state = 0 # opaque data, but preserve
|
||||
self.cached_main_pin = bytearray(32)
|
||||
|
||||
# If set, a spending policy is in effect, and so even tho we know the master
|
||||
# seed, we are not going to let them see it, nor sign things we dont like, etc.
|
||||
self.hobbled_mode = False
|
||||
|
||||
assert MAX_PIN_LEN == 32 # update FMT otherwise
|
||||
assert ustruct.calcsize(PIN_ATTEMPT_FMT_V1) == PIN_ATTEMPT_SIZE_V1
|
||||
@ -533,6 +535,7 @@ class PinAttempt:
|
||||
from trick_pins import TC_DELTA_MODE
|
||||
return bool(self.delay_required & TC_DELTA_MODE)
|
||||
|
||||
|
||||
def get_tc_values(self):
|
||||
# Mk4 only
|
||||
# return (tc_flags, tc_arg)
|
||||
|
||||
@ -879,6 +879,8 @@ class SeedVaultMenu(MenuSystem):
|
||||
ch = await ux_show_story(title="[" + rec.xfp + "]", msg=msg, escape=esc)
|
||||
if ch == "x": return
|
||||
|
||||
assert not pa.hobbled_mode
|
||||
|
||||
dis.fullscreen("Saving...")
|
||||
|
||||
wipe_slot = not current_active and (ch != "1")
|
||||
@ -890,6 +892,7 @@ class SeedVaultMenu(MenuSystem):
|
||||
xs.blank()
|
||||
del xs
|
||||
|
||||
|
||||
# CAUTION: will get shadow copy if in tmp seed mode already
|
||||
seeds = settings.master_get("seeds", [])
|
||||
try:
|
||||
@ -926,6 +929,8 @@ class SeedVaultMenu(MenuSystem):
|
||||
from glob import dis
|
||||
from ux import ux_input_text
|
||||
|
||||
assert not pa.hobbled_mode
|
||||
|
||||
idx, old = item.arg
|
||||
new_label = await ux_input_text(old.label, confirm_exit=False, max_len=40)
|
||||
|
||||
@ -956,6 +961,8 @@ class SeedVaultMenu(MenuSystem):
|
||||
async def _add_current_tmp(*a):
|
||||
from pincodes import pa
|
||||
|
||||
assert not pa.hobbled_mode
|
||||
|
||||
assert pa.tmp_value
|
||||
main_xfp = settings.master_get("xfp", 0)
|
||||
|
||||
@ -997,6 +1004,7 @@ class SeedVaultMenu(MenuSystem):
|
||||
seeds = list(seed_vault_iter())
|
||||
|
||||
if not seeds:
|
||||
assert not pa.hobbled_mode
|
||||
rv.append(MenuItem('(none saved yet)'))
|
||||
if pa.tmp_value:
|
||||
rv.append(add_current_tmp)
|
||||
@ -1016,8 +1024,8 @@ class SeedVaultMenu(MenuSystem):
|
||||
submenu = [
|
||||
MenuItem(rec.label, f=cls._detail, arg=(rec, encoded)),
|
||||
MenuItem('Use This Seed', f=cls._set, arg=encoded),
|
||||
MenuItem('Rename', f=cls._rename, arg=(i, rec)),
|
||||
MenuItem('Delete', f=cls._remove, arg=(i, rec, encoded)),
|
||||
MenuItem('Rename', f=cls._rename, arg=(i, rec), predicate=not pa.hobbled_mode),
|
||||
MenuItem('Delete', f=cls._remove, arg=(i, rec, encoded), predicate=not pa.hobbled_mode),
|
||||
]
|
||||
if is_active:
|
||||
submenu[1] = MenuItem("Seed In Use")
|
||||
@ -1035,7 +1043,7 @@ class SeedVaultMenu(MenuSystem):
|
||||
rv.append(item)
|
||||
|
||||
if pa.tmp_value:
|
||||
if seeds and (not tmp_in_sv):
|
||||
if seeds and (not tmp_in_sv) and not pa.hobbled_mode:
|
||||
# give em chance to store current active
|
||||
rv.append(add_current_tmp)
|
||||
|
||||
@ -1137,6 +1145,8 @@ class EphemeralSeedMenu(MenuSystem):
|
||||
|
||||
|
||||
async def make_ephemeral_seed_menu(*a):
|
||||
assert not pa.hobbled_mode
|
||||
|
||||
if (not pa.tmp_value) and (not settings.master_get("seedvault", False)):
|
||||
# force a warning on them, unless they are already doing it.
|
||||
if not await ux_confirm(
|
||||
|
||||
@ -307,11 +307,17 @@ async def kt_accept_values(dtype, raw):
|
||||
- `b` - complete system backup file (text, internal format)
|
||||
'''
|
||||
from flow import has_se_secrets, goto_top_menu
|
||||
from pincodes import pa
|
||||
|
||||
enc = None
|
||||
origin = 'Teleported'
|
||||
label = None
|
||||
|
||||
if pa.hobbled_mode and dtype != 'p':
|
||||
await ux_show_story('Only PSBT for multisig accepted in this mode.', title='FAILED')
|
||||
return
|
||||
|
||||
|
||||
if dtype == 's':
|
||||
# words / bip 32 master / xprv, etc
|
||||
enc = bytearray(72)
|
||||
@ -475,6 +481,12 @@ def decode_step2(session_key, noid_key, body):
|
||||
async def kt_incoming(type_code, payload):
|
||||
# incoming BBQr was scanned (via main menu, etc)
|
||||
|
||||
from pincodes import pa
|
||||
if pa.hobbled_mode and type_code != 'E':
|
||||
# only PSBT rx is supported in hobbled mode
|
||||
# TODO: fail silently? good enough?
|
||||
return
|
||||
|
||||
if type_code == 'R':
|
||||
# they want to send to this guy
|
||||
return await kt_start_send(payload)
|
||||
@ -495,6 +507,10 @@ class SecretPickerMenu(MenuSystem):
|
||||
def __init__(self, rx_pubkey):
|
||||
self.rx_pubkey = rx_pubkey
|
||||
|
||||
# this menu should be unreachable in hobbled mode.
|
||||
from pincodes import pa
|
||||
assert not pa.hobbled_mode
|
||||
|
||||
from flow import word_based_seed, is_tmp, has_se_secrets
|
||||
has_notes = bool(NoteContentBase.count())
|
||||
has_sv = bool(settings.get('seedvault', False))
|
||||
|
||||
@ -32,7 +32,7 @@ TC_WORD_WALLET = const(0x1000)
|
||||
TC_XPRV_WALLET = const(0x0800)
|
||||
TC_DELTA_MODE = const(0x0400)
|
||||
TC_REBOOT = const(0x0200)
|
||||
TC_RFU = const(0x0100)
|
||||
TC_FW_DEFINED = const(0x0100)
|
||||
# for our use, not implemented in bootrom
|
||||
TC_BLANK_WALLET = const(0x0080)
|
||||
TC_COUNTDOWN = const(0x0040) # tc_arg = minutes of delay
|
||||
@ -40,6 +40,10 @@ TC_COUNTDOWN = const(0x0040) # tc_arg = minutes of delay
|
||||
# tc_args encoding:
|
||||
# TC_WORD_WALLET -> BIP-85 index, 1001..1003 for 24 words, 2001..2003 for 12-words
|
||||
|
||||
# If TC_FW_DEFINED is true, then we can do anything with this PIN at the firmware
|
||||
# level. First application is to unlock spending stuff.
|
||||
TCA_SP_UNLOCK = const(0x0001) # spending policy unlock
|
||||
|
||||
# special "pin" used as catch-all for wrong pins
|
||||
WRONG_PIN_CODE = '!p'
|
||||
|
||||
@ -274,6 +278,10 @@ class TrickPinMgmt:
|
||||
# put them in order, with "wrong" last
|
||||
return sorted(self.tp.keys(), key=lambda i: i if (i != WRONG_PIN_CODE) else 'Z')
|
||||
|
||||
def define_unlock_pin(self, new_pin):
|
||||
# user is setting the bypass PIN for first time.
|
||||
self.update_slot(new_pin.encode(), new=True, tc_flags=TC_FW_DEFINED, tc_arg=TCA_SP_UNLOCK)
|
||||
|
||||
def was_countdown_pin(self):
|
||||
# was the trick pin just used? if so how much delay needed (or zero if not)
|
||||
from pincodes import pa
|
||||
@ -284,6 +292,30 @@ class TrickPinMgmt:
|
||||
else:
|
||||
return 0
|
||||
|
||||
def was_sp_unlock(self):
|
||||
# was a trick pin just used that enables acess to spending policy?
|
||||
# - ok if it's also a trick PIN .. a wiping bypass for example
|
||||
from pincodes import pa
|
||||
tc_flags, tc_arg = pa.get_tc_values()
|
||||
return bool(tc_flags & TC_FW_DEFINED) and (tc_arg == TCA_SP_UNLOCK)
|
||||
|
||||
def has_sp_unlock(self):
|
||||
# if spending policy defined, this PIN allows adjustment
|
||||
# - not TRICK bypass choices, like ones that wipe
|
||||
# - could be multiple, but only first returned.
|
||||
for k, (sn,flags,arg) in self.tp.items():
|
||||
if (flags == TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
|
||||
return k
|
||||
return None
|
||||
|
||||
def delete_sp_unlock_pins(self):
|
||||
# remove all bypass pins, they are done w/ feature
|
||||
for k, (sn,flags,arg) in self.tp.items():
|
||||
if (flags & TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
|
||||
self.clear_slots(range(sn, sn+1))
|
||||
self.forget_pin(k)
|
||||
|
||||
|
||||
def get_deltamode_pins(self):
|
||||
# iterate over all delta-mode PIN's defined.
|
||||
for k, (sn,flags,args) in self.tp.items():
|
||||
@ -606,6 +638,9 @@ the seed phrase, but still a somewhat riskier mode.
|
||||
For this mode only, trick PIN must be same length as true PIN and \
|
||||
differ only in final 4 positions (ignoring dash).\
|
||||
''', flags=TC_DELTA_MODE),
|
||||
StoryMenuItem('Policy Unlock', "Adds (another?) Spending Policy unlock PIN.", flags=TC_FW_DEFINED, arg=TCA_SP_UNLOCK),
|
||||
StoryMenuItem('Policy Unlock & Wipe' if version.has_qwerty else 'P.U. & Wipe',
|
||||
"Pretends correct Spending Policy unlock PIN given, but silently wipes seed before asking for main PIN.", flags=TC_FW_DEFINED|TC_WIPE, arg=TCA_SP_UNLOCK),
|
||||
]
|
||||
m = MenuSystem(FirstMenu)
|
||||
m.goto_idx(1)
|
||||
@ -651,9 +686,14 @@ setting) the Coldcard will always brick after 13 failed PIN attempts.''')
|
||||
the_ux.push(m)
|
||||
|
||||
async def clear_all(self, m,l,item):
|
||||
|
||||
if not await ux_confirm("Remove ALL TRICK PIN codes and special wrong-pin handling?"):
|
||||
return
|
||||
|
||||
if tp.has_sp_unlock():
|
||||
if not await ux_confirm("You will not be able to bypass spending policy anymore."):
|
||||
return
|
||||
|
||||
if any(tp.get_duress_pins()):
|
||||
if not await ux_confirm("Any funds on the duress wallet(s) have been moved already?"):
|
||||
return
|
||||
@ -662,7 +702,7 @@ setting) the Coldcard will always brick after 13 failed PIN attempts.''')
|
||||
m.update_contents()
|
||||
|
||||
async def hide_pin(self, m,l, item):
|
||||
pin, slot_num, flags = item.arg
|
||||
pin, slot_num, flags, arg = item.arg
|
||||
|
||||
if flags & TC_DELTA_MODE:
|
||||
await ux_show_story('''Delta mode PIN will be hidden if trick PIN menu is shown \
|
||||
@ -670,12 +710,14 @@ to attacker, and we need to update this record if the main PIN is changed, so we
|
||||
hiding this item.''')
|
||||
return
|
||||
|
||||
if pin != WRONG_PIN_CODE:
|
||||
if (flags == TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
|
||||
msg = "It will still be possible to change or disable the spending policy if this PIN if known."
|
||||
elif pin == WRONG_PIN_CODE:
|
||||
msg = "This will hide what happens with wrong PINs from the menus but it will still be in effect."
|
||||
else:
|
||||
msg = '''This will hide the PIN from the menus but it will still be in effect.
|
||||
|
||||
You can restore it by trying to re-add the same PIN (%s) again later.''' % pin
|
||||
else:
|
||||
msg = "This will hide what happens with wrong PINs from the menus but it will still be in effect."
|
||||
|
||||
if not await ux_confirm(msg): return
|
||||
|
||||
@ -715,12 +757,16 @@ You can restore it by trying to re-add the same PIN (%s) again later.''' % pin
|
||||
await ux_show_story("Failed: %s" % exc)
|
||||
|
||||
async def delete_pin(self, m,l, item):
|
||||
pin, slot_num, flags = item.arg
|
||||
pin, slot_num, flags, arg = item.arg
|
||||
|
||||
if flags & (TC_WORD_WALLET | TC_XPRV_WALLET):
|
||||
if not await ux_confirm("Any funds on this duress wallet have been moved already?"):
|
||||
return
|
||||
|
||||
if (flags == TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
|
||||
if not await ux_confirm("Changes to the spending policy will not be possible anymore."):
|
||||
return
|
||||
|
||||
if pin == WRONG_PIN_CODE:
|
||||
msg = "Remove special handling of wrong PINs?"
|
||||
else:
|
||||
@ -748,8 +794,7 @@ You can restore it by trying to re-add the same PIN (%s) again later.''' % pin
|
||||
|
||||
ch = await ux_show_story('''\
|
||||
This will temporarily load the secrets associated with this trick wallet \
|
||||
so you may perform transactions with it. Reboot the Coldcard to restore \
|
||||
normal operation.''')
|
||||
so you may perform transactions with it.''')
|
||||
if ch != 'y': return
|
||||
|
||||
b, slot = tp.get_by_pin(pin)
|
||||
@ -882,6 +927,8 @@ Wallet is XPRV-based and derived from a fixed path.''' % pin
|
||||
rv.append(MenuItem("↳Pretends Wrong"))
|
||||
elif flags & TC_DELTA_MODE:
|
||||
rv.append(MenuItem("↳Delta Mode"))
|
||||
elif (flags & TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
|
||||
rv.append(MenuItem("↳Unlock Policy")) # width issues on Mk4
|
||||
|
||||
for m, msg in [
|
||||
(TC_WIPE, '↳Wipes seed'),
|
||||
@ -895,8 +942,8 @@ Wallet is XPRV-based and derived from a fixed path.''' % pin
|
||||
rv.append(MenuItem("Activate Wallet", f=self.activate_wallet, arg=(pin, flags, arg)))
|
||||
|
||||
rv.extend([
|
||||
MenuItem('Hide Trick', f=self.hide_pin, arg=(pin, slot_num, flags)),
|
||||
MenuItem('Delete Trick', f=self.delete_pin, arg=(pin, slot_num, flags)),
|
||||
MenuItem('Hide Trick', f=self.hide_pin, arg=(pin, slot_num, flags, arg)),
|
||||
MenuItem('Delete Trick', f=self.delete_pin, arg=(pin, slot_num, flags, arg)),
|
||||
])
|
||||
if pin != WRONG_PIN_CODE:
|
||||
rv.append(
|
||||
@ -907,6 +954,7 @@ Wallet is XPRV-based and derived from a fixed path.''' % pin
|
||||
|
||||
class StoryMenuItem(MenuItem):
|
||||
def __init__(self, label, story, flags=0, **kws):
|
||||
# arg= .. handled by super
|
||||
self.story = story
|
||||
self.flags = flags
|
||||
super().__init__(label, **kws)
|
||||
|
||||
@ -429,6 +429,8 @@ def clean_shutdown(style=0):
|
||||
# wipe SPI flash and shutdown (wiping main memory)
|
||||
# - mk4: SPI flash not used, but NFC may hold data (PSRAM cleared by bootrom)
|
||||
# - bootrom wipes every byte of SRAM, so no need to repeat here
|
||||
# - style=2 => reboot and try login again
|
||||
# - default is logout and (if applicable) power down.
|
||||
import callgate
|
||||
|
||||
# save if anything pending
|
||||
|
||||
@ -553,6 +553,16 @@ def ux_draw_words(y, num_words, words):
|
||||
# Draw seed words on single screen (hard) and return x/y position of start of each
|
||||
from glob import dis
|
||||
|
||||
if num_words == 2:
|
||||
# simple version for first & last words, used only during login to spending policy
|
||||
X = 14
|
||||
Y = y+1
|
||||
dis.text(X-7, Y, 'FIRST: %s' % words[0])
|
||||
dis.text(X-4, Y+1, '⋯')
|
||||
dis.text(X-6, Y+2, 'LAST: %s' % words[-1])
|
||||
|
||||
return [ (X, Y), (X, Y+2) ]
|
||||
|
||||
if num_words == 12:
|
||||
cols = 2
|
||||
xpos = [2, 18]
|
||||
@ -902,6 +912,8 @@ class QRScannerInteraction:
|
||||
async def scan_anything(self, expect_secret=False, tmp=False):
|
||||
# start a QR scan, and act on what we find, whatever it may be.
|
||||
from ux import ux_show_story
|
||||
from pincodes import pa
|
||||
|
||||
problem = None
|
||||
while 1:
|
||||
prompt = 'Scan any QR code, or CANCEL' if not expect_secret else \
|
||||
@ -923,6 +935,21 @@ class QRScannerInteraction:
|
||||
problem = "Unable to decode QR"
|
||||
continue
|
||||
|
||||
if pa.hobbled_mode:
|
||||
# block most imports in hobbled mode.
|
||||
# - specific checks in place for teleport (PSBT is okay)
|
||||
from ccc import sssp_spending_policy
|
||||
whitelist = {'psbt', 'addr', 'vmsg', 'text', 'xpub', 'teleport' }
|
||||
|
||||
sv_ok = sssp_spending_policy('okeys')
|
||||
if sv_ok:
|
||||
# seed vault, and tmp seeds are okay with user, even in hobble mode
|
||||
whitelist.update({'xprv', 'words'})
|
||||
|
||||
if what not in whitelist:
|
||||
await ux_show_story("Blocked when Spending Policy is in force", title='Sorry')
|
||||
return
|
||||
|
||||
if what == 'xprv':
|
||||
from actions import import_extended_key_as_secret
|
||||
text_xprv, = vals
|
||||
@ -1104,6 +1131,7 @@ async def ux_visualize_bip21(proto, addr, args):
|
||||
await OWNERSHIP.search_ux(addr)
|
||||
|
||||
async def ux_visualize_wif(wif_str, kp, compressed, testnet):
|
||||
# TODO: remove until we support signing w/ WIF keys IMHO
|
||||
from ux import ux_show_story
|
||||
msg = wif_str + "\n\n"
|
||||
msg += "chain: %s\n\n" % ("XTN" if testnet else "BTC")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user