# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# trick_pins.py - manage the "trick" PIN codes, which can do anything but let you in!
#
# - mk4+ only
# - uses SE2 to store PIN codes (hashed) and what actions to perform for each
# - replaces old "duress wallet" and "brickme" features 
# - changes require knowledge of real PIN code (it is checked)
# 
import version, uctypes, errno, ngu, sys, ckcc, stash, bip39
from ubinascii import hexlify as b2a_hex
from menu import MenuSystem, MenuItem
from ux import ux_show_story, ux_confirm, ux_dramatic_pause, ux_enter_number, the_ux, ux_aborted
from stash import SecretStash
from drv_entro import bip85_derive

# see from mk4-bootloader/se2.h
NUM_TRICKS      = const(14)
TRICK_SLOT_LAYOUT = {
    "slot_num": 0 | uctypes.INT32,
    "tc_flags": 4 | uctypes.UINT16,
    "tc_arg": 6 | uctypes.UINT16,
    "xdata": (8 | uctypes.ARRAY, 64 | uctypes.UINT8),
    "pin": (8+64 | uctypes.ARRAY, 16 | uctypes.UINT8),
    "pin_len": (8+64+16) | uctypes.INT32,
    "blank_slots": (8+64+16+4) | uctypes.UINT32,
    "spare": ((8+64+16+4+4) | uctypes.ARRAY, 8|uctypes.INT32),
}
TC_WIPE         = const(0x8000)
TC_BRICK        = const(0x4000)
TC_FAKE_OUT     = const(0x2000)
TC_WORD_WALLET  = const(0x1000)
TC_XPRV_WALLET  = const(0x0800)
TC_DELTA_MODE   = const(0x0400)
TC_REBOOT       = const(0x0200)
TC_RFU          = const(0x0100)
# for our use, not implemented in bootrom
TC_BLANK_WALLET = const(0x0080)
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

# special "pin" used as catch-all for wrong pins
WRONG_PIN_CODE = '!p'
    
def validate_delta_pin(true_pin, proposed_delta_pin):
    # Check delta pin proposal works w/ limitations and
    # provide error msg, and/or calc required tc_arg value.
    right = true_pin.replace('-', '')
    fake = proposed_delta_pin.replace('-', '')

    if (len(right) != len(fake)) or (right[0:-4] != fake[0:-4]):
        prob = '''\
Trick PIN must be same length (%d) as true PIN and \
up to last four digits can be different between true PIN and trick.''' % len(right)
        return prob, 0

    a = 0
    for i in range(4):
        dx = -(1+i)
        if right[dx] == fake[dx]:
            # no need to reveal this digit to SE2 hacker if same
            a |= 0xf << (i*4)
        else:
            a |= (ord(right[-(1+i)]) - 0x30) << (i*4)

    return None, a

def construct_duress_secret(flags, tc_arg):
    # is duress wallet required and if so, what are the secret values (32 or 64 bytes)
    if flags & TC_WORD_WALLET:
        # derive the secret via BIP-85
        nwords = 24 if (tc_arg//1000 == 1) else 12
        mmode = 0 if (nwords == 12) else 2          # weak: based on menu design
        new_secret, _, _, path = bip85_derive(mmode, tc_arg)
        path = "BIP85(words=%d, index=%d)" % (nwords, tc_arg)

    elif flags & TC_XPRV_WALLET:
        # use old method for duress wallets
        with stash.SensitiveValues() as sv:
            node, path = sv.duress_root()
            new_secret = SecretStash.encode(xprv=node)[1:65]
            assert len(new_secret) == 64
    else:
        return (None, None)

    return path, new_secret

def make_slot():
    b = bytearray(uctypes.sizeof(TRICK_SLOT_LAYOUT))
    return b, uctypes.struct(uctypes.addressof(b), TRICK_SLOT_LAYOUT)

class TrickPinMgmt:

    def __init__(self):
        assert uctypes.sizeof(TRICK_SLOT_LAYOUT) == 128
        self.reload()

    def reload(self):
        # we track known PINS as a dictionary:
        #   pin (in ascii) => (slot_num, tc_flags, arg)
        from glob import settings
        self.tp = settings.get('tp', {})

    def save_record(self):
        # commit changes back to settings
        from glob import settings
        settings.set('tp', self.tp)
        settings.save()

    def roundtrip(self, method_num, slot_buf=None):
        from pincodes import pa

        if slot_buf is not None:
            arg = slot_buf
        else:
            # use zeros
            assert method_num == 0
            arg = bytes(uctypes.sizeof(TRICK_SLOT_LAYOUT))

        rc, data = pa.trick_request(method_num, arg)

        if slot_buf is not None:
            # overwrite request w/ result (works inplace)
            slot_buf[:] = data

        return rc

    def clear_all(self):
        # get rid of them all
        self.roundtrip(0)
        self.tp = {}
        self.save_record()

    def forget_pin(self, pin):
        # forget about settings for a PIN
        self.tp.pop(pin, None)
        self.save_record()

    def restore_pin(self, new_pin):
        # remember/restore PIN that we "forgot", return T if worked
        b, slot = tp.get_by_pin(new_pin)
        if slot is None: return False

        record = (slot.slot_num, slot.tc_flags, 
                        0xffff if slot.tc_flags & TC_DELTA_MODE else slot.tc_arg)
        self.tp[new_pin] = record
        self.save_record()

        return True

    def clear_slots(self, slot_nums):
        # remove some slots, not all
        b, slot = make_slot()
        slot.blank_slots = sum(1<<s for s in slot_nums)
        self.roundtrip(2, b)

    def get_available_slots(self):
        # do an impossible search, so we can get blank_slots field back
        b, slot = make_slot()
        slot.pin_len = 1
        self.roundtrip(1, b)        # expects ENOENT=2

        blk = slot.blank_slots

        # bug workaround: don't use slot 10, in bootrom 3.1.4 and earlier
        blk &= ~(1<<10)

        return [i for i in range(NUM_TRICKS) if (1<<i & blk)]

    def find_empty_slots(self, qty_needed):
        # locate a slot (or 3) that are available for use
        avail = self.get_available_slots()
        if qty_needed == 1:
            return avail[0] if avail else None
        else:
            for sn in avail:
                if all((sn+i in avail) for i in range(1, qty_needed)):
                    return sn
            return None

    def get_by_pin(self, pin):
        # fetch slot details based on a PIN code (which must be known already somehow)
        b, slot = make_slot()

        if isinstance(pin, str):
            pin = pin.encode()

        slot.pin_len = len(pin)
        slot.pin[0:slot.pin_len] = pin

        rc = self.roundtrip(1, b)
        if rc == errno.ENOENT:
            return None, None

        # these fields are zeros on return, but we need them for CRUD
        slot.pin_len = len(pin)
        slot.pin[0:slot.pin_len] = pin

        return b, slot

    def update_slot(self, pin, new=False, new_pin=None, tc_flags=None, tc_arg=None, secret=None):
        # create or update a trick pin
        # - doesn't support wallet to no-wallet transitions
        '''
        >>> from pincodes import pa; pa.setup(b'12-12'); pa.login(); from trick_pins import *
        '''
        assert isinstance(pin, bytes)

        b, slot = self.get_by_pin(pin)
        if not slot:
            if not new: raise KeyError("wrong pin")

            # Making a new entry
            b, slot = make_slot()
            new_pin = pin

            # pick a free slot
            sn = self.find_empty_slots(1 if not secret else 1+(len(secret)//32))
            if sn == None:
                # we are full
                raise RuntimeError("no space left")

            slot.slot_num = sn

        if new_pin is not None:
            slot.pin_len = len(new_pin)
            slot.pin[0:slot.pin_len] = new_pin
            if new_pin != pin:
                self.tp.pop(pin.decode(), None)
            pin = new_pin

        if tc_flags is not None:
            assert 0 <= tc_flags <= 65536
            slot.tc_flags = tc_flags

        if tc_arg is not None:
            assert 0 <= tc_arg <= 65536
            slot.tc_arg = tc_arg

        if secret is not None:
            # expecting an encoded secret
            if len(secret) <= 32:
                # words.
                assert slot.tc_flags & TC_WORD_WALLET
                slot.xdata[0:len(secret)] = secret
            elif len(secret) == 64:
                # expecting 64 bytes encoded already
                assert slot.tc_flags & TC_XPRV_WALLET
                slot.xdata[0:64] = secret
            else:
                raise ValueError()

        # Save config for later
        # - deltamode: don't document real pin digits
        record = (slot.slot_num, slot.tc_flags, 
                        0xffff if slot.tc_flags & TC_DELTA_MODE else slot.tc_arg)

        slot.blank_slots = 0
        rc = self.roundtrip(2, b)
        assert rc == 0

        # record key details.
        self.tp[pin.decode()] = record
        self.save_record()

        return b, slot

    def all_tricks(self):
        # put them in order, with "wrong" last
        return sorted(self.tp.keys(), key=lambda i: i if (i != WRONG_PIN_CODE) else 'Z')

    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
        tc_flags, tc_arg = pa.get_tc_values()

        if tc_flags & TC_COUNTDOWN:
            return tc_arg or 60
        else:
            return 0

    def get_deltamode_pins(self):
        # iterate over all delta-mode PIN's defined.
        for k, (sn,flags,args) in self.tp.items():
            if flags & TC_DELTA_MODE:
                yield k

    def get_duress_pins(self):
        # iterate over all duress wallets
        for k, (sn,flags,args) in self.tp.items():
            if flags & (TC_WORD_WALLET | TC_XPRV_WALLET):
                yield k

    def check_new_main_pin(self, pin):
        # user is trying to change main PIN to new value; check for issues
        # - dups bad but also: delta mode pin might not work w/ longer main true pin
        # - return error msg or None
        assert isinstance(pin, str)
        if pin in self.tp:
            return 'That PIN is already in use as a Trick PIN.'

        for d_pin in self.get_deltamode_pins():
            prob, _ = validate_delta_pin(pin, d_pin)
            if prob:
                return 'That PIN value makes problems with a Delta Mode Trick PIN.'

    def main_pin_has_changed(self, new_main_pin):
        # update any delta-mode entries we have
        for d_pin in self.get_deltamode_pins():
            prob, arg = validate_delta_pin(new_main_pin, d_pin)
            assert not prob             # see check_new_main_pin() above
            self.update_slot(d_pin.encode(), tc_arg=arg)

    def backup_duress_wallets(self, sv):
        # for backup file, yield (label, path, pairs-of-data)
        done = set()
        for pin in self.get_duress_pins():
            sn, flags, arg = self.tp[pin]

            if (flags, arg) in done:
                continue
            done.add( (flags, arg) ) 

            if flags & TC_WORD_WALLET:
                label = "Duress: BIP-85 Derived wallet"
                nwords = 12 if ((arg // 1000) == 2) else 24
                path = "BIP85(words=%d, index=%d)" % (nwords, arg)
                b, slot = tp.get_by_pin(pin)
                words = bip39.b2a_words(slot.xdata[0:(32 if nwords==24 else 16)])

                d = [ ('duress_%d_words' % arg, words) ]
            elif flags & TC_XPRV_WALLET:
                label = "Duress: XPRV Wallet"
                node, path = sv.duress_root()
                path = 'path = ' + path
                # backwards compat name, but skipping xpub this time
                d = [ ('duress_xprv', sv.chain.serialize_private(node)) ]

            yield (label, path, d)

    def restore_backup(self, vals):
        # restoring backup value
        # - need to re-populate SE2 w/ these values, including duress wallets
        # - being restored: vals=self.tp
        # - CAUTION: new true-pin may not match old true-pin; skip any that would
        #     not work w/ new pin (conflicting value, or deltamode issues)
        from pincodes import pa
        true_pin = pa.pin.decode()

        for pin in vals:
            (sn, flags, arg) = vals[pin]

            if pin == true_pin:
                # drop conflicting trick pin vs. (new) true pin
                continue

            if flags & TC_DELTA_MODE:
                prob = validate_delta_pin(true_pin, pin)
                if prob:
                    # just forget it, no UI here to report issue
                    continue           

            try:
                # might need to construct a BIP-85 or XPRV secret to match
                path, new_secret = construct_duress_secret(flags, arg)

                b, slot = tp.update_slot(pin.encode(), new=True,
                                     tc_flags=flags, tc_arg=arg, secret=new_secret)
            except Exception as exc:
                sys.print_exception(exc)        # not visible
            

tp = TrickPinMgmt()

class TrickPinMenu(MenuSystem):

    def __init__(self):
        self.WillWipeMenu = None
        super().__init__(self.construct())

    @classmethod
    async def make_menu(cls, *unused):
        # used to build menu at runtime, in response to parent menu item
        return cls()

    @property
    def current_pin(self):
        from pincodes import pa
        return pa.pin.decode()

    def construct(self):
        # Dynamic menu with PIN codes as the items, plus a few static choices

        # not going to work well if tmp secret in effect
        from pincodes import pa
        if bool(pa.tmp_value):
            return [MenuItem('Not Available')]

        tp.reload()
        tricks = tp.all_tricks()

        if self.current_pin in tricks:
            # They got into here with a trick PIN, so it must be 
            # a deltamode pin, or something else tricky ... hide it from menu
            # since it reveals that fact to attacker.
            tricks.remove(self.current_pin)

        has_wrong = False

        rv = []

        if tricks:
            rv.append(MenuItem('Trick PINs:'))
            for pin in tricks:
                if pin == WRONG_PIN_CODE:
                    rv.append(MenuItem('↳WRONG PIN', menu=self.pin_submenu, arg=pin))
                else:
                    rv.append(MenuItem('↳'+pin, menu=self.pin_submenu, arg=pin))


        rv.append(MenuItem('Add New Trick', f=self.add_new))
        has_wrong = any(pin == WRONG_PIN_CODE for pin in tricks)
        if not has_wrong:
            rv.append(MenuItem('Add If Wrong', f=self.set_any_wrong))

        # even if menu "looks" empty, many times we need this anyway
        rv.append(MenuItem('Delete All', f=self.clear_all))

        return rv

    def update_contents(self):
        tmp = self.construct()
        self.replace_items(tmp)

    async def done_picking(self, item, parents):
        # done picking/drilling down tree.
        # - shows point-form summary and gets confirmation
        wants_wipe = (self.WillWipeMenu in parents)
        self.WillWipeMenu = None        # memory free

        flags = item.flags
        tc_arg = item.arg

        if self.proposed_pin == WRONG_PIN_CODE:
            if tc_arg == 0:
                msg = "Any Wrong PIN\n↳%s" % item.label
            else:
                msg = "%d Wrong PINs\n↳%s" % (tc_arg, item.label)
        else:
            msg = "PIN %s\n↳%s" % (self.proposed_pin, item.label)

            if wants_wipe:
                msg += " (after wiping secret)"
                flags |= TC_WIPE

        msg += '\n\n'

        path, new_secret = construct_duress_secret(flags, tc_arg)


        if path:
            msg += "Duress wallet will use path:\n\n%s\n\n" % path

        if flags & TC_DELTA_MODE:
            # Calculate the value needed for args: BCD encoded final 4 digits
            # of the true PIN!
            prob, a = validate_delta_pin(self.current_pin, self.proposed_pin)
            if prob:
                await ux_show_story(prob, 'Sorry!')
                return
            tc_arg = a

        msg += "Ok?"
        ch = await ux_show_story(msg)
        if ch != 'y': return

        # save it
        try:
            bpin = self.proposed_pin.encode()
            b, slot = tp.update_slot(bpin, new=True, tc_flags=flags,
                                        tc_arg=tc_arg, secret=new_secret)
            await ux_dramatic_pause("Saved.", 1)
        except BaseException as exc:
            sys.print_exception(exc)
            await ux_show_story("Failed: %s" % exc)

        self.update_contents()


    async def get_new_pin(self, existing_pin=None):
        # get a new PIN code and check not a dup
        # - show msg if aborted
        # - recover "forgotten" pins

        from login import LoginUX
        lll = LoginUX()
        lll.is_setting = True
        lll.subtitle = "New Trick PIN"
        new_pin = await lll.prompt_pin()

        if new_pin is None:
            return

        if new_pin == existing_pin:
            await ux_show_story("That isn't a new value")
            return

        have = tp.all_tricks()
        if existing_pin and (existing_pin in have):
            have.remove(existing_pin)

        if (new_pin == self.current_pin) or (new_pin in have):
            await ux_show_story("That PIN (%s) is already in use. All PIN codes must be unique." % new_pin);
            return

        # check if we "forgot" this pin, and read it back if we did.
        # - important this is after the above checks so we don't reveal any trick pin used
        #   to get here
        if tp.restore_pin(new_pin):
            await ux_show_story("Hmm. I remember that PIN now.")
            self.update_contents()
            return

        return new_pin

    async def add_new(self, *a):
        # Add a new PIN code
        from pincodes import pa
        from glob import settings

        if pa.is_secret_blank() or pa.is_blank() or not pa.pin:
            await ux_show_story("Please set true PIN and wallet seed before creating trick pins.")
            return

        # get the new pin
        self.proposed_pin = await self.get_new_pin()
        if not self.proposed_pin: return
        nwords = settings.get('words', 24)
        if nwords == 12:
            dbase = 2000
        else:
            # 24-word typical duress wallet
            # - cannot handle 18-word seeds exactly, so map to 24
            # - also XPRV -> duress word wallet will be 24-word type
            dbase = 1000

        b85 = "This PIN will lead to a functional 'duress' wallet using seed words produced by the standard BIP-85 process. Index number is %d...%d for #1..#3 duress wallets. Same number of seed words as your true seed." \
                % (dbase+1, dbase+3)

        DuressOptions = [
            #              xxxxxxxxxxxxxxxx
            StoryMenuItem('BIP-85 Wallet #1', b85, arg=dbase+1, flags=TC_WORD_WALLET),
            StoryMenuItem('BIP-85 Wallet #2', b85, arg=dbase+2, flags=TC_WORD_WALLET),
            StoryMenuItem('BIP-85 Wallet #3', b85, arg=dbase+3, flags=TC_WORD_WALLET),
            StoryMenuItem('Legacy Wallet', "Uses duress wallet created on Mk3 Coldcard, using a fixed derivation.\n\nRecommended only for existing UTXO compatibility.", flags=TC_XPRV_WALLET),
            StoryMenuItem('Blank Coldcard', "Look and act like a freshly wiped Coldcard", flags=TC_BLANK_WALLET),
        ]
        self.WillWipeMenu = MenuSystem([
            #              xxxxxxxxxxxxxxxx
            StoryMenuItem('Wipe & Reboot', "Seed is wiped and Coldcard reboots without notice.",
                            flags=TC_WIPE|TC_REBOOT),
            StoryMenuItem('Silent Wipe', "Seed is silently wiped and Coldcard acts as if PIN code was just wrong.",
                            flags=TC_WIPE|TC_FAKE_OUT),
            StoryMenuItem('Wipe -> Wallet', "Seed is silently wiped, and Coldcard logs into a duress wallet. Select type of wallet on next menu.", menu=DuressOptions),
            StoryMenuItem('Say Wiped, Stop', "Seed is wiped and a message is shown.",
                            flags=TC_WIPE),
        ])
        from countdowns import lgto_map
        def_to = settings.get('lgto', 0) or 60   # use 1hour or current countdown length as default

        countdown_menu = MenuSystem([
            #              xxxxxxxxxxxxxxxx
            StoryMenuItem('Wipe & Countdown', "Seed is wiped at start of countdown.",
                            flags=TC_WIPE|TC_COUNTDOWN, arg=def_to),
            StoryMenuItem('Countdown & Brick', "Does the countdown, then system is bricked.",
                            flags=TC_WIPE|TC_BRICK|TC_COUNTDOWN, arg=def_to),
            StoryMenuItem('Just Countdown', "Shows countdown, has no effect on seed.",
                            flags=TC_COUNTDOWN, arg=def_to),
        ])
        FirstMenu = [
            #MenuItem('"%s" =>' % self.proposed_pin),
            MenuItem('[%s]' % self.proposed_pin),
            StoryMenuItem('Brick Self', "Become a brick instantly and forever.", flags=TC_BRICK),
            StoryMenuItem('Wipe Seed', "Wipe the seed and maybe do more. See next menu.",
                                            menu=self.WillWipeMenu),
            StoryMenuItem('Duress Wallet', "Goes directly to a specific duress wallet. No side effects.", menu=DuressOptions),
            StoryMenuItem('Login Countdown', "Pretends a login countdown timer (%s) is in effect but wipes seed first. Resets system at end of countdown or bricks it." % lgto_map[def_to].strip(),
                    menu=countdown_menu),
            StoryMenuItem('Look Blank', "Look and act like a freshly- wiped Coldcard but don't affect actual seed.", flags=TC_BLANK_WALLET),
            StoryMenuItem('Just Reboot', "Reboot when this PIN is entered. Doesn't do anything else.", flags=TC_REBOOT),
            StoryMenuItem('Delta Mode', '''\
Advanced! Logs into REAL seed and allows attacker to do most things, \
but will produce incorrect signatures when signing PSBT files. \
Wipes seed if they try to do certain actions that might reveal \
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),
        ]
        m = MenuSystem(FirstMenu)
        m.goto_idx(1)
        the_ux.push(m)


    async def set_any_wrong(self, *a):
        ch = await ux_show_story('''\
After X incorrect PIN attempts, this feature will be triggered. It can wipe \
the seed phrase, and/or brick the Coldcard. Regardless of this (or any other \
setting) the Coldcard will always brick after 13 failed PIN attempts.''')
        if ch == 'x': return

        self.proposed_pin = WRONG_PIN_CODE
        num = await ux_enter_number("#of wrong attempts", 12)
        if num is None: return

        # - can't do countdown here because of only one tc_arg value per slot
        # - zero and one effectively the same
        if num == 0:
            num = 1

        rel = ['', 'ANY', '2nd', '3rd'][num] if num <= 3 else ('%dth' % num)

        m = MenuSystem([
            #              xxxxxxxxxxxxxxxx
            MenuItem('[%s WRONG PIN]' % rel),
            StoryMenuItem('Wipe, Stop', "Seed is wiped and a message is shown.",
                arg=num, flags=TC_WIPE),
            StoryMenuItem('Wipe & Reboot', "Seed is wiped and Coldcard reboots without notice.",
                            arg=num, flags=TC_WIPE|TC_REBOOT),
            StoryMenuItem('Silent Wipe', "Seed is silently wiped and Coldcard acts as if PIN code was just wrong.",
                            arg=num, flags=TC_WIPE|TC_FAKE_OUT),
            StoryMenuItem('Brick Self', "Become a brick instantly and forever.", flags=TC_BRICK),
            StoryMenuItem('Last Chance', "Wipe seed, then give one more try and then brick if wrong PIN.", arg=num, flags=TC_WIPE|TC_BRICK),
            StoryMenuItem('Look Blank', "Look and act like a freshly- wiped Coldcard but don't affect actual seed.", arg=num, flags=TC_BLANK_WALLET),
            StoryMenuItem('Just Reboot', "Reboot when this happens. Doesn't do anything else.", arg=num, flags=TC_REBOOT),
        ])

        m.goto_idx(1)
        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 any(tp.get_duress_pins()):
            if not await ux_confirm("Any funds on the duress wallet(s) have been moved already?"):
                return

        tp.clear_all()
        m.update_contents()

    async def hide_pin(self, m,l, item):
        pin, slot_num, flags = item.arg

        if flags & TC_DELTA_MODE:
            await ux_show_story('''Delta mode PIN will be hidden if trick PIN menu is shown \
to attacker, and we need to update this record if the main PIN is changed, so we don't support \
hiding this item.''')
            return

        if pin != WRONG_PIN_CODE:
            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

        # just a settings change
        tp.forget_pin(pin)

        self.pop_submenu()

    def pop_submenu(self):
        the_ux.pop()
        m = the_ux.top_of_stack()
        m.update_contents()

    async def change_pin(self, m,l, item):
        # Change existing PIN code.
        old_pin, slot_num, flags, tc_arg = item.arg

        new_pin = await self.get_new_pin(old_pin)
        if new_pin is None:
            return

        if flags & TC_DELTA_MODE:
            # if delta mode ... must apply rules to new PIN
            prob, a = validate_delta_pin(self.current_pin, new_pin)
            if prob:
                await ux_show_story(prob, 'Sorry!')
                return
            tc_arg = a

        try:
            tp.update_slot(old_pin.encode(), new_pin=new_pin.encode(), tc_arg=tc_arg)
            await ux_dramatic_pause("Changed.", 1)

            self.pop_submenu()      # too lazy to get redraw right
        except BaseException as exc:
            sys.print_exception(exc)
            await ux_show_story("Failed: %s" % exc)

    async def delete_pin(self, m,l, item):
        pin, slot_num, flags = 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 pin == WRONG_PIN_CODE:
            msg = "Remove special handling of wrong PINs?"
        else:
            msg = "Removing trick PIN:\n  %s\n\nOk?" % pin

        if not await ux_confirm(msg):
            return

        if flags & TC_WORD_WALLET:
            nslots = 2
        elif flags & TC_XPRV_WALLET:
            nslots = 3
        else:
            nslots = 1

        tp.clear_slots(range(slot_num, slot_num+nslots))
        tp.forget_pin(pin)

        self.pop_submenu()

    async def activate_wallet(self, m, l, item):
        # load the secrets of a wallet for immediate use
        # - duress or blank wallet
        pin, flags, arg = item.arg

        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.''')
        if ch != 'y': return

        from pincodes import pa, AE_SECRET_LEN
        b, slot = tp.get_by_pin(pin)
        assert slot

        # TC_BLANK_WALLET here would be nice, but no support working w/ fake empty secret

        # emulate stash.py encoding
        if flags & TC_XPRV_WALLET:
            encoded = b'\x01' + slot.xdata[0:64]
        elif flags & TC_WORD_WALLET and (arg // 1000 == 1):
            encoded = b'\x82' + slot.xdata[0:32]
        elif flags & TC_WORD_WALLET and (arg // 1000 == 2):
            encoded = b'\x80' + slot.xdata[0:16]
        else:
            raise ValueError('f=0x%x a=%d' % (flags, args))

        from glob import dis

        # switch over to new secret!
        dis.fullscreen("Applying...")
        pa.tmp_secret(encoded)
        tp.reload()

        await ux_show_story("New master key in effect until next power down.")

        from actions import goto_top_menu
        goto_top_menu()

    async def countdown_details(self, m, l, item):
        # explain details of the countdown case
        # - allow change of time period
        from countdowns import lgto_map, lgto_va, lgto_ch
        from menu import start_chooser

        pin, flags, arg = item.arg

        # "arg" can be out-of-date, if they edited timer value after parent was
        # rendered, where arg was captured into item.arg ... so don't use it.
        cd_val = tp.tp[pin][2]

        msg = 'Shows login countdown (%s)' % lgto_map.get(cd_val, '???').strip()
        if flags & TC_WIPE:
            msg += ', wipes the seed'
        else:
            msg += ' and reboots at end of countdown'
        if flags & TC_BRICK:
            msg += ' and bricks system at end of countdown'

        msg += '.\n\nPress 4 to change time.'
        ch = await ux_show_story(msg, escape='4')
        if ch != '4': return

        def adjust_countdown_chooser():
            # 'disabled' choice not appropriate for this case
            ch = lgto_ch[1:]
            va = lgto_va[1:]

            def set_it(idx, text):
                new_val = va[idx+1]
                # save it
                try:
                    b, slot = tp.update_slot(pin.encode(), tc_flags=flags, tc_arg=new_val)
                except BaseException as exc:
                    sys.print_exception(exc)

            return va.index(cd_val), lgto_ch[1:], set_it

        start_chooser(adjust_countdown_chooser)

    async def duress_details(self, m, l, item):
        # explain details of a duress wallet
        pin, flags, arg = item.arg
        if flags & TC_XPRV_WALLET:
            msg = '''The legacy duress wallet will be activated if '%s' is provded. \
You probably created this on an older Mk2 or Mk3 Coldcard. \
Wallet is XPRV-based and derived from a fixed path.''' % pin
        elif flags & TC_WORD_WALLET:
            nwords = 12 if (arg // 1000 == 2) else 24
            msg = '''BIP-85 derived wallet (%d words), with index #%d, is provided if '%s'.''' \
                        % (nwords, arg, pin)
        else:
            raise ValueError(hex(flags))

        ch = await ux_show_story(msg + '\n\nPress 6 to view associated secrets.', escape='6')
        if ch != '6': return

        b, s = tp.get_by_pin(pin)
        if s == None:
            # could not find in SE2. Our settings vs. SE2 are not in sync.
            msg = "Not found in SE2. Delete and remake."
        else:
            from actions import render_master_secrets

            assert s.tc_flags == flags
            if flags & TC_XPRV_WALLET:
                node = ngu.hdnode.HDNode()
                ch, pk = s.xdata[0:32], s.xdata[32:64]
                node.from_chaincode_privkey(ch, pk)

                msg, *_ = render_master_secrets('xprv', None, node)
            elif flags & TC_WORD_WALLET:
                raw = s.xdata[0:(32 if nwords == 24 else 16)]
                msg, *_ = render_master_secrets('words', raw, None)
            else:
                raise ValueError(hex(flags))

        await ux_show_story(msg, sensitive=True)
        

    async def pin_submenu(self, menu, label, item):
        # drill down into a sub-menu per existing PIN
        # - data display only, no editing; just clear and redo
        pin = item.arg
        slot_num, flags, arg = tp.tp[pin] if (pin in tp.tp) else (-1, 0, 0)

        rv = []

        if pin != WRONG_PIN_CODE:
            rv.append(MenuItem('PIN %s' % pin))
        else:
            rv.append(MenuItem("After %d wrong:" % arg))

        if flags & (TC_WORD_WALLET | TC_XPRV_WALLET):
            rv.append(MenuItem("↳Duress Wallet", f=self.duress_details, arg=(pin, flags, arg)))
        elif flags & TC_BLANK_WALLET:
            rv.append(MenuItem("↳Blank Wallet"))
        elif flags & TC_COUNTDOWN:
            rv.append(MenuItem("↳Countdown", f=self.countdown_details, arg=(pin, flags, arg)))
        elif flags & TC_FAKE_OUT:
            rv.append(MenuItem("↳Pretends Wrong"))
        elif flags & TC_DELTA_MODE:
            rv.append(MenuItem("↳Delta Mode"))

        for m, msg in [
            (TC_WIPE,   '↳Wipes seed'),
            (TC_BRICK,  '↳Bricks CC'),
            (TC_REBOOT, '↳Reboots'),
        ]:
            if flags & m:
                rv.append(MenuItem(msg))

        if flags & (TC_WORD_WALLET | TC_XPRV_WALLET):
            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)),
        ])
        if pin != WRONG_PIN_CODE:
            rv.append(
                MenuItem('Change PIN', f=self.change_pin, arg=(pin, slot_num, flags, arg)),
            )

        return rv

class StoryMenuItem(MenuItem):
    def __init__(self, label, story, flags=0, **kws):
        self.story = story
        self.flags = flags
        super().__init__(label, **kws)

    async def activate(self, menu, idx):
        ch = await ux_show_story(self.story)
        if ch == 'x':
            return

        if getattr(self, 'next_menu', None):
            # drill down more
            return await super().activate(menu, idx)

        # pop some levels, and note the drill-down path that was used
        parents = []
        while 1:
            the_ux.pop()
            parent = the_ux.top_of_stack()
            assert parent

            parents.insert(0, parent)

            if isinstance(parent, TrickPinMenu):
                await parent.done_picking(self, parents)
                return

# EOF
