Beispiel #1
0
    def handle_bag_number(self, bag_num):
        import version, callgate
        from glob import dis
        from pincodes import pa

        if version.is_factory_mode and bag_num:
            # check state first
            assert settings.get('tested', False)
            assert pa.is_blank()
            assert 8 <= len(bag_num) < 32

            # do the change
            failed = callgate.set_bag_number(bag_num)
            assert not failed

            callgate.set_rdp_level(2 if not is_devmode else 0)
            pa.greenlight_firmware()
            dis.fullscreen(bytes(bag_num).decode())

            self.call_after(callgate.show_logout, 1)

        # always report the existing/new value
        val = callgate.get_bag_number() or b''

        return b'asci' + val
Beispiel #2
0
    async def append(self, xfp, bip39pw):
        # encrypt and save; always appends.
        from ux import ux_dramatic_pause
        from glob import dis
        from actions import needs_microsd

        while 1:
            dis.fullscreen('Saving...')

            try:
                with CardSlot() as card:
                    self._calc_key(card)

                    data = self._read(card) if self.key else []

                    data.append(dict(xfp=xfp, pw=bip39pw))

                    encrypt = ngu.aes.CTR(self.key)

                    msg = encrypt.cipher(ujson.dumps(data))

                    with open(self.filename(card), 'wb') as fd:
                        fd.write(msg)

                await ux_dramatic_pause("Saved.", 1)
                return

            except CardMissingError:
                ch = await needs_microsd()
                if ch == 'x':       # undocumented, but needs escape route
                    break
Beispiel #3
0
async def make_json_wallet(label, generator, fname_pattern='new-wallet.json'):
    # Record **public** values and helpful data into a JSON file

    from glob import dis
    from files import CardSlot, CardMissingError
    from actions import needs_microsd

    dis.fullscreen('Generating...')

    body = generator()

    # choose a filename
        
    try:
        with CardSlot() as card:
            fname, nice = card.pick_filename(fname_pattern)

            # do actual write
            with open(fname, 'wt') as fd:
                ujson.dump(body, fd)

    except CardMissingError:
        await needs_microsd()
        return
    except Exception as e:
        await ux_show_story('Failed to write!\n\n\n'+str(e))
        return

    msg = '''%s file written:\n\n%s''' % (label, nice)
    await ux_show_story(msg)
Beispiel #4
0
    def __init__(self, *args):
        super().__init__()

        from glob import dis
        dis.fullscreen('Wait...')

        # this must set self.address and do other slow setup
        self.setup(*args)
Beispiel #5
0
        def make_msg():
            msg = ''
            if n > 1:
                if start == 0:
                    msg = "Press 1 to save to MicroSD."
                    if version.has_fatram and not ms_wallet:
                        msg += " 4 to view QR Codes."
                    msg += '\n\n'
                msg += "Addresses %d..%d:\n\n" % (start, start + n - 1)
            else:
                # single address, from deep path given by user
                msg += "Showing single address."
                if version.has_fatram:
                    msg += " Press 4 to view QR Codes."
                msg += '\n\n'

            addrs = []
            chain = chains.current_chain()

            dis.fullscreen('Wait...')

            if ms_wallet:
                # IMPORTANT safety feature: never show complete address
                # but show enough they can verify addrs shown elsewhere.
                # - makes a redeem script
                # - converts into addr
                # - assumes 0/0 is first address.
                for (i, paths, addr, script) in ms_wallet.yield_addresses(start, n):
                    if i == 0 and ms_wallet.N <= 4:
                        msg += '\n'.join(paths) + '\n =>\n'
                    else:
                        msg += '.../0/%d =>\n' % i

                    addrs.append(addr)
                    msg += truncate_address(addr) + '\n\n'
                    dis.progress_bar_show(i/n)

            else:
                # single-singer wallets

                with stash.SensitiveValues() as sv:

                    for idx in range(start, start + n):
                        deriv = path.format(account=self.account_num, change=0, idx=idx)
                        node = sv.derive_path(deriv, register=False)
                        addr = chain.address(node, addr_fmt)
                        addrs.append(addr)

                        msg += "%s =>\n%s\n\n" % (deriv, addr)

                        dis.progress_bar_show(idx/n)

                    stash.blank_object(node)

            if n > 1:
                msg += "Press 9 to see next group, 7 to go back. X to quit."

            return msg, addrs
Beispiel #6
0
async def make_summary_file(fname_pattern='public.txt'):
    from glob import dis

    # record **public** values and helpful data into a text file
    dis.fullscreen('Generating...')

    # generator function:
    body = generate_public_contents()

    await write_text_file(fname_pattern, body, 'Summary')
Beispiel #7
0
async def make_bitcoin_core_wallet(account_num=0,
                                   fname_pattern='bitcoin-core.txt'):
    from glob import dis
    import ustruct
    xfp = xfp2str(settings.get('xfp'))

    dis.fullscreen('Generating...')

    # make the data
    examples = []
    imp_multi = []
    imp_desc = []
    for a, b in generate_bitcoin_core_wallet(account_num, examples):
        imp_multi.append(a)
        imp_desc.append(b)

    imp_multi = ujson.dumps(imp_multi)
    imp_desc = ujson.dumps(imp_desc)

    body = '''\
# Bitcoin Core Wallet Import File

https://github.com/Coldcard/firmware/blob/master/docs/bitcoin-core-usage.md

## For wallet with master key fingerprint: {xfp}

Wallet operates on blockchain: {nb}

## Bitcoin Core RPC

The following command can be entered after opening Window -> Console
in Bitcoin Core, or using bitcoin-cli:

importdescriptors '{imp_desc}'

### Bitcoin Core before v0.21.0 

This command can be used on older versions, but it is not as robust
and "importdescriptors" should be prefered if possible:

importmulti '{imp_multi}'

## Resulting Addresses (first 3)

'''.format(imp_multi=imp_multi,
           imp_desc=imp_desc,
           xfp=xfp,
           nb=chains.current_chain().name)

    body += '\n'.join('%s => %s' % t for t in examples)

    body += '\n'

    await write_text_file(fname_pattern, body, 'Bitcoin Core')
Beispiel #8
0
async def ux_dramatic_pause(msg, seconds):
    from glob import dis, hsm_active

    if hsm_active:
        return

    # show a full-screen msg, with a dramatic pause + progress bar
    n = seconds * 8
    dis.fullscreen(msg)
    for i in range(n):
        dis.progress_bar_show(i / n)
        await sleep_ms(125)

    ux_clear_keys()
Beispiel #9
0
    def wipe_most(self):
        # erase everything except settings: takes 5 seconds at least
        from nvstore import SLOTS
        end = SLOTS[0]

        from glob import dis
        dis.fullscreen("Cleanup...")

        for addr in range(0, end, self.BLOCK_SIZE):
            self.block_erase(addr)
            dis.progress_bar_show(addr / end)

            while self.is_busy():
                pass
Beispiel #10
0
    def __init__(self, text, subpath, addr_fmt, approved_cb=None):
        super().__init__()
        self.text = text
        self.subpath = subpath
        self.approved_cb = approved_cb

        from glob import dis
        dis.fullscreen('Wait...')

        with stash.SensitiveValues() as sv:
            node = sv.derive_path(subpath)
            self.address = sv.chain.address(node, addr_fmt)

        dis.progress_bar_show(1)
Beispiel #11
0
    async def render(self):
        # Choose from a truncated list of index 0 common addresses, remember
        # the last address the user selected and use it as the default
        from glob import dis
        chain = chains.current_chain()

        dis.fullscreen('Wait...')

        with stash.SensitiveValues() as sv:

            # Create list of choices (address_index_0, path, addr_fmt)
            choices = []
            for name, path, addr_fmt in chains.CommonDerivations:
                if '{coin_type}' in path:
                    path = path.replace('{coin_type}', str(chain.b44_cointype))

                if self.account_num != 0 and '{account}' not in path:
                    # skip derivations that are not affected by account number
                    continue

                deriv = path.format(account=self.account_num, change=0, idx=0)
                node = sv.derive_path(deriv, register=False)
                address = chain.address(node, addr_fmt)
                choices.append( (truncate_address(address), path, addr_fmt) )

                dis.progress_bar_show(len(choices) / len(chains.CommonDerivations))

            stash.blank_object(node)

        items = [MenuItem(address, f=self.pick_single, arg=(path, addr_fmt)) 
                        for i, (address, path, addr_fmt) in enumerate(choices)]

        # some other choices
        if self.account_num == 0:
            items.append(MenuItem("Account Number", f=self.change_account))
            items.append(MenuItem("Custom Path", menu=self.make_custom))

            # if they have MS wallets, add those next
            for ms in MultisigWallet.iter_wallets():
                if not ms.addr_fmt: continue
                items.append(MenuItem(ms.name, f=self.pick_multisig, arg=ms))
        else:
            items.append(MenuItem("Account: %d" % self.account_num, f=self.change_account))

        self.goto_idx(settings.get('axi', 0))      # weak

        self.replace_items(items)
Beispiel #12
0
def set_bip39_passphrase(pw):
    # apply bip39 passphrase for now (volatile)

    # takes a bit, so show something
    from glob import dis
    dis.fullscreen("Working...")

    # set passphrase
    import stash
    stash.bip39_passphrase = pw

    # capture updated XFP
    with stash.SensitiveValues() as sv:
        # can't do it without original seed words (late, but caller has checked)
        assert sv.mode == 'words'

        sv.capture_xpub()
Beispiel #13
0
def sign_message_digest(digest, subpath, prompt):
    # do the signature itself!
    from glob import dis

    if prompt:
        dis.fullscreen(prompt, percent=.25)

    with stash.SensitiveValues() as sv:
        dis.progress_bar_show(.50)

        node = sv.derive_path(subpath)
        pk = node.privkey()
        sv.register(pk)

        dis.progress_bar_show(.75)
        rv = ngu.secp256k1.sign(pk, digest, 0).to_bytes()

    dis.progress_bar_show(1)

    return rv
Beispiel #14
0
    async def interact(self):
        from version import decode_firmware_header
        from sflash import SF

        date, version, _ = decode_firmware_header(self.hdr)

        msg = '''\
Install this new firmware?

  {version}
  {built}

Binary checksum and signature will be further verified before any changes are made.
'''.format(version=version, built=date)

        try:
            ch = await ux_show_story(msg)

            if ch == 'y':
                # Accepted:
                # - write final file header, so bootloader will see it
                # - reboot to start process
                from glob import dis
                import callgate
                SF.write(self.length, self.hdr)

                dis.fullscreen('Upgrading...', percent=1)

                callgate.show_logout(2)
            else:
                # they don't want to!
                self.refused = True
                SF.block_erase(0)           # just in case, but not required
                await ux_dramatic_pause("Refused.", 2)

        except BaseException as exc:
            self.failed = "Exception"
            sys.print_exception(exc)
        finally:
            UserAuthorizedAction.cleanup()      # because no results to store
            self.pop_menu()
Beispiel #15
0
def wipe_microsd_card():
    # Erase and re-format SD card. Not secure erase, because that is too slow.
    import ckcc, pyb
    from glob import dis
    
    try:
        os.umount('/sd')
    except:
        pass

    sd = pyb.SDCard()
    assert sd

    if not sd.present(): return

    # power cycle so card details (like size) are re-read from current card
    sd.power(0)
    sd.power(1)

    dis.fullscreen('Part Erase...')
    cutoff = 1024       # arbitrary
    blk = bytearray(512)

    for  bnum in range(cutoff):
        ckcc.rng_bytes(blk)
        sd.writeblocks(bnum, blk)
        dis.progress_bar_show(bnum/cutoff)

    dis.fullscreen('Formatting...')

    # remount, with newfs option
    os.mount(sd, '/sd', readonly=0, mkfs=1)

    # done, cleanup
    os.umount('/sd')

    # important: turn off power
    sd = pyb.SDCard()
    sd.power(0)
Beispiel #16
0
def wipe_flash_filesystem():
    # erase and re-format the flash filesystem (/flash/)
    import ckcc, pyb
    from glob import dis
    
    dis.fullscreen('Erasing...')
    os.umount('/flash')

    # from extmod/vfs.h
    BP_IOCTL_SEC_COUNT = (4)
    BP_IOCTL_SEC_SIZE  = (5)

    # block-level erase
    fl = pyb.Flash()
    bsize = fl.ioctl(BP_IOCTL_SEC_SIZE, 0)
    assert bsize == 512
    bcount = fl.ioctl(BP_IOCTL_SEC_COUNT, 0)

    blk = bytearray(bsize)
    ckcc.rng_bytes(blk)
    
    # trickiness: actual flash blocks are offset by 0x100 (FLASH_PART1_START_BLOCK)
    # so fake MBR can be inserted. Count also inflated by 2X, but not from ioctl above.
    for n in range(bcount):
        fl.writeblocks(n + 0x100, blk)
        ckcc.rng_bytes(blk)

        dis.progress_bar_show(n*2/bcount)
        
    # rebuild and mount /flash
    dis.fullscreen('Rebuilding...')
    ckcc.wipe_fs()

    # re-store current settings
    from nvstore import settings
    settings.save()

    # remount it
    os.mount(fl, '/flash')
Beispiel #17
0
def set_seed_value(words=None, encoded=None):
    # Save the seed words into secure element, and reboot. BIP-39 password
    # is not set at this point (empty string)
    if words:
        bip39.a2b_words(words)      # checksum check

        # map words to bip39 wordlist indices
        data = [bip39.wordlist_en.index(w) for w in words]

        # map to packed binary representation.
        val = 0
        for v in data:
            val <<= 11
            val |= v

        # remove the checksum part
        vlen = (len(words) * 4) // 3
        val >>= (len(words) // 3)

        # convert to bytes
        seed = val.to_bytes(vlen, 'big')
        assert len(seed) == vlen

        # encode it for our limited secret space
        nv = SecretStash.encode(seed_phrase=seed)
    else:
        nv = encoded

    from glob import dis
    dis.fullscreen('Applying...')
    pa.change(new_secret=nv)

    # re-read settings since key is now different
    # - also captures xfp, xpub at this point
    pa.new_main_secret(nv)

    # check and reload secret
    pa.reset()
    pa.login()
Beispiel #18
0
async def make_bitcoin_core_wallet(account_num=0,
                                   fname_pattern='bitcoin-core.txt'):
    from glob import dis
    import ustruct
    xfp = xfp2str(settings.get('xfp'))

    dis.fullscreen('Generating...')

    # make the data
    examples = []
    payload = ujson.dumps(
        list(generate_bitcoin_core_wallet(examples, account_num)))

    body = '''\
# Bitcoin Core Wallet Import File

https://github.com/Coldcard/firmware/blob/master/docs/bitcoin-core-usage.md

## For wallet with master key fingerprint: {xfp}

Wallet operates on blockchain: {nb}

## Bitcoin Core RPC

The following command can be entered after opening Window -> Console
in Bitcoin Core, or using bitcoin-cli:

importmulti '{payload}'

## Resulting Addresses (first 3)

'''.format(payload=payload, xfp=xfp, nb=chains.current_chain().name)

    body += '\n'.join('%s => %s' % t for t in examples)

    body += '\n'

    await write_text_file(fname_pattern, body, 'Bitcoin Core')
Beispiel #19
0
async def remember_bip39_passphrase():
    # Compute current xprv and switch to using that as root secret.
    import stash
    from glob import dis

    dis.fullscreen('Check...')

    with stash.SensitiveValues() as sv:
        nv = SecretStash.encode(xprv=sv.node)

    # Important: won't write new XFP to nvram if pw still set
    stash.bip39_passphrase = ''

    dis.fullscreen('Saving...')
    pa.change(new_secret=nv)

    # re-read settings since key is now different
    # - also captures xfp, xpub at this point
    pa.new_main_secret(nv)

    # check and reload secret
    pa.reset()
    pa.login()
Beispiel #20
0
async def make_address_summary_file(path, addr_fmt, ms_wallet, account_num, count=250):
    # write addresses into a text file on the MicroSD
    from glob import dis
    from files import CardSlot, CardMissingError
    from actions import needs_microsd

    # simple: always set number of addresses.
    # - takes 60 seconds to write 250 addresses on actual hardware

    dis.fullscreen('Saving 0-%d' % count)
    fname_pattern='addresses.csv'

    # generator function
    body = generate_address_csv(path, addr_fmt, ms_wallet, account_num, count)

    # pick filename and write
    try:
        with CardSlot() as card:
            fname, nice = card.pick_filename(fname_pattern)

            # do actual write
            with open(fname, 'wb') as fd:
                for idx, part in enumerate(body):
                    fd.write(part.encode())

                    if idx % 5 == 0:
                        dis.progress_bar_show(idx / count)

    except CardMissingError:
        await needs_microsd()
        return
    except Exception as e:
        await ux_show_story('Failed to write!\n\n\n'+str(e))
        return

    msg = '''Address summary file written:\n\n%s''' % nice
    await ux_show_story(msg)
Beispiel #21
0
def clear_seed():
    from glob import dis
    import utime, version

    dis.fullscreen('Clearing...')

    # clear settings associated with this key, since it will be no more
    settings.blank()

    # save a blank secret (all zeros is a special case, detected by bootloader)
    nv = bytes(AE_SECRET_LEN)
    pa.change(new_secret=nv)

    if version.has_608:
        # wipe the long secret too
        nv = bytes(AE_LONG_SECRET_LEN)
        pa.ls_change(nv)

    dis.fullscreen('Reboot...')
    utime.sleep(1)

    # security: need to reboot to really be sure to clear the secrets from main memory.
    from machine import reset
    reset()
Beispiel #22
0
    async def doit(self, *a, have_key=None):
        # make the wallet.
        from glob import dis

        try:
            import ngu
            from chains import current_chain
            from serializations import hash160
            from stash import blank_object

            if not have_key:
                # get some random bytes
                await ux_dramatic_pause("Picking key...", 2)
                pair = ngu.secp256k1.keypair()
            else:
                # caller must range check this already: 0 < privkey < order
                # - actually libsecp256k1 will check it again anyway
                pair = ngu.secp256k1.keypair(have_key)

            # pull out binary versions (serialized) as we need
            privkey = pair.privkey()
            pubkey = pair.pubkey().to_bytes(False)       # always compressed style

            dis.fullscreen("Rendering...")

            # make payment address
            digest = hash160(pubkey)
            ch = current_chain()
            if self.is_segwit:
                addr = ngu.codecs.segwit_encode(ch.bech32_hrp, 0, digest)
            else:
                addr = ngu.codecs.b58_encode(ch.b58_addr + digest)

            wif = ngu.codecs.b58_encode(ch.b58_privkey + privkey + b'\x01')

            if self.can_do_qr():
                with imported('uqr') as uqr:
                    # make the QR's now, since it's slow
                    is_alnum = self.is_segwit 
                    qr_addr = uqr.make(addr if not is_alnum else addr.upper(), 
                                min_version=4, max_version=4,
                                encoding=(uqr.Mode_ALPHANUMERIC if is_alnum else 0))

                    qr_wif = uqr.make(wif, min_version=4, max_version=4, encoding=uqr.Mode_BYTE)
            else:
                qr_addr = None
                qr_wif = None

            # Use address as filename. clearly will be unique, but perhaps a bit
            # awkward to work with.
            basename = addr

            dis.fullscreen("Saving...")
            with CardSlot() as card:
                fname, nice_txt = card.pick_filename(basename + 
                                        ('-note.txt' if self.template_fn else '.txt'))

                with open(fname, 'wt') as fp:
                    self.make_txt(fp, addr, wif, privkey, qr_addr, qr_wif)

                if self.template_fn:
                    fname, nice_pdf = card.pick_filename(basename + '.pdf')

                    with open(fname, 'wb') as fp:
                        self.make_pdf(fp, addr, wif, qr_addr, qr_wif)
                else:
                    nice_pdf = ''

            # Half-hearted attempt to cleanup secrets-contaminated memory
            # - better would be force user to reboot
            # - and yet, we just output the WIF to SDCard anyway
            blank_object(privkey)
            blank_object(wif)
            del qr_wif

        except CardMissingError:
            await needs_microsd()
            return
        except Exception as e:
            await ux_show_story('Failed to write!\n\n\n'+str(e))
            return

        await ux_show_story('Done! Created file(s):\n\n%s\n\n%s' % (nice_txt, nice_pdf))
Beispiel #23
0
async def restore_from_dict(vals):
    # Restore from a dict of values. Already JSON decoded.
    # Reboot on success, return string on failure
    from glob import dis

    #print("Restoring from: %r" % vals)

    # step1: the private key
    # - prefer raw_secret over other values
    # - TODO: fail back to other values
    try:
        chain = chains.get_chain(vals.get('chain', 'BTC'))

        assert 'raw_secret' in vals
        raw = bytearray(AE_SECRET_LEN)
        rs = vals.pop('raw_secret')
        if len(rs) % 2:
            rs += '0'
        x = a2b_hex(rs)
        raw[0:len(x)] = x

        # 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 = chain.serialize_private(node)
            assert check_xprv == vals['xprv'], 'xprv mismatch'

    except Exception as e:
        return ('Unable to decode raw_secret and '
                'restore the seed value!\n\n\n' + str(e))

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

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

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

    # force the right chain
    pa.new_main_secret(raw, chain)  # updates xfp/xpub

    # NOTE: don't fail after this point... they can muddle thru w/ just right seed

    if ls is not None:
        try:
            pa.ls_change(ls)
        except Exception as exc:
            sys.print_exception(exc)
            # but keep going

    # restore settings from backup file

    for idx, k in enumerate(vals):
        dis.progress_bar_show(idx / len(vals))
        if not k.startswith('setting.'):
            continue

        if k == 'xfp' or k == 'xpub': continue

        settings.set(k[8:], vals[k])

    # write out
    settings.save()

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

    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()
Beispiel #24
0
    async def handle_upload(self, offset, total_size, data):
        from sflash import SF
        from glob import dis, hsm_active
        from utils import check_firmware_hdr
        from sigheader import FW_HEADER_OFFSET, FW_HEADER_SIZE, FW_HEADER_MAGIC

        # maintain a running SHA256 over what's received
        if offset == 0:
            self.file_checksum = sha256()
            self.is_fw_upgrade = False

        assert offset % 256 == 0, 'alignment'
        assert offset + len(data) <= total_size <= MAX_UPLOAD_LEN, 'long'

        if hsm_active:
            # additional restrictions in HSM mode
            assert offset + len(data) <= total_size <= MAX_TXN_LEN, 'psbt'
            if offset == 0:
                assert data[0:5] == b'psbt\xff', 'psbt'

        for pos in range(offset, offset + len(data), 256):
            if pos % 4096 == 0:
                # erase here
                dis.fullscreen("Receiving...", offset / total_size)

                SF.sector_erase(pos)

                # expect 10-22 ms delay here
                await sleep_ms(12)
                while SF.is_busy():
                    await sleep_ms(2)

            # write up to 256 bytes
            here = data[pos - offset:pos - offset + 256]
            self.file_checksum.update(here)

            # Very special case for firmware upgrades: intercept and modify
            # header contents on the fly, and also fail faster if wouldn't work
            # on this specific hardware.
            # - workaround: ckcc-protocol upgrade process understates the file
            #   length and appends hdr, but that's kinda a bug, so support both
            is_trailer = (pos == (total_size - FW_HEADER_SIZE)
                          or pos == total_size)

            if pos == (FW_HEADER_OFFSET & ~255):
                hdr = memoryview(here)[-128:]
                magic, = unpack_from('<I', hdr[0:4])
                if magic == FW_HEADER_MAGIC:
                    self.is_fw_upgrade = bytes(hdr)

                    prob = check_firmware_hdr(hdr, total_size)
                    if prob:
                        raise ValueError(prob)

            if is_trailer and self.is_fw_upgrade:
                # expect the trailer to exactly match the original one
                assert len(here) == 128  # == FW_HEADER_SIZE
                hdr = memoryview(here)[-128:]
                assert hdr == self.is_fw_upgrade  # indicates hacking

                # but don't write it, instead offer user a chance to abort
                from auth import authorize_upgrade
                authorize_upgrade(self.is_fw_upgrade, pos)

                # pretend we wrote it, so ckcc-protocol or whatever gives normal feedback
                return offset

            SF.write(pos, here)

            # full page write: 0.6 to 3ms
            while SF.is_busy():
                await sleep_ms(1)

        if offset + len(data) >= total_size and not hsm_active:
            # probably done
            dis.progress_bar_show(1.0)
            ux.restore_menu()

        return offset
Beispiel #25
0
def drv_entro_step2(_1, picked, _2):
    from glob import dis
    from files import CardSlot, CardMissingError

    the_ux.pop()

    index = await ux_enter_number("Index Number?", 9999)

    if picked in (0,1,2):
        # BIP-39 seed phrases (we only support English)
        num_words = (12, 18, 24)[picked]
        width = (16, 24, 32)[picked]        # of bytes
        path = "m/83696968'/39'/0'/{num_words}'/{index}'".format(num_words=num_words, index=index)
        s_mode = 'words'
    elif picked == 3:
        # HDSeed for Bitcoin Core: but really a WIF of a private key, can be used anywhere
        s_mode = 'wif'
        path = "m/83696968'/2'/{index}'".format(index=index)
        width = 32
    elif picked == 4:
        # New XPRV
        path = "m/83696968'/32'/{index}'".format(index=index)
        s_mode = 'xprv'
        width = 64
    elif picked in (5, 6):
        width = 32 if picked == 5 else 64
        path = "m/83696968'/128169'/{width}'/{index}'".format(width=width, index=index)
        s_mode = 'hex'
    else:
        raise ValueError(picked)

    dis.fullscreen("Working...")
    encoded = None

    with stash.SensitiveValues() as sv:
        node = sv.derive_path(path)
        entropy = ngu.hmac.hmac_sha512(b'bip-entropy-from-k', node.privkey())
    
        sv.register(entropy)

        # truncate for this application
        new_secret = entropy[0:width]
            

    # only "new_secret" is interesting past here (node already blanked at this point)
    del node

    # Reveal to user!
    chain = chains.current_chain()

    if s_mode == 'words':
        # BIP-39 seed phrase, various lengths
        words = bip39.b2a_words(new_secret).split(' ')

        msg = 'Seed words (%d):\n' % len(words)
        msg += '\n'.join('%2d: %s' % (i+1, w) for i,w in enumerate(words))

        encoded = stash.SecretStash.encode(seed_phrase=new_secret)

    elif s_mode == 'wif':
        # for Bitcoin Core: a 32-byte of secret exponent, base58 w/ prefix 0x80
        # - always "compressed", so has suffix of 0x01 (inside base58)
        # - we're not checking it's on curve
        # - we have no way to represent this internally, since we rely on bip32

        # append 0x01 to indicate it's a compressed private key
        pk = new_secret + b'\x01'

        msg = 'WIF (privkey):\n' + ngu.codecs.b58_encode(chain.b58_privkey + pk)

    elif s_mode == 'xprv':
        # Raw XPRV value.
        ch, pk = new_secret[0:32], new_secret[32:64]
        master_node = ngu.hdnode.HDNode().from_chaincode_privkey(ch, pk)

        encoded = stash.SecretStash.encode(xprv=master_node)
        
        msg = 'Derived XPRV:\n' + chain.serialize_private(master_node)

    elif s_mode == 'hex':
        # Random hex number for whatever purpose
        msg = ('Hex (%d bytes):\n' % width) + str(b2a_hex(new_secret), 'ascii')

        stash.blank_object(new_secret)
        new_secret = None       # no need to print it again
    else:
        raise ValueError(s_mode)

    msg += '\n\nPath Used (index=%d):\n  %s' % (index, path)

    if new_secret:
        msg += '\n\nRaw Entropy:\n' + str(b2a_hex(new_secret), 'ascii')

    #print(msg)      # XXX debug

    prompt = '\n\nPress 1 to save to MicroSD card'
    if encoded is not None:
        prompt += ', 2 to switch to derived secret.'

    while 1:
        ch = await ux_show_story(msg+prompt, sensitive=True, escape='12')

        if ch == '1':
            # write to SD card: simple text file
            try:
                with CardSlot() as card:
                    fname, out_fn = card.pick_filename('drv-%s-idx%d.txt' % (s_mode, index))

                    with open(fname, 'wt') as fp:
                        fp.write(msg)
                        fp.write('\n')
            except CardMissingError:
                await needs_microsd()
                continue
            except Exception as e:
                await ux_show_story('Failed to write!\n\n\n'+str(e))
                continue

            await ux_show_story("Filename is:\n\n%s" % out_fn, title='Saved')
        else:
            break

    if new_secret is not None:
        stash.blank_object(new_secret)
    stash.blank_object(msg)

    if ch == '2' and (encoded is not None):
        from glob import dis
        from pincodes import pa

        # switch over to new secret!
        dis.fullscreen("Applying...")

        pa.tmp_secret(encoded)

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

    if encoded is not None:
        stash.blank_object(encoded)
Beispiel #26
0
async def restore_complete_doit(fname_or_fd, words, file_cleanup=None):
    # 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 glob import dis
    from files import CardSlot, CardMissingError
    from actions import needs_microsd

    # build password
    password = '******'.join(words)

    prob = None

    try:
        with CardSlot() as card:
            # filename already picked, taste it and maybe consider using its data.
            try:
                fd = open(fname_or_fd, 'rb') if isinstance(
                    fname_or_fd, str) else fname_or_fd
            except:
                return 'Unable to open backup file.\n\n' + str(fname_or_fd)

            try:
                if not words:
                    contents = fd.read()
                else:
                    try:
                        compat7z.check_file_headers(fd)
                    except Exception as e:
                        return 'Unable to read backup file. Has it been touched?\n\nError: ' \
                                            + str(e)

                    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'

                    except Exception as e:
                        # assume everything here is "password wrong" errors
                        #print("pw wrong?  %s" % e)

                        return (
                            'Unable to decrypt backup file. Incorrect password?'
                            '\n\nTried:\n\n' + password)
            finally:
                fd.close()

                if file_cleanup:
                    file_cleanup(fname_or_fd)

    except CardMissingError:
        await needs_microsd()
        return

    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!

    # this leads to reboot if it works, else errors shown, etc.
    return await restore_from_dict(vals)
Beispiel #27
0
def sign_psbt_file(filename):
    # sign a PSBT file found on a MicroSD card
    from files import CardSlot, CardMissingError, securely_blank_file
    from glob import dis
    from sram2 import tmp_buf
    from utils import HexStreamer, Base64Streamer, HexWriter, Base64Writer

    UserAuthorizedAction.cleanup()

    #print("sign: %s" % filename)


    # copy file into our spiflash
    # - can't work in-place on the card because we want to support writing out to different card
    # - accepts hex or base64 encoding, but binary prefered
    with CardSlot() as card:
        with open(filename, 'rb') as fd:
            dis.fullscreen('Reading...')

            # see how long it is
            psbt_len = fd.seek(0, 2)
            fd.seek(0)

            # determine encoding used, altho we prefer binary
            taste = fd.read(10)
            fd.seek(0)

            if taste[0:5] == b'psbt\xff':
                decoder = None
                output_encoder = lambda x: x
            elif taste[0:10] == b'70736274ff':
                decoder = HexStreamer()
                output_encoder = HexWriter
                psbt_len //= 2
            elif taste[0:6] == b'cHNidP':
                decoder = Base64Streamer()
                output_encoder = Base64Writer
                psbt_len = (psbt_len * 3 // 4) + 10

            total = 0
            with SFFile(TXN_INPUT_OFFSET, max_size=psbt_len) as out:
                # blank flash
                await out.erase()

                while 1:
                    n = fd.readinto(tmp_buf)
                    if not n: break

                    if n == len(tmp_buf):
                        abuf = tmp_buf
                    else:
                        abuf = memoryview(tmp_buf)[0:n]

                    if not decoder:
                        out.write(abuf)
                        total += n
                    else:
                        for here in decoder.more(abuf):
                            out.write(here)
                            total += len(here)

                    dis.progress_bar_show(total / psbt_len)

            # might have been whitespace inflating initial estimate of PSBT size
            assert total <= psbt_len
            psbt_len = total

    async def done(psbt):
        orig_path, basename = filename.rsplit('/', 1)
        orig_path += '/'
        base = basename.rsplit('.', 1)[0]
        out2_fn = None
        out_fn = None
        txid = None

        from nvstore import settings
        import os
        del_after = settings.get('del', 0)

        while 1:
            # try to put back into same spot, but also do top-of-card
            is_comp = psbt.is_complete()
            if not is_comp:
                # keep the filename under control during multiple passes
                target_fname = base.replace('-part', '')+'-part.psbt'
            else:
                # add -signed to end. We won't offer to sign again.
                target_fname = base+'-signed.psbt'

            for path in [orig_path, None]:
                try:
                    with CardSlot() as card:
                        out_full, out_fn = card.pick_filename(target_fname, path)
                        out_path = path
                        if out_full: break
                except CardMissingError:
                    prob = 'Missing card.\n\n'
                    out_fn = None

            if not out_fn: 
                # need them to insert a card
                prob = ''
            else:
                # attempt write-out
                try:
                    with CardSlot() as card:
                        if is_comp and del_after:
                            # don't write signed PSBT if we'd just delete it anyway
                            out_fn = None
                        else:
                            with output_encoder(open(out_full, 'wb')) as fd:
                                # save as updated PSBT
                                psbt.serialize(fd)

                        if is_comp:
                            # write out as hex too, if it's final
                            out2_full, out2_fn = card.pick_filename(
                                base+'-final.txn' if not del_after else 'tmp.txn', out_path)

                            if out2_full:
                                with HexWriter(open(out2_full, 'w+t')) as fd:
                                    # save transaction, in hex
                                    txid = psbt.finalize(fd)

                                if del_after:
                                    # rename it now that we know the txid
                                    after_full, out2_fn = card.pick_filename(
                                                            txid+'.txn', out_path, overwrite=True)
                                    os.rename(out2_full, after_full)

                    if del_after:
                        # this can do nothing if they swapped SDCard between steps, which is ok,
                        # but if the original file is still there, this blows it away.
                        # - if not yet final, the foo-part.psbt file stays
                        try:
                            securely_blank_file(filename)
                        except: pass

                    # success and done!
                    break

                except OSError as exc:
                    prob = 'Failed to write!\n\n%s\n\n' % exc
                    sys.print_exception(exc)
                    # fall thru to try again

            # prompt them to input another card?
            ch = await ux_show_story(prob+"Please insert an SDCard to receive signed transaction, "
                                        "and press OK.", title="Need Card")
            if ch == 'x':
                await ux_aborted()
                return

        # done.
        if out_fn:
            msg = "Updated PSBT is:\n\n%s" % out_fn
            if out2_fn:
                msg += '\n\n'
        else:
            # del_after is probably set
            msg = ''

        if out2_fn:
            msg += 'Finalized transaction (ready for broadcast):\n\n%s' % out2_fn
            if txid and not del_after:
                msg += '\n\nFinal TXID:\n'+txid

        await ux_show_story(msg, title='PSBT Signed')

        UserAuthorizedAction.cleanup()

    UserAuthorizedAction.active_request = ApproveTransaction(psbt_len, approved_cb=done)

    # kill any menu stack, and put our thing at the top
    abort_and_goto(UserAuthorizedAction.active_request)
Beispiel #28
0
 def __enter__(self):
     if self.message:
         from glob import dis
         dis.fullscreen(self.message)
     return self
Beispiel #29
0
    async def interact(self):
        # Prompt user w/ details and get approval
        from glob import dis, hsm_active

        # step 1: parse PSBT from sflash into in-memory objects.

        try:
            dis.fullscreen("Reading...")
            with SFFile(TXN_INPUT_OFFSET, length=self.psbt_len) as fd:
                self.psbt = psbtObject.read_psbt(fd)
        except BaseException as exc:
            if isinstance(exc, MemoryError):
                msg = "Transaction is too complex"
                exc = None
            else:
                msg = "PSBT parse failed"

            return await self.failure(msg, exc)

        dis.fullscreen("Validating...")

        # Do some analysis/ validation
        try:
            await self.psbt.validate()      # might do UX: accept multisig import
            self.psbt.consider_inputs()
            self.psbt.consider_keys()
            self.psbt.consider_outputs()
        except FraudulentChangeOutput as exc:
            print('FraudulentChangeOutput: ' + exc.args[0])
            return await self.failure(exc.args[0], title='Change Fraud')
        except FatalPSBTIssue as exc:
            print('FatalPSBTIssue: ' + exc.args[0])
            return await self.failure(exc.args[0])
        except BaseException as exc:
            del self.psbt
            gc.collect()

            if isinstance(exc, MemoryError):
                msg = "Transaction is too complex"
                exc = None
            else:
                msg = "Invalid PSBT"

            return await self.failure(msg, exc)

        # step 2: figure out what we are approving, so we can get sign-off
        # - outputs, amounts
        # - fee 
        #
        # notes: 
        # - try to handle lots of outputs
        # - cannot calc fee as sat/byte, only as percent
        # - somethings are 'warnings':
        #       - fee too big
        #       - inputs we can't sign (no key)
        #
        try:
            msg = uio.StringIO()

            # mention warning at top
            wl= len(self.psbt.warnings)
            if wl == 1:
                msg.write('(1 warning below)\n\n')
            elif wl >= 2:
                msg.write('(%d warnings below)\n\n' % wl)

            self.output_summary_text(msg)
            gc.collect()

            fee = self.psbt.calculate_fee()
            if fee is not None:
                msg.write("\nNetwork fee:\n%s %s\n" % self.chain.render_value(fee))

            # NEW: show where all the change outputs are going
            self.output_change_text(msg)
            gc.collect()

            if self.psbt.warnings:
                msg.write('\n---WARNING---\n\n')

                for label, m in self.psbt.warnings:
                    msg.write('- %s: %s\n\n' % (label, m))

            if self.do_visualize:
                # stop here and just return the text of approval message itself
                self.result = await self.save_visualization(msg, (self.stxn_flags & STXN_SIGNED))
                del self.psbt
                self.done()

                return

            if not hsm_active:
                msg.write("\nPress OK to approve and sign transaction. X to abort.")
                ch = await ux_show_story(msg, title="OK TO SEND?")
            else:
                ch = await hsm_active.approve_transaction(self.psbt, self.psbt_sha, msg.getvalue())
                dis.progress_bar(1)     # finish the Validating...

        except MemoryError:
            # recovery? maybe.
            try:
                del self.psbt
                del msg
            except: pass        # might be NameError since we don't know how far we got
            gc.collect()

            msg = "Transaction is too complex"
            return await self.failure(msg)

        if ch != 'y':
            # they don't want to!
            self.refused = True

            await ux_dramatic_pause("Refused.", 1)

            del self.psbt

            self.done()
            return

        # do the actual signing.
        try:
            dis.fullscreen('Wait...')
            gc.collect()           # visible delay causes by this but also sign_it() below
            self.psbt.sign_it()
        except FraudulentChangeOutput as exc:
            return await self.failure(exc.args[0], title='Change Fraud')
        except MemoryError:
            msg = "Transaction is too complex"
            return await self.failure(msg)
        except BaseException as exc:
            return await self.failure("Signing failed late", exc)

        if self.approved_cb:
            # for micro sd case
            await self.approved_cb(self.psbt)
            self.done()
            return

        txid = None
        try:
            # re-serialize the PSBT back out
            with SFFile(TXN_OUTPUT_OFFSET, max_size=MAX_TXN_LEN, message="Saving...") as fd:
                await fd.erase()

                if self.do_finalize:
                    txid = self.psbt.finalize(fd)
                else:
                    self.psbt.serialize(fd)

                self.result = (fd.tell(), fd.checksum.digest())

            self.done(redraw=(not txid))

        except BaseException as exc:
            return await self.failure("PSBT output failed", exc)

        if self.do_finalize and txid and not hsm_active:
            # Show txid when we can; advisory
            # - maybe even as QR, hex-encoded in alnum mode
            tmsg = txid

            if version.has_fatram:
                tmsg += '\n\nPress 1 for QR Code of TXID.'

            ch = await ux_show_story(tmsg, "Final TXID", escape='1')

            if version.has_fatram and ch=='1':
                await show_qr_code(txid, True)
Beispiel #30
0
async def write_complete_backup(words,
                                fname_pattern,
                                write_sflash=False,
                                allow_copies=True):
    # Just do the writing
    from glob import dis
    from files import CardSlot, CardMissingError

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

    gc.collect()

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

        pw = ' '.join(words)
        zz = compat7z.Builder(password=pw, 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)

        filesize = len(body) + MAX_BACKUP_FILE_SIZE

        del body

        gc.collect()
    else:
        # cleartext dump
        zz = None
        filesize = len(body) + 10

    if write_sflash:
        # for use over USB and unit testing: commit file into SPI flash
        from sffile import SFFile

        with SFFile(0, max_size=filesize, message='Saving...') as fd:
            await fd.erase()

            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 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
            import sys
            sys.print_exception(e)
            # catch any error
            ch = await ux_show_story(
                'Failed to write! Please insert formated MicroSD card, '
                'and press OK to try again.\n\nX to cancel.\n\n\n' + str(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 == 'y': return
                if ch == '2': break

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