firmware/shared/ccc.py
2025-03-28 09:23:48 -04:00

870 lines
30 KiB
Python

# (c) Copyright 2024 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# ccc.py - ColdCard Co-sign feature. Be a leg in a 2-of-3 that is signed based on a policy.
#
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
from glob import settings, dis
from ux import ux_confirm, ux_show_story, the_ux, OK, ux_dramatic_pause, ux_enter_number, ux_aborted
from menu import MenuSystem, MenuItem, start_chooser
from seed import seed_words_to_encoded_secret
from stash import SecretStash
from charcodes import KEY_QR, KEY_CANCEL, KEY_NFC
from exceptions import CCCPolicyViolationError
# limit to number of addresses in list
MAX_WHITELIST = const(25)
class CCCFeature:
# we don't show the user the reason for policy fail (by design, so attacker
# cannot maximize their take against the policy), but during setup/experiments
# we offer to show the reason in the menu
last_fail_reason = ""
@classmethod
def is_enabled(cls):
# Is the feature enabled right now?
return bool(settings.get('ccc', False))
@classmethod
def words_check(cls, words):
# Test if words provided are right
enc = seed_words_to_encoded_secret(words)
exp = cls.get_encoded_secret()
return enc == exp
@classmethod
def get_num_words(cls):
# return 12 or 24
return SecretStash.is_words(cls.get_encoded_secret())
@classmethod
def get_encoded_secret(cls):
# Gets the key C as encoded binary secret, compatible w/
# encodings used in stash.
return deserialize_secret(settings.get('ccc')['secret'])
@classmethod
def get_xfp(cls):
# Just the XFP value for our key C
ccc = settings.get('ccc')
return ccc['c_xfp'] if ccc else None
@classmethod
def get_master_xpub(cls):
ccc = settings.get('ccc')
return ccc['c_xpub'] if ccc else None
@classmethod
def init_setup(cls, words):
# Encode 12 or 24 words into the secret to held as key C.
# - also capture XFP and XPUB for key C
# TODO: move to "storage locker"?
assert len(words) in (12, 24)
enc = seed_words_to_encoded_secret(words)
_,_,node = SecretStash.decode(enc)
chain = chains.current_chain()
xfp = swab32(node.my_fp())
xpub = chain.serialize_public(node) # fully useless value tho
# NOTE: b_xfp and b_xpub still needed, but that's another step, not yet.
v = dict(secret=SecretStash.storage_serialize(enc),
c_xfp=xfp, c_xpub=xpub,
pol=CCCFeature.default_policy())
settings.put('ccc', v)
settings.save()
@classmethod
def default_policy(cls):
# a very basic and permissive policy, but non-zero too.
# - 1BTC per day
chain = chains.current_chain()
return dict(mag=1, vel=144, block_h=chain.ccc_min_block, web2fa='', addrs=[])
@classmethod
def get_policy(cls):
# de-serialize just the spending policy
return dict(settings.get('ccc', dict(pol={})).get('pol'))
@classmethod
def update_policy(cls, pol):
# serialize the spending policy, save it
v = dict(settings.get('ccc', {}))
v['pol'] = dict(pol)
settings.set('ccc', v)
return v['pol']
@classmethod
def update_policy_key(cls, **kws):
# update a few elements of the spending policy
# - all settings "saved" as they are changed.
# - return updated policy
p = cls.get_policy()
p.update(kws)
return cls.update_policy(p)
@classmethod
def remove_ccc(cls):
# delete our settings complete; lose key C .. already confirmed
# - leave MS in place
settings.remove_key('ccc')
settings.save()
@classmethod
def meets_policy(cls, psbt):
# Does policy allow signing this? Else raise why
pol = cls.get_policy()
# not safe to sign any txn w/ warnings: might be complaining about
# massive miner fees, or weird OP_RETURN stuff
if psbt.warnings:
raise CCCPolicyViolationError("has warnings")
# Magnitude: size limits for output side (non change)
magnitude = pol.get("mag", None)
if magnitude is not None:
if magnitude < 1000:
# it is a BTC, convert to sats
magnitude = magnitude * 100000000
outgoing = psbt.total_value_out - psbt.total_change_value
if outgoing > magnitude:
raise CCCPolicyViolationError("magnitude")
# Velocity: if zero => no velocity checks
velocity = pol.get("vel", None)
if velocity:
if not psbt.lock_time:
raise CCCPolicyViolationError("no nLockTime")
if psbt.lock_time >= NLOCK_IS_TIME:
# this is unix timestamp - not allowed - fail
raise CCCPolicyViolationError("nLockTime not height")
block_h = pol.get("block_h", chains.current_chain().ccc_min_block)
if psbt.lock_time <= block_h:
raise CCCPolicyViolationError("rewound")
# we won't sign txn unless old height + velocity >= new height
if psbt.lock_time < (block_h + velocity):
raise CCCPolicyViolationError("velocity")
# Whitelist of outputs addresses
wl = pol.get("addrs", None)
if wl:
c = chains.current_chain()
wl = set(wl)
for idx, txo in psbt.output_iter():
out = psbt.outputs[idx]
if not out.is_change: # ignore change
addr = c.render_address(txo.scriptPubKey)
if addr not in wl:
raise CCCPolicyViolationError("whitelist")
# Web 2FA
# - slow, requires UX, and they might not acheive it...
# - wait until about to do signature
if pol.get('web2fa', False):
psbt.warnings.append(('CCC', 'Web 2FA required.'))
return True
@classmethod
def could_sign(cls, psbt):
# We are looking at a PSBT: can we sign it, and would we?
# - if we **could** but will not, due to policy, add warning msg
# - return (we could sign, needs 2fa step)
if not cls.is_enabled:
return False, False
ms = psbt.active_multisig
if not ms:
# single-sig CCC not supported
return False, False
# TODO: if key B has already signed the PSBT, and so we don't need key C,
# don't try to sign; maybe show warning?
xfp = cls.get_xfp()
if xfp not in ms.xfp_paths:
# does not involve us
return False, False
try:
# check policy
needs_2fa = cls.meets_policy(psbt)
except CCCPolicyViolationError as e:
cls.last_fail_reason = str(e)
psbt.warnings.append(('CCC', "Violates spending policy. Won't sign."))
return False, False
return True, needs_2fa
@classmethod
async def web2fa_challenge(cls):
# they are trying to sign something, so make them get out their phone
# - at this point they have already ok'ed the details of the txn
# - and we have approved other elements of the spending policy.
# - could show MS wallet name, or txn details but will not because that is
# an info leak to Coinkite... and we just don't want to know.
pol = cls.get_policy()
ok = await web2fa.perform_web2fa('Approve CCC Transaction', pol.get('web2fa'))
if not ok:
cls.last_fail_reason = '2FA Fail'
raise CCCPolicyViolationError
@classmethod
def sign_psbt(cls, psbt):
# do the math
psbt.sign_it(cls.get_encoded_secret(), cls.get_xfp())
cls.last_fail_reason = ""
old_h = cls.get_policy().get('block_h', 1)
if old_h < psbt.lock_time < NLOCK_IS_TIME:
# always update last block height, even if velocity isn't enabled yet
# - attacker might have changed to testnet, but there is no
# reason to ever lower block height. strictly ascending
cls.update_policy_key(block_h=psbt.lock_time)
settings.save()
def render_mag_value(mag):
# handle integer bitcoins, and satoshis in same value
if mag < 1000:
return '%d BTC' % mag
else:
return '%d SATS' % mag
class CCCConfigMenu(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('CCC [%s]' % xfp2str(my_xfp), f=self.show_ident),
MenuItem('Spending Policy', menu=CCCPolicyMenu.be_a_submenu),
MenuItem('Export CCC XPUBs', f=self.export_xpub_c),
MenuItem('Multisig Wallets'),
]
# look for wallets that are defined related to CCC feature, shortcut to them
count = 0
for ms in MultisigWallet.get_all():
if my_xfp in ms.xfp_paths:
items.append(MenuItem('%d/%d: %s' % (ms.M, ms.N, ms.name),
menu=make_ms_wallet_menu, arg=ms.storage_idx))
count += 1
items.append(MenuItem('↳ Build 2-of-N', f=self.build_2ofN, arg=count))
if CCCFeature.last_fail_reason:
# xxxxxxxxxxxxxxxx
items.insert(1, MenuItem('Last Violation', f=self.debug_last_fail))
items.append(MenuItem('Load Key C', f=self.enter_temp_mode))
items.append(MenuItem('Remove CCC', f=self.remove_ccc))
return items
async def debug_last_fail(self, *a):
# debug for customers: why did we reject that last txn?
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_ccc(self, *a):
# disable and remove feature
if not await ux_confirm('Key C will be lost, and policy settings forgotten.'
' This unit will only be able to partly sign transactions.'
' To completely remove this wallet, proceed to the multisig'
' menu and remove related wallet entries.'):
return
if not await ux_confirm("Funds in related wallet/s may be impacted.", confirm_key='4'):
return await ux_aborted()
CCCFeature.remove_ccc()
the_ux.pop()
async def on_cancel(self):
# trying to exit from CCCConfigMenu
from seed import in_seed_vault
enc = CCCFeature.get_encoded_secret()
if in_seed_vault(enc):
# remind them to clear the seed-vault copy of Key C because it defeats feature
await ux_show_story("Key C is in your Seed Vault. If you are done with setup, "
"you MUST delete it from the Vault!", title='REMINDER')
the_ux.pop()
async def export_xpub_c(self, *a):
# do standard Coldcard export for multisig setups
xfp = CCCFeature.get_xfp()
enc = CCCFeature.get_encoded_secret()
from multisig import export_multisig_xpubs
await export_multisig_xpubs(xfp=xfp, alt_secret=enc, skip_prompt=True)
async def build_2ofN(self, m, l, i):
count = i.arg
# ask for a key B, assume A and C are defined => export MS config and import into self.
# - like the airgap setup, but assume A and C are this Coldcard
m = '''Builds simple 2-of-N multisig wallet, with this Coldcard's main secret (key A), \
the CCC policy-controlled key C, and at least one other device, as key B. \
\nYou will need to export the XPUB from another Coldcard and place it on an SD Card, or \
be ready to show it as a QR, before proceeding.'''
if await ux_show_story(m) != 'y':
return
from multisig import create_ms_step1
# picks addr fmt, QR or not, gets at least one file, then...
await create_ms_step1(for_ccc=(CCCFeature.get_encoded_secret(), count))
# prompt for file, prompt for our acct number, unless already exported to this card?
async def show_ident(self, *a):
# give some background? or just KISS for now?
xfp = xfp2str(CCCFeature.get_xfp())
xpub = CCCFeature.get_master_xpub()
await ux_show_story(
"Key C:\n\n"
"XFP (Master Fingerprint):\n\n %s\n\n"
"Master Extended Public Key:\n\n %s " % (xfp, xpub))
async def enter_temp_mode(self, *a):
# apply key C as temp seed, so you can do anything with it
# - just a shortcut, since they have the words, and could enter them
# - one-way trip because the CCC feature won't be enabled inside the temp seed settings
if await ux_show_story(
'Loads the CCC controlled seed (key C) as a Temporary Seed and allows '
'easy use of all Coldcard features on that key.\n\nIf you save into Seed Vault, '
'access to CCC Config menu is quick and easy.') != 'y':
return
from seed import set_ephemeral_seed
from actions import goto_top_menu
enc = CCCFeature.get_encoded_secret()
await set_ephemeral_seed(enc, origin='Key C from CCC')
goto_top_menu()
class PolCheckedMenuItem(MenuItem):
# Show a checkmark if **policy** setting is defined and not the default
# - only works inside CCCPolicyMenu
def __init__(self, label, polkey, **kws):
super().__init__(label, **kws)
self.polkey = polkey
def is_chosen(self):
# should we show a check in parent menu? check the policy
m = the_ux.top_of_stack()
#assert isinstance(m, CCCPolicyMenu)
return bool(m.policy.get(self.polkey, False))
class CCCAddrWhitelist(MenuSystem):
# simulator arg: --seq tcENTERENTERsENTERwENTER
def __init__(self):
items = self.construct()
super().__init__(items)
def update_contents(self):
tmp = self.construct()
self.replace_items(tmp)
@classmethod
async def be_a_submenu(cls, *a):
return cls()
def construct(self):
# list of addresses
addrs = CCCFeature.get_policy().get('addrs', [])
maxxed = (len(addrs) >= MAX_WHITELIST)
items = [MenuItem(truncate_address(a), f=self.edit_addr, arg=a) for a in addrs]
if not items:
items.append(MenuItem("(none yet)"))
if version.has_qr:
items.append(MenuItem('Scan QR', f=(self.maxed_out if maxxed else self.scan_qr),
shortcut=KEY_QR))
items.append(MenuItem('Import from File',
f=(self.maxed_out if maxxed else self.import_file)))
return items
async def edit_addr(self, menu, idx, item):
# show detail and offer delete
addr = item.arg
msg = ('Spends to this address will be permitted:\n\n%s'
'\n\nPress (4) to delete.' % show_single_address(addr))
ch = await ux_show_story(msg, escape='4')
if ch == '4':
self.delete_addr(addr)
def delete_addr(self, addr):
# no confirm, stakes are low
addrs = CCCFeature.get_policy().get('addrs', [])
addrs.remove(addr)
CCCFeature.update_policy_key(addrs=addrs)
self.update_contents()
async def import_file(self, *a):
# Import from a file, or NFC.
# - simulator: --seq tcENTERENTERsENTERwENTERiENTER1
# - very forgiving, does not care about file format
# - but also silent on all errors
from ux import import_export_prompt
from glob import NFC
from actions import file_picker
from files import CardSlot
from utils import cleanup_payment_address
choice = await import_export_prompt("List of addresses", is_import=True, no_qr=True)
if choice == KEY_CANCEL:
return
elif choice == KEY_NFC:
addr = await NFC.read_address()
if not addr:
# error already displayed in nfc.py
return
await self.add_addresses([addr])
return
# loose RE to match any group of chars that could be addresses
# - really just removing whitespace and punctuation
# - lacking re.findall(), so using re.split() on negatives
pat = re.compile(r'[^A-Za-z0-9]')
# pick a likely-looking file: just looking at size and extension
fn = await file_picker(suffix=['csv', 'txt'],
min_size=20, max_size=20000,
none_msg="Must contain payment addresses", **choice)
if not fn: return
results = []
with CardSlot(readonly=True, **choice) as card:
with open(fn, 'rt') as fd:
for ln in fd.readlines():
for here in pat.split(ln):
if len(here) >= 4:
try:
addr = cleanup_payment_address(here)
results.append(addr)
except: pass
if not results:
await ux_show_story("Unable to find any payment addresses in that file.")
else:
# silently limit to first 25 results; lets them use addresses.csv easily
await self.add_addresses(results[:MAX_WHITELIST])
async def scan_qr(self, *a):
# Scan and return a text string. For things like BIP-39 passphrase
# and perhaps they are re-using a QR from something else. Don't act on contents.
from ux_q1 import QRScannerInteraction
q = QRScannerInteraction()
got = []
ln = ''
while 1:
here = await q.scan_for_addresses("Bitcoin Address(es) to Whitelist", line2=ln)
if not here: break
got.extend(here)
ln = 'Got %d so far. ENTER to apply.' % len(got)
if got:
# import them
await self.add_addresses(got)
async def maxed_out(self, *a):
await ux_show_story("Max %d items in whitelist. Please make room first." % MAX_WHITELIST)
async def add_addresses(self, more_addrs):
# add new entries, if unique; preserve ordering
addrs = CCCFeature.get_policy().get('addrs', [])
new = []
for a in more_addrs:
if a not in addrs:
addrs.append(a)
new.append(a)
if not new:
await ux_show_story("Already in whitelist:\n\n" +
'\n\n'.join(show_single_address(a) for a in more_addrs))
return
if len(addrs) > MAX_WHITELIST:
return await self.maxed_out()
CCCFeature.update_policy_key(addrs=addrs)
self.update_contents()
if len(new) > 1:
await ux_show_story("Added %d new addresses to whitelist:\n\n%s" %
(len(new), '\n\n'.join(show_single_address(a) for a in new)))
else:
await ux_show_story("Added new address to whitelist:\n\n%s" % show_single_address(new[0]))
class CCCPolicyMenu(MenuSystem):
# Build menu stack that allows edit of all features of the spending
# policy. Key C is set already at this point.
# - and delete/cancel CCC (clears setting?)
# - be a sticky menu that's hard to exit (ie. SAVE choice and no cancel out)
def __init__(self):
self.policy = CCCFeature.get_policy()
items = self.construct()
super().__init__(items)
def update_contents(self):
tmp = self.construct()
self.replace_items(tmp)
@classmethod
async def be_a_submenu(cls, *a):
return cls()
def construct(self):
items = [
# xxxxxxxxxxxxxxxx
PolCheckedMenuItem('Max Magnitude', 'mag', f=self.set_magnitude),
PolCheckedMenuItem('Limit Velocity', 'vel', f=self.set_velocity),
PolCheckedMenuItem('Whitelist' + (' Addresses' if version.has_qr else ''),
'addrs', menu=CCCAddrWhitelist.be_a_submenu),
PolCheckedMenuItem('Web 2FA', 'web2fa', f=self.toggle_2fa),
]
if self.policy.get('web2fa'):
items.extend([
MenuItem('↳ Test 2FA', f=self.test_2fa),
MenuItem('↳ Enroll More', f=self.enroll_more_2fa),
])
return items
async def test_2fa(self, *a):
ss = self.policy.get('web2fa')
assert ss
ok = await web2fa.perform_web2fa('CCC Test', ss)
await ux_show_story('Correct code was given.' if ok else 'Failed or aborted.')
async def enroll_more_2fa(self, *a):
# let more phones in on the party
ss = self.policy.get('web2fa')
assert ss
await web2fa.web2fa_enroll('CCC', ss)
async def set_magnitude(self, *a):
# Looks decent on both Q and Mk4...
was = self.policy.get('mag', 0)
val = await ux_enter_number('Transaction Max:', max_value=int(1e8),
can_cancel=True, value=(was or ''))
args = dict(mag=val)
if (val is None) or (val == was):
msg = "Did not change"
val = was
else:
msg = "You have set the"
unchanged = False
if not val:
msg = "No check for maximum transaction size will be done. "
if self.policy.get('vel', 0):
msg += 'Velocity check also disabled. '
args['vel'] = 0
else:
msg += " maximum per-transaction: \n\n %s" % render_mag_value(val)
self.policy = CCCFeature.update_policy_key(**args)
await ux_show_story(msg, title="Txn Magnitude")
async def set_velocity(self, *a):
mag = self.policy.get('mag', 0) or 0
if not mag:
msg = 'Velocity limit requires a per-transaction magnitude to be set.'\
' This has been set to 1BTC as a starting value.'
self.policy = CCCFeature.update_policy_key(mag=1)
await ux_show_story(msg)
start_chooser(self.velocity_chooser)
def velocity_chooser(self):
# offer some useful values from a menu
vel = self.policy.get('vel', 0) # in blocks
# reminder: dont forget the poor Mk4 users
# xxxxxxxxxxxxxxxx
ch = [ 'Unlimited',
'6 blocks (hour)',
'24 blocks (4h)',
'48 blocks (8h)',
'72 blocks (12h)',
'144 blocks (day)',
'288 blocks (2d)',
'432 blocks (3d)',
'720 blocks (5d)',
'1008 blocks (1w)',
'2016 blocks (2w)',
'3024 blocks (3w)',
'4032 blocks (4w)',
]
va = [0] + [int(x.split()[0]) for x in ch[1:]]
try:
which = va.index(vel)
except ValueError:
which = 0
def set(idx, text):
self.policy = CCCFeature.update_policy_key(vel=va[idx])
return which, ch, set
async def toggle_2fa(self, *a):
if self.policy.get('web2fa'):
# enabled already
if not await ux_confirm("Disable web 2FA check? Effect is immediate."):
return
self.policy = CCCFeature.update_policy_key(web2fa='')
self.update_contents()
await ux_show_story("Web 2FA has been disabled. If you re-enable it, a new "
"secret will be generated, so it is safe to remove it from your "
"phone at this point.")
return
ch = await ux_show_story('''When enabled, any spend (signing) requires \
use of mobile 2FA application (TOTP RFC-6238). Shared-secret is picked now, \
and loaded on your phone via QR code.
WARNING: You will not be able to sign transactions if you do not have an NFC-enabled \
phone with Internet access and 2FA app holding correct shared-secret.''',
title="Web 2FA")
if ch != 'y':
return
# challenge them, and don't set unless it works
ss = await web2fa.web2fa_enroll('CCC')
if not ss:
return
# update state
self.policy = CCCFeature.update_policy_key(web2fa=ss)
self.update_contents()
async def gen_or_import():
# returns 12 words, or None to abort
from seed import WordNestMenu, generate_seed, approve_word_list, SeedVaultChooserMenu
msg = "Press %s to generate a new 12-word seed phrase to be used "\
"as the Coldcard Co-Signing Secret (key C).\n\nOr press (1) to import existing "\
"12-words or (2) for 24-words import." % OK
if settings.master_get("seedvault", False):
msg += ' Press (6) to import from Seed Vault.'
ch = await ux_show_story(msg, escape='126', title="CCC Key C")
if ch in '12':
nwords = 24 if ch == '2' else 12
async def done_key_C_import(words):
if not version.has_qwerty:
WordNestMenu.pop_all()
await enable_step1(words)
if version.has_qwerty:
from ux_q1 import seed_word_entry
await seed_word_entry('Key C Seed Words', nwords, done_cb=done_key_C_import)
else:
nxt = WordNestMenu(nwords, done_cb=done_key_C_import)
the_ux.push(nxt)
return None # will call parent again
elif ch == '6':
# pick existing from Seed Vault
picked = await SeedVaultChooserMenu.pick(words_only=True)
if picked:
words = SecretStash.decode_words(deserialize_secret(picked.encoded))
await enable_step1(words)
return None
elif ch == 'y':
# normal path: pick 12 words, quiz them
await ux_dramatic_pause('Generating...', 3)
seed = generate_seed()
words = await approve_word_list(seed, 12)
else:
return None
return words
async def toggle_ccc_feature(*a):
# The only menu item show to user!
if settings.get('ccc'):
return await modify_ccc_settings()
# enable the feature -- not simple!
# - create C key (maybe import?)
# - collect a policy setup, maybe 2FA enrol too
# - lock that down
# - TODO copy
ch = await ux_show_story('''\
Adds an additional seed to your Coldcard, and enforces a "spending policy" whenever \
it signs with that key. Spending policies can restrict: magnitude (BTC out), \
velocity (blocks between txn), address whitelisting, and/or require confirmation by 2FA phone app.
Assuming the use of a 2-of-3 multisig wallet, keys are as follows:\n
A=Coldcard (master seed), B=Backup Key (offline/recovery), C=Spending Policy Key.
Spending policy cannot be viewed or changed without knowledge of key C.\
''',
title="Coldcard Co-Signing" if version.has_qwerty else 'CC Co-Sign')
if ch != 'y':
# just a tourist
return
await enable_step1(None)
async def enable_step1(words):
if not words:
words = await gen_or_import()
if not words: return
dis.fullscreen("Wait...")
dis.busy_bar(True)
try:
# do BIP-32 basics: capture XFP and XPUB and encoded version of the secret
CCCFeature.init_setup(words)
finally:
dis.busy_bar(False)
# continue into config menu
m = CCCConfigMenu()
the_ux.push(m)
async def modify_ccc_settings():
# Generally not expecting changes to policy on the fly because
# that's the whole point. Use the B key to override individual spends
# but if you can prove you have C key, then it's harmless to allow changes
# since you could just spend as needed.
enc = CCCFeature.get_encoded_secret()
bypass = False
from seed import in_seed_vault
if in_seed_vault(enc):
# If seed vault enabled and they have the key C in there already, just go
# directly into menu (super helpful for debug/setup/testing time). We do warn tho.
await ux_show_story('''You have a copy of the CCC key C in the Seed Vault, so \
you may proceed to change settings now.\n\nYou must delete that key from the vault once \
setup and debug is finished, or all benefit of this feature is lost!''')
bypass = True
else:
ch = await ux_show_story(
"Spending policy cannot be viewed, changed nor disabled, "
"unless you have the seed words for key C.",
title="CCC Enabled", escape='6')
if ch == '6' and version.is_devmode:
# debug hack: skip word entry
bypass = True
elif ch != 'y': return
if bypass:
# doing full decode cycle here for better testing
chk, raw, _ = SecretStash.decode(enc)
assert chk == 'words'
words = bip39.b2a_words(raw).split(' ')
await key_c_challenge(words)
return
# small info-leak here: exposing 12 vs 24 words, but we expect most to be 12 anyway
nwords = CCCFeature.get_num_words()
import seed
if version.has_qwerty:
from ux_q1 import seed_word_entry
await seed_word_entry('Enter Seed Words', nwords, done_cb=key_c_challenge)
else:
return seed.WordNestMenu(nwords, done_cb=key_c_challenge)
NUM_CHALLENGE_FAILS = 0
async def key_c_challenge(words):
# They entered some words, if they match our key C then allow edit of policy
if not version.has_qwerty:
from seed import WordNestMenu
WordNestMenu.pop_all()
dis.fullscreen('Verifying...')
if not CCCFeature.words_check(words):
# keep an in-memory counter, and after 3 fails, reboot
global NUM_CHALLENGE_FAILS
NUM_CHALLENGE_FAILS += 1
if NUM_CHALLENGE_FAILS >= 3:
from utils import clean_shutdown
clean_shutdown()
await ux_show_story("Sorry, those words are incorrect.")
return
# success. they are in.
# got to config menu
m = CCCConfigMenu()
the_ux.push(m)
# EOF