# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# backups.py - Save and restore backup data.
#
import compat7z, stash, ckcc, chains, gc, sys, bip39, uos, ngu
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
from utils import deserialize_secret, swab32, xfp2str
from sffile import SFFile
from ux import ux_show_story, ux_confirm, ux_dramatic_pause, OK, X, ux_input_text
import version, ujson
from uio import StringIO, BytesIO
import seed
from glob import settings
from pincodes import pa

# we make passwords with this number of words
num_pw_words = const(12)
bkpw_min_len = const(32)

# max size we expect for a backup data file (encrypted or cleartext)
# - limited by size of LFS area of flash, since all settings are held there
MAX_BACKUP_FILE_SIZE = const(128*1024)     # bytes

def render_backup_contents(bypass_tmp=False):
    # simple text format: 
    #   key = value
    # or #comments
    # but value is JSON
    current_tmp = None
    rv = StringIO()

    def COMMENT(val=None):
        if val:
            rv.write('\n# %s\n' % val)
        else:
            rv.write('\n')

    def ADD(key, val):
        rv.write('%s = %s\n' % (key, ujson.dumps(val)))

    rv.write('# Coldcard backup file! DO NOT CHANGE.\n')

    chain = chains.current_chain()

    COMMENT('Private key details: ' + chain.name)

    with stash.SensitiveValues(bypass_tmp=bypass_tmp, enforce_delta=True) as sv:
        if sv.mode == 'words':
            ADD('mnemonic', bip39.b2a_words(sv.raw))

        if sv.mode == 'master':
            ADD('bip32_master_key', b2a_hex(sv.raw))

        ADD('chain', chain.ctype)
        ADD('xprv', chain.serialize_private(sv.node))
        ADD('xpub', chain.serialize_public(sv.node))

        # BTW: everything is really a duplicate of this value
        ADD('raw_secret', b2a_hex(sv.secret).rstrip(b'0'))

        if version.has_608:
            # save the so-called long-secret
            ADD('long_secret', b2a_hex(pa.ls_fetch()))

        # Duress wallets (somewhat optional, since derived)
        from trick_pins import tp
        for label, path, pairs in tp.backup_duress_wallets(sv):
            COMMENT()
            COMMENT(label + ' (informational)')
            COMMENT(path)
            for k,v in pairs:
                ADD(k, v)

        if bypass_tmp and pa.tmp_value:
            current_tmp = pa.tmp_value[:]
            pa.tmp_value = None
            # we also need correct settings from main seed
            nv = stash.SecretStash.encode(seed_phrase=sv.raw)
            settings.set_key(nv)
            settings.load()
            stash.blank_object(nv)
    
    COMMENT('Firmware version (informational)')
    date, vers, timestamp = version.get_mpy_version()[0:3]
    ADD('fw_date', date)
    ADD('fw_version', vers)
    ADD('fw_timestamp', timestamp)
    COMMENT('Coldcard Hardware')
    ADD('serial', version.serial_number())
    ADD('hardware', version.hw_label)

    COMMENT('User preferences')

    # user preferences
    for k,v in settings.current.items():
        if k[0] == '_': continue        # debug stuff in simulator
        if k == 'xpub': continue        # redundant, and wrong if bip39pw
        if k == 'xfp': continue         # redundant, and wrong if bip39pw
        if k == 'bkpw': continue        # confusing/circular
        if k == 'sd2fa': continue       # do NOT backup SD 2FA (card can be lost or damaged)
        if k == 'words': continue       # words length is recalculated from secret
        if k == 'ccc': continue         # not supported, security issue
        if k == 'ktrx': continue        # not useful after the fact
        if k == 'lfr': continue         # temporary error msg value
        if k == 'seedvault' and not v: continue
        if k == 'seeds' and not v: continue
        ADD('setting.' + k, v)

    if version.supports_hsm:
        import hsm
        if hsm.hsm_policy_available():
            ADD('hsm_policy', hsm.capture_backup())

    rv.write('\n# EOF\n')

    if bypass_tmp and current_tmp:
        # go back to tmp secret and its settings
        stash.SensitiveValues.clear_cache()
        pa.tmp_value = current_tmp
        settings.set_key()
        settings.load()

    return rv.getvalue()

def extract_raw_secret(vals):
    # step1: the private key
    # - prefer raw_secret over other values
    # - TODO: fail back to other values
    assert 'raw_secret' in vals
    rs = vals.pop('raw_secret')

    raw = deserialize_secret(rs)

    # check we can decode this right (might be different firmare)
    opmode, bits, node = stash.SecretStash.decode(raw)
    assert node

    # verify against xprv value (if we have it)
    if 'xprv' in vals:
        check_xprv = chains.get_chain(vals.get('chain', 'BTC')).serialize_private(node)
        assert check_xprv == vals['xprv'], 'xprv mismatch'

    return raw, node

def extract_long_secret(vals):
    ls = None
    if ('long_secret' in vals) and version.has_608:
        try:
            ls = a2b_hex(vals.pop('long_secret'))
        except:
            # sys.print_exception(exc)
            # but keep going.
            pass
    return ls

def restore_from_dict_ll(vals, raw):
    # Restore from a dict of values. Already JSON decoded.
    # Need a Reboot on success, return string on failure
    # - low-level version, factored out for better testing
    from glob import dis

    need_ftux = False

    #print("Restoring from: %r" % vals)
    chain = chains.get_chain(vals.get('chain', 'BTC'))

    dis.fullscreen("Saving...")
    dis.progress_bar_show(.1)

    # clear (in-memory) settings and change also nvram key
    # - also captures xfp, xpub at this point
    pa.change(new_secret=raw)
    dis.progress_bar_show(.25)

    # force the right chain
    pa.new_main_secret(raw, chain)         # updates xfp/xpub
    pb = .45  # last Progress Bar value
    dis.progress_bar_show(pb)

    # NOTE: don't fail after this point... they can muddle thru w/ just right seed
    ls = extract_long_secret(vals)
    if ls is not None:
        try:
            pa.ls_change(ls)
        except: pass # but keep going
        pb = .70
        dis.progress_bar_show(pb)

    # if sd2fa is encountered during backup restore - purge it
    settings.remove_key("sd2fa")

    # restore settings from backup file
    vals_len = len(vals)
    g = (1-pb) / vals_len
    for key in vals:
        pb += g
        dis.progress_bar_show(pb)
        if not key[:8] == "setting.":
            continue

        k = key[8:]

        if k == 'sd2fa':
            # do NOT restore sd2fa as SD card can be lost or damaged
            # new version of firmware 5.1.3+ will not back sd2fa
            # old backups need this to function properly
            continue

        if k == 'ccc':
            # CCC feature cannot be backed-up nor restored for security reasons
            # (would allow replay attacks)
            continue

        if k == 'tp':
            # restore trick pins, which may involve many ops
            from trick_pins import tp
            try:
                tp.restore_backup(vals[key])
            except: pass

            # continue as `tp.restore_backup` handles
            # saving into settings
            continue

        if k == 'notes' and not version.has_qwerty:
            # Secure notes only supported on keyboard-equiped units
            continue

        # possible that user arrived into already set-up settings
        # that he maybe used as an ephemeral before - we need to set
        # proper values wrt HW switches
        if k == 'du':
            # inverted (Disable Usb)
            if not vals[key]:
                vals[key] = 1
                need_ftux = True

        if k in ('nfc', 'vidsk'):
            if vals[key]:
                vals[key] = 0
                need_ftux = True

        settings.set(k, vals[key])

    if not settings.get("du", None):
        # settings.set("du", 1)
        # above will be done in ftux
        need_ftux = True

    # write out
    settings.save()
    dis.progress_bar_show(1)

    if version.supports_hsm and ('hsm_policy' in vals):
        import hsm
        hsm.restore_backup(vals['hsm_policy'])

    return None, need_ftux

def text_bk_parser(contents):
    # given a (binary encoded) text file, decode into a dict of values
    # - use json rules to decode the "value" sides
    vals = {}
    for line in contents.decode().split('\n'):
        if not line: continue
        if line[0] == '#': continue

        try:
            k,v = line.split(' = ', 1)
            #print("%s = %s" % (k, v))

            vals[k] = ujson.loads(v)
        except:
            print("unable to decode line: %r" % line)
            # but keep going!

    return vals

async def restore_tmp_from_dict_ll(vals, raw):
    from glob import dis

    chain = chains.get_chain(vals.get('chain', 'BTC'))

    dis.fullscreen("Applying...")
    from seed import set_ephemeral_seed
    from actions import goto_top_menu

    await set_ephemeral_seed(raw, chain, origin="Coldcard Backup")
    for k, v in vals.items():
        if not k[:8] == "setting.":
            continue
        key = k[8:]
        if key == "miniscript":
            # whitelist
            settings.set(key, v)

    goto_top_menu()

async def restore_from_dict(vals, raw):
    # Restore from a dict of values. Already JSON decoded (ie. dict object).
    # Need a Reboot on success, return string on failure

    prob, need_ftux = restore_from_dict_ll(vals, raw)
    if prob: return prob

    if need_ftux:
        from ftux import FirstTimeUX
        # do not Welcome them as we are pre-reboot now
        await FirstTimeUX().interact(title=None)

    await ux_show_story('Everything has been successfully restored. '
            'We must now reboot to install the '
            'updated settings and seed.', title='Success!')

    from machine import reset
    reset()


async def make_complete_backup(fname_pattern='backup.7z', write_sflash=False):
    from stash import bip39_passphrase

    pwd = None
    skip_quiz = False
    bypass_tmp = False

    if bip39_passphrase and pa.tmp_value:
        # this is a BIP39 password ephemeral wallet
        msg = ("BIP39 passphrase is in effect. Backup ignores passphrases "
               "and produces backup of main seed. Press %s to back-up main wallet,"
               " press (2) to back-up BIP39 passphrase wallet "
               "(extended private key created via seed + pass)" % OK)
        ch = await ux_show_story(msg, escape="2")
        if ch == "x": return
        if ch == "y":
            bypass_tmp = True

    elif pa.tmp_value:
        if not await ux_confirm("A temporary seed is in effect, "
                                "so backup will be of that seed."):
            return

    # first check if bkpw already defined on tmp seed settings
    stored_pwd = None
    master_pwd = settings.master_get("bkpw", None)
    if pa.tmp_value:
        stored_pwd = settings.get('bkpw', None)

    if not stored_pwd and master_pwd:
        stored_pwd = master_pwd

    if stored_pwd:
        # we can have words or other type of password here
        split_pwd = stored_pwd.split()
        if len(split_pwd) == num_pw_words:  # weak
            hint = " 1: %s\n   ...\n%d: %s" % (split_pwd[0], len(split_pwd), split_pwd[-1])
        else:
            hint = " %s...%s" % (stored_pwd[0], stored_pwd[-1])

        ch = await ux_show_story("Use same backup file password as last time?\n\n" + hint,
                                 sensitive=True)

        if ch == 'y':
            pwd = stored_pwd  # string, not list
            skip_quiz = True

    if not pwd:
        # Pick a password: like bip39 but no checksum word
        #
        b = bytearray(32)
        while 1:
            ckcc.rng_bytes(b)
            pwd = bip39.b2a_words(b).rsplit(' ', num_pw_words)[0]

            ch = await seed.show_words(
                prompt="Record this (%d word) backup file password:\n" % num_pw_words,
                words=pwd.split(" "), escape='6'
            )

            if (ch == '6') and not write_sflash:
                # Secret feature: plaintext mode
                # - only safe for people living in faraday cages inside locked vaults.
                if await ux_confirm("The file will **NOT** be encrypted and "
                                    "anyone who finds the file will get all of your money for free!"):
                    pwd = []
                    fname_pattern = 'backup.txt'
                    break
                continue

            if ch == 'x':
                return

            break

    if pwd and not skip_quiz:
        # quiz them, but be nice and do a shorter test.
        ch = await seed.word_quiz(pwd.split(" "), limited=(num_pw_words//3))
        if ch == 'x': return

    if pwd and pwd != stored_pwd:
        ch = await ux_show_story("Would you like to use these same words next time you perform a backup?"
                                 " Press (1) to save them into this Coldcard for next time.", escape='1')

        if ch == '1':
            settings.set('bkpw', pwd)  # if on tmp save to tmp, do not update master
            settings.save()
        # stop droping bkpw just because someone decided to use differrent password
        # elif stored_words:
        #     settings.remove_key('bkpw')
        #     settings.save()

    return await write_complete_backup(pwd, fname_pattern, write_sflash=write_sflash,
                                       bypass_tmp=bypass_tmp)

async def write_complete_backup(pwd, fname_pattern, write_sflash=False,
                                allow_copies=True, bypass_tmp=False):
    # Just do the writing
    from glob import dis
    from files import CardSlot

    # Show progress:
    dis.fullscreen('Encrypting...' if pwd else 'Generating...')
    body = render_backup_contents(bypass_tmp=bypass_tmp).encode()

    gc.collect()

    if pwd:
        # NOTE: Takes a few seconds to do the key-streching, but little actual
        # time to do the encryption.

        zz = compat7z.Builder(password=pwd, progress_fcn=dis.progress_bar_show)
        zz.add_data(body)

        # pick random filename, but ending in .txt
        word = bip39.wordlist_en[ngu.random.uniform(2048)]
        num = ngu.random.uniform(1000)
        fname = '%s%d.txt' % (word, num)

        hdr, footer = zz.save(fname)

        del body

        gc.collect()
    else:
        # cleartext dump
        zz = None

    if write_sflash:
        # for use over USB and unit testing: commit file into PSRAM
        with SFFile(0, max_size=MAX_BACKUP_FILE_SIZE, message='Saving...') as fd:
            if zz:
                fd.write(hdr)
                fd.write(zz.body)
                fd.write(footer)
            else:
                fd.write(body)

            return fd.tell(), fd.checksum.digest()

    for copy in range(25):
        # choose a filename

        try:
            with CardSlot() as card:
                fname, nice = card.pick_filename(fname_pattern)

                # do actual write
                with card.open(fname, 'wb') as fd:
                    if zz:
                        fd.write(hdr)
                        fd.write(zz.body)
                        fd.write(footer)
                    else:
                        fd.write(body)

        except Exception as e:
            # includes CardMissingError
            # catch any error
            ch = await ux_show_story('Failed to write! Please insert formated MicroSD card, '
                                    'and press %s to try again.\n\n%s to cancel.\n\n\n%s' % (OK, X, e))
            if ch == 'x': break
            continue

        if not allow_copies:
            return

        if copy == 0:
            while 1:
                msg = '''Backup file written:\n\n%s\n\n\
To view or restore the file, you must have the full password.\n\n\
Insert another SD card and press (2) to make another copy.''' % nice
    
                ch = await ux_show_story(msg, escape='2')

                if ch in 'xy': return
                if ch == '2': break

        else:
            ch = await ux_show_story('''File (#%d) written:\n\n%s\n\n\
Press %s for another copy, or press %s to stop.''' % (copy+1, nice, OK, X), escape='2')
            if ch == 'x': break

async def verify_backup_file(fname):
    # read 7z header, and measure checksums
    # - no password is wanted/required
    # - really just checking CRC32, but that's enough against truncated files
    from files import CardSlot, CardMissingError, needs_microsd
    prob = None
    fd = None

    # filename already picked, open it.
    try:
        with CardSlot(readonly=True) as card:
            prob = 'Unable to open backup file.'
            fd = card.open(fname, 'rb')

            prob = 'Unable to read backup file headers. Might be truncated.'
            compat7z.check_file_headers(fd)

            prob = 'Unable to verify backup file contents.'
            zz = compat7z.Builder()
            files = zz.verify_file_crc(fd, MAX_BACKUP_FILE_SIZE)

            assert len(files) == 1
            fname, fsize = files[0]
            assert fname.endswith('.txt')
            assert 400 < fsize < MAX_BACKUP_FILE_SIZE, 'size'

    except CardMissingError:
        await needs_microsd()
        return
    except Exception as e:
        await ux_show_story(prob + '\n\nError: ' + str(e))
        return
    finally:
        if fd is not None:
            try:
                fd.close()
            except OSError:
                # might be already closed on vdisk case due to filesystem unmount/mount
                pass

    await ux_show_story("Backup file CRC checks out okay.\n\n"
                        "Please note this is only a check against accidental truncation and similar."
                        " Targeted modifications can still pass this test. You may further verify"
                        " this backup file by starting the normal restore process (Restore Backup)"
                        " and aborting it once decryption has been achieved.")


async def restore_complete(fname_or_fd, temporary=False, words=True, usb=False):
    from ux import the_ux

    async def done(words):
        # remove all pw-picking from menu stack
        if not version.has_qwerty and words:
            seed.WordNestMenu.pop_all()

        prob = await restore_complete_doit(fname_or_fd, words,
                                           temporary=temporary)
        if prob:
            await ux_show_story(prob, title='FAILED')

    if words:
        if version.has_qwerty:
            from ux_q1 import seed_word_entry, CHARS_W

            basename = None
            if isinstance(fname_or_fd, str):
                basename = fname_or_fd.split('/')[-1]
                if len(basename) > CHARS_W:
                    basename = basename[:16] + "⋯" + basename[-16:]

            return await seed_word_entry("Enter Password%s:" % (" for" if basename else ""),
                                         num_pw_words, done_cb=done, has_checksum=False,
                                         line2=basename)

        # give them a menu to pick from, and start picking
        if usb:
            # we're not originating from a menu
            words = await seed.WordNestMenu.get_n_words(12)
            await done(words)
        else:
            m = seed.WordNestMenu(num_words=num_pw_words, has_checksum=False, done_cb=done)
            the_ux.push(m)

    else:
        pwd = []  # cleartext if words=None
        if words is False:
            ipw = await ux_input_text("", prompt="Your Backup Password",
                                      min_len=bkpw_min_len, max_len=128)
            if not ipw: return
            pwd.append(ipw)

        await done(pwd)


def check_and_decrypt(fd, password):
    try:
        compat7z.check_file_headers(fd)
    except Exception as e:
        raise RuntimeError('Unable to read backup file.'
                           ' Has it been touched?\n\nError: '+str(e))

    from glob import dis
    dis.fullscreen("Decrypting...")
    try:
        zz = compat7z.Builder()
        fname, contents = zz.read_file(fd, password, MAX_BACKUP_FILE_SIZE,
                                       progress_fcn=dis.progress_bar_show)

        # simple quick sanity checks
        assert fname.endswith('.txt')  # was == 'ckcc-backup.txt'
        assert contents[0:1] == b'#' and contents[-1:] == b'\n'
        return contents

    except Exception as e:
        # assume everything here is "password wrong" errors
        raise RuntimeError('Unable to decrypt backup file. Incorrect password?'
                           '\n\nTried:\n\n' + password)


async def restore_complete_doit(fname_or_fd, words, file_cleanup=None, temporary=False,
                                ux_confirm=True):
    # Open file, read it, maybe decrypt it; return string if any error
    # - some errors will be shown, None return in that case
    # - no return if successful (due to reboot)
    from files import CardSlot, CardMissingError, needs_microsd

    # build password
    password = ' '.join(words)
    prob = None

    if isinstance(fname_or_fd, int):
        # USB restore - backup is already in PSRAM, fname of fd is length
        # TXN_INPUT_OFFSET = 0
        with SFFile(0, length=fname_or_fd) as fd:
            if not words:
                contents = fd.read(fname_or_fd)
            else:
                # read full size, then decrypt
                fd = BytesIO(fd.read(fname_or_fd))
                try:
                    contents = check_and_decrypt(fd, password)
                except RuntimeError as e:
                    return str(e)
    else:
        try:
            with CardSlot(readonly=True) as card:
                # filename already picked, taste it and maybe consider using its data.
                try:
                    fd = open(fname_or_fd, 'rb')
                except:
                    return 'Unable to open backup file.\n\n' + str(fname_or_fd)

                try:
                    if words:
                        contents = check_and_decrypt(fd, password)
                    else:
                        contents = fd.read()

                except RuntimeError as e:
                    return str(e)
                finally:
                    fd.close()

                if file_cleanup:
                    file_cleanup(fname_or_fd)

        except CardMissingError:
            await needs_microsd()
            return

    try:
        vals = text_bk_parser(contents)
    except:
        return "Invalid backup file."

    try:
        raw, node = extract_raw_secret(vals)
    except Exception as e:
        return ('Unable to decode raw_secret and '
                'restore the seed value!\n\n\n'+str(e))

    if ux_confirm:
        # check master fingerprint from raw secret that is actually being loaded
        # master extended public keys can be wrong & is unverified
        xfp_str = xfp2str(swab32(node.my_fp()))
        ch = await ux_show_story("Above is the master fingerprint of the seed stored in the backup."
                                 " Press %s to continue, and load backup as %s seed. Press %s"
                                 " to abort." % (OK, "temporary" if temporary else "master", X),
                                 title="["+xfp_str+"]")
        if ch != "y":
            await ux_dramatic_pause('Aborted.', 2)
            return

    # this leads to reboot if it works, else errors shown, etc.
    if temporary:
        return await restore_tmp_from_dict_ll(vals, raw)
    else:
        return await restore_from_dict(vals, raw)

async def clone_start(*a):
    # Begins cloning process, on target device.
    from files import CardSlot, CardMissingError, needs_microsd

    ch = await ux_show_story('''Insert a MicroSD card and press %s to start. A small \
file with an ephemeral public key will be written.''' % OK)
    if ch != 'y': return

    # pick a random key pair, just for this cloning session
    pair = ngu.secp256k1.keypair()
    my_pubkey = pair.pubkey().to_bytes(False)

    # write to SD Card, fixed filename for ease of use
    try:
        with CardSlot() as card:
            fname, nice = card.pick_filename('ccbk-start.json', overwrite=True)
            with card.open(fname, 'wb') as fd:
                fd.write(ujson.dumps(dict(pubkey=b2a_hex(my_pubkey))))
            
    except CardMissingError:
        await needs_microsd()
        return
    except Exception as e:
        await ux_show_story('Error: ' + str(e))
        return
    
    # Wait for incoming clone file, allow retries
    ch = await ux_show_story('''Keep power on this Coldcard, and take MicroSD card \
to source Coldcard. Select Advanced/Tools > Backup > Clone Coldcard to write to card. Bring that card \
back and press %s to complete clone process.''' % OK)

    while 1:
        if ch != 'y': 
            # try to clean up, but card probably not there? No errors.
            try:
                with CardSlot() as card:
                    uos.remove(fname)
            except:
                pass 

            await ux_dramatic_pause('Aborted.', 2)
            return

        # Hopefully we have a suitable 7z file now. Pubkey in the filename
        incoming = None
        try:
            with CardSlot() as card:
                for path in card.get_paths():
                    for fn, ftype, *var in uos.ilistdir(path):
                        if fn.endswith('-ccbk.7z'):
                            incoming = path + '/' + fn
                            his_pubkey = a2b_hex(fn[0:66])

                            assert len(his_pubkey) == 33
                            assert 2 <= his_pubkey[0] <= 3
                            break

        except CardMissingError:
            await needs_microsd()
            continue
        except Exception as e:
            pass

        if incoming:
            break

        ch = await ux_show_story("Clone file not found. %s to try again, %s to stop." % (OK, X))

    # calculate point
    session_key = pair.ecdh_multiply(his_pubkey)

    # "password" is that hex value
    words = [b2a_hex(session_key).decode()]

    def delme(xfn):
        # Callback to delete file after its read; could still fail but
        # need to start over in that case anyway.
        uos.remove(xfn)
        uos.remove(fname)       # ccbk-start.json

    # this will reset in successful case, no return (but delme is called)
    # no need to ask for UX confirmation during clone - as user can see what is loaded on source CC
    prob = await restore_complete_doit(incoming, words, file_cleanup=delme,
                                       ux_confirm=False)
    if prob:
        await ux_show_story(prob, title='FAILED')

async def clone_write_data(*a):
    # Write encrypted backup file, for cloning purposes, based on a public key
    # found on the SD Card.
    # - input file must already exist on inserted card
    from files import CardSlot, CardMissingError

    try:
        with CardSlot() as card:
            path = card.get_sd_root()
            with open(path + '/ccbk-start.json', 'rb') as fd:
                d = ujson.load(fd)
                his_pubkey = a2b_hex(d.get('pubkey'))
                # expect compress pubkey
                assert len(his_pubkey) == 33
                assert 2 <= his_pubkey[0] <= 3

            # remove any other clone-files on this card, so no confusion
            # on receiving end; unlikely they can work anyway since new key each time
            for path in card.get_paths():
                for fn, ftype, *var in uos.ilistdir(path):
                    if fn.endswith('-ccbk.7z'):
                        try:
                            uos.remove(path + '/' + fn)
                        except:
                            pass

    except (CardMissingError, OSError) as exc:
        # Standard msg shown if no SD card detected when we need one.
        await ux_show_story("Start this process on the other Coldcard, which will write a file onto MicroSD card as the first step.\n\nInsert that card and try again here.")
        return

    # pick our own temp keys for this encryption
    pair = ngu.secp256k1.keypair()
    my_pubkey = pair.pubkey().to_bytes(False)
    session_key = pair.ecdh_multiply(his_pubkey)

    fname = b2a_hex(my_pubkey).decode() + '-ccbk.7z'

    await write_complete_backup(b2a_hex(session_key).decode(), fname, allow_copies=False, bypass_tmp=True)

    await ux_show_story("Done.\n\nTake this MicroSD card back to other Coldcard and continue from there.")

# EOF
