예제 #1
0
파일: main.py 프로젝트: lucasmoten/ckbunker
async def startup(setup_mode, force_local_mode, config_file, first_psbt):
    # All startup/operation code

    loop = asyncio.get_running_loop()
    if loop.get_debug():
        # quiet noise about slow stuff
        loop.slow_callback_duration = 10

    from utils import setup_logging
    setup_logging()

    from persist import Settings
    Settings.startup(config_file)

    aws = []

    # copy some args into status area
    from status import STATUS
    STATUS.force_local_mode = force_local_mode
    STATUS.setup_mode = setup_mode

    # preload the contents of a PSBT
    if first_psbt:
        STATUS.import_psbt(first_psbt)

    from torsion import TOR
    aws.append(TOR.startup())

    from conn import Connection
    aws.append(Connection(force_serial).run())

    import webapp
    aws.append(webapp.startup(setup_mode))

    await asyncio.gather(*aws)
예제 #2
0
파일: conn.py 프로젝트: lucasmoten/ckbunker
    async def hsm_status(self, h=None):
        # refresh HSM status
        b4 = STATUS.hsm.get('active', False)

        try:
            b4_nlc = STATUS.hsm.get('next_local_code')
            h = h or (await self.send_recv(CCProtocolPacker.hsm_status()))
            STATUS.hsm = h = json_loads(h)
            STATUS.notify_watchers()
        except MissingColdcard:
            h = {}

        if h.get('next_local_code') and STATUS.psbt_hash:
            if b4_nlc != h.next_local_code:
                STATUS.local_code = calc_local_pincode(
                    a2b_hex(STATUS.psbt_hash), h.next_local_code)
        else:
            # won't be required
            STATUS.local_code = None

        # has it just transitioned into HSM mode?
        if STATUS.connected and STATUS.hsm.active and not b4:
            await self.activated_hsm()

        return STATUS.hsm
예제 #3
0
    async def connect(self, raise_on_error=True):
        from stem.connection import connect

        if self.controller:
            return self.controller

        def doit():
            self.controller = connect(control_port=('127.0.0.1',
                                                    settings.TORD_PORT))

            if self.controller:
                logging.info("Tord version: " +
                             str(self.controller.get_version()))
            else:
                logging.error("Unable to connect to local 'tord' server")
                if raise_on_error:
                    raise RuntimeError("No local 'tord' server")

            return self.controller

        loop = asyncio.get_running_loop()
        rv = await loop.run_in_executor(executor, doit)

        STATUS.tord_good = bool(rv)
        STATUS.notify_watchers()
예제 #4
0
    async def hsm_status(self, h=None):
        # refresh HSM status
        b4 = STATUS.hsm.get('active', False)

        try:
            b4_nlc = STATUS.hsm.get('next_local_code')
            h = h or (await self.send_recv(CCProtocolPacker.hsm_status()))
            STATUS.hsm = h = json_loads(h)
            STATUS.notify_watchers()
        except MissingColdcard:
            h = {}

        if h.get('next_local_code') and STATUS.psbt_hash:
            if b4_nlc != h.next_local_code:
                STATUS.local_code = calc_local_pincode(
                    a2b_hex(STATUS.psbt_hash), h.next_local_code)
        else:
            # won't be required
            STATUS.local_code = None

        if ('summary' in h) and h.summary and not BP.get(
                'priv_over_ux') and not BP.get('summary'):
            logging.info("Captured CC's summary of the policy")
            BP['summary'] = h.summary
            BP.save()

        # has it just transitioned into HSM mode?
        if STATUS.connected and STATUS.hsm.active and not b4:
            await self.activated_hsm()

        return STATUS.hsm
예제 #5
0
파일: conn.py 프로젝트: lucasmoten/ckbunker
    async def run(self):
        # connect to, and maintain a connection to a single Coldcard

        logging.info("Connecting to Coldcard.")

        while 1:
            try:
                if not self.serial and os.path.exists(settings.SIMULATOR_SOCK):
                    # if simulator is running, just use it.
                    sn = settings.SIMULATOR_SOCK
                else:
                    sn = self.serial

                d = ColdcardDevice(sn=sn)
                logging.info(f"Found Coldcard {d.serial}.")

                await asyncio.get_running_loop().run_in_executor(
                    executor, d.check_mitm)

                async with self.lock:
                    self.dev = d
            except:
                logging.error("Cannot connect to Coldcard (will retry)",
                              exc_info=0)
                await asyncio.sleep(settings.RECONNECT_DELAY)
                continue

            # stay connected, and check we are working periodically
            logging.info(f"Connected to Coldcard {self.dev.serial}.")

            STATUS.connected = True

            # read static info about coldcard
            STATUS.xfp = xfp2str(self.dev.master_fingerprint)
            STATUS.serial_number = self.dev.serial
            STATUS.is_testnet = (self.dev.master_xpub[0] == 't')
            STATUS.hsm = {}
            STATUS.reset_pending_auth()
            STATUS.notify_watchers()
            await self.hsm_status()

            while 1:
                await asyncio.sleep(settings.PING_RATE)
                try:
                    # use long timeout here, even tho simple command, because the CC may
                    # we working on something else right now (thinking).
                    h = await self.send_recv(CCProtocolPacker.hsm_status(),
                                             timeout=20000)
                    logging.info("ping ok")
                    await self.hsm_status(h)
                except MissingColdcard:
                    self._conn_broken()
                    break
                except:
                    logging.error("Ping failed", exc_info=1)
예제 #6
0
파일: conn.py 프로젝트: lucasmoten/ckbunker
    async def activated_hsm(self):
        # just connected to a Coldcard w/ HSM active already
        # - ready storage locker, decrypt and use those settings
        logging.info("Coldcard now in HSM mode. Fetching storage locker.")

        try:
            sl = await self.get_storage_locker()
        except CCProtoError as exc:
            if 'consumed' in str(exc):
                import os, sys
                msg = "Coldcard refused access to storage locker. Reboot it and enter HSM again"
                logging.error(msg)
                print(msg, file=sys.stderr)
                sys.exit(1)
            else:
                raise

        try:
            import policy
            xk = policy.decode_sl(sl)
        except:
            logging.error("Unable to parse contents of storage locker: %r" %
                          sl)
            return

        if BP.open(xk):
            # unable to read our settings specific to this CC? Go to defaults
            # or continue?
            logging.error(
                "Unable to read bunker settings for this Coldcard; forging on")
        else:
            STATUS.sl_loaded = True

        if BP.get(
                'tor_enabled',
                False) and not (STATUS.force_local_mode or STATUS.setup_mode):
            # get onto Tor as a HS
            from torsion import TOR
            STATUS.tor_enabled = True
            logging.info(f"Starting hidden service: %s" % BP['onion_addr'])
            asyncio.create_task(TOR.start_tunnel())

        h = STATUS.hsm
        if ('summary' in h) and h.summary and not BP.get(
                'priv_over_ux') and not BP.get('summary'):
            logging.info("Captured CC's summary of the policy")
            BP['summary'] = h.summary
            BP.save()

        STATUS.reset_pending_auth()
        STATUS.notify_watchers()
예제 #7
0
파일: conn.py 프로젝트: lucasmoten/ckbunker
    def _conn_broken(self, setup_time=False):
        # our connection is lost, so clear/reset system state
        if self.dev:
            self.dev.close()
            self.dev = None

        STATUS.connected = False
        STATUS.xfp = None
        STATUS.serial_number = None
        STATUS.is_testnet = False
        STATUS.hsm = {}
        STATUS.reset_pending_auth()

        if not setup_time:
            BP.reset()

        STATUS.notify_watchers()
예제 #8
0
    async def stop_tunnel(self):
        # hang up if running
        if not self.service:
            return

        def doit():
            if self.service:
                logging.info(
                    f"Disconnecting previous service at: {self.service.service_id}.onion"
                )
                self.controller.remove_ephemeral_hidden_service(
                    self.service.service_id)
                self.service = None

        STATUS.onion_addr = None
        STATUS.notify_watchers()

        loop = asyncio.get_running_loop()
        return await loop.run_in_executor(executor, doit)
예제 #9
0
async def add_shared_ctx(request, **rv):
    # add ctx vars needed to support fancy Vue.js stuff on logged-in pages

    ses = await get_session(request)


    rv.update(dict(
        ws_url = '/websocket/' + ses.get('ws_token'),
        STATUS=STATUS.as_dict(),
    ))

    rv['CUR_PAGE'] = '/' + request.path.split('/')[-1]

    return rv
예제 #10
0
    async def start_tunnel(self):
        from persist import BP, settings

        c = await self.connect()

        def doit():
            if self.service:
                logging.info(
                    f"Disconnecting previous service at: {self.service.service_id}.onion"
                )
                self.controller.remove_ephemeral_hidden_service(
                    self.service.service_id)
                self.service = None

            # give Tor the key from earlier run
            k = BP['onion_pk']
            s = self.controller.create_ephemeral_hidden_service(
                {80: settings.PORT_NUMBER},
                detached=False,
                discard_key=True,
                await_publication=True,
                key_type='ED25519-V3',
                key_content=k)

            addr = s.service_id + '.onion'
            assert addr == BP[
                'onion_addr'], f"Mismatch, got: {addr} not {BP.onion_addr} expected"

            self.service = s

            return addr

        loop = asyncio.get_running_loop()
        addr = await loop.run_in_executor(executor, doit)

        STATUS.onion_addr = addr
        STATUS.notify_watchers()
예제 #11
0
async def push_status_updates_handler(ws):
    # block for a bit, and then send display updates (and all other system status changes)

    # - there is no need for immediate update because when we rendered the HTML on page
    #   load, we put in current values.
    await asyncio.sleep(0.250)

    last = None
    while 1:
        # get latest state
        now = STATUS.as_dict()

        if last != now:
            # it has changed, so send it.
            await ws.send_str(json_dumps(dict(vue_app_cb=dict(update_status=now))))
            last = now

        # wait until next update, or X seconds max (for keep alive/just in case)
        try:
            await asyncio.wait_for(STATUS._update_event.wait(), 120)
        except asyncio.TimeoutError:
            # force an update
            last = None
예제 #12
0
async def ws_api_handler(ses, send_json, req, orig_request):     # handle_api
    #
    # Handle incoming requests over websocket; send back results.
    # req = already json parsed request coming in
    # send_json() = means to send the response back
    #
    action = req.action
    args = getattr(req, 'args', None)

    #logging.warn("API action=%s (%r)" % (action, args))        # MAJOR info leak XXX
    logging.debug(f"API action={action}")

    if action == '_connected':
        logging.info("Websocket connected: %r" % args)

        # can send special state update at this point, depending on the page

    elif action == 'start_hsm_btn':
        await Connection().hsm_start()
        await send_json(show_flash_msg=APPROVE_CTA)
        
    elif action == 'delete_user':
        name, = args
        assert 1 <= len(name) <= MAX_USERNAME_LEN, "bad username length"
        await Connection().delete_user(name.encode('utf8'))

        # assume it worked, so UX updates right away
        try:
            STATUS.hsm.users.remove(name)
        except ValueError:
            pass
        STATUS.notify_watchers()

    elif action == 'create_user':
        name, authmode, new_pw = args

        assert 1 <= len(name) <= MAX_USERNAME_LEN, "bad username length"
        assert ',' not in name, "no commas in names"

        if authmode == 'totp':
            mode = USER_AUTH_TOTP | USER_AUTH_SHOW_QR
            new_pw = ''
        elif authmode == 'rand_pw':
            mode = USER_AUTH_HMAC | USER_AUTH_SHOW_QR
            new_pw = ''
        elif authmode == 'give_pw':
            mode = USER_AUTH_HMAC
        else:
            raise ValueError(authmode)

        await Connection().create_user(name.encode('utf8'), mode, new_pw)

        # assume it worked, so UX updates right away
        try:
            STATUS.hsm.users = list(set(STATUS.hsm.users + [name]))
        except ValueError:
            pass
        STATUS.notify_watchers()

    elif action == 'submit_policy':
        # get some JSON w/ everything the user entered.
        p, save_copy = args

        proposed = policy.web_cleanup(json_loads(p))

        policy.update_sl(proposed)

        await Connection().hsm_start(proposed)

        STATUS.notify_watchers()

        await send_json(show_flash_msg=APPROVE_CTA)

        if save_copy:
            d = policy.desensitize(proposed)
            await send_json(local_download=dict(data=json_dumps(d, indent=2),
                                filename=f'hsm-policy-{STATUS.xfp}.json.txt'))

    elif action == 'download_policy':

        proposed = policy.web_cleanup(json_loads(args[0]))
        await send_json(local_download=dict(data=json_dumps(proposed, indent=2),
                                filename=f'hsm-policy-{STATUS.xfp}.json.txt'))

    elif action == 'import_policy':
        # they are uploading a JSON capture, but need values we can load in Vue
        proposed = args[0]
        cooked = policy.web_cookup(proposed)
        await send_json(vue_app_cb=dict(update_policy=cooked),
                        show_flash_msg="Policy file imported.")

    elif action == 'pick_onion_addr':
        from torsion import TOR
        addr, pk = await TOR.pick_onion_addr()
        await send_json(vue_app_cb=dict(new_onion_addr=[addr, pk]))

    elif action == 'pick_master_pw':
        pw = b64encode(os.urandom(12)).decode('ascii')
        pw = pw.replace('/', 'S').replace('+', 'p')
        assert '=' not in pw

        await send_json(vue_app_cb=dict(new_master_pw=pw))

    elif action == 'new_bunker_config':
        from torsion import TOR
        # save and apply config values
        nv = json_loads(args[0])

        assert 4 <= len(nv.master_pw) < 200, "Master password must be at least 4 chars long"

        # copy in simple stuff
        for fn in [ 'tor_enabled', 'master_pw', 'easy_captcha', 'allow_reboots']:
            if fn in nv:
                BP[fn] = nv[fn]


        # update onion stuff only if PK is known (ie. they changed it)
        if nv.get('onion_pk', False) or False:
            for fn in [ 'onion_addr', 'onion_pk']:
                if fn in nv:
                    BP[fn] = nv[fn]

        BP.save()

        await send_json(show_flash_msg="Bunker settings encrypted and saved to disk.")

        STATUS.tor_enabled = BP['tor_enabled']
        STATUS.notify_watchers()

        if not BP['tor_enabled']:
            await TOR.stop_tunnel()
        elif BP.get('onion_pk') and not (STATUS.force_local_mode or STATUS.setup_mode) \
            and TOR.get_current_addr() != BP.get('onion_addr'):
                # disconnect/reconnect
                await TOR.start_tunnel()

    elif action == 'sign_message':
        # sign a short text message
        # - lots more checking could be done here, but CC does it anyway
        msg_text, path, addr_fmt = args

        addr_fmt = AF_P2WPKH if addr_fmt != 'classic' else AF_CLASSIC

        try:
            sig, addr = await Connection().sign_text_msg(msg_text, path, addr_fmt)
        except:
            # get the spinner to stop: error msg will be "refused by policy" typically
            await send_json(vue_app_cb=dict(msg_signing_result='(failed)'))
            raise

        sig = b64encode(sig).decode('ascii').replace('\n', '')

        await send_json(vue_app_cb=dict(msg_signing_result=f'{sig}\n{addr}'))

    elif action == 'upload_psbt':
        # receiving a PSBT for signing

        size, digest, contents = args
        psbt = b64decode(contents)
        assert len(psbt) == size, "truncated/padded in transit"
        assert sha256(psbt).hexdigest() == digest, "corrupted in transit"

        STATUS.import_psbt(psbt)
        STATUS.notify_watchers()

    elif action == 'clear_psbt':
        STATUS.clear_psbt()
        STATUS.notify_watchers()

    elif action == 'preview_psbt':
        STATUS.psbt_preview = 'Wait...'
        STATUS.notify_watchers()
        try:
            txt = await Connection().sign_psbt(STATUS._pending_psbt, flags=STXN_VISUALIZE)
            txt = txt.decode('ascii')
            # force some line splits, especially for bech32, 32-byte values (p2wsh)
            probs = re.findall(r'([a-zA-Z0-9]{36,})', txt)
            for p in probs:
                txt = txt.replace(p, p[0:30] + '\u22ef\n\u22ef' + p[30:])
            STATUS.psbt_preview = txt
        except:
            # like if CC doesn't like the keys, whatever ..
            STATUS.psbt_preview = None
            raise
        finally:
            STATUS.notify_watchers()

    elif action == 'auth_set_name':
        idx, name = args

        assert 0 <= len(name) <= MAX_USERNAME_LEN
        assert 0 <= idx < len(STATUS.pending_auth)

        STATUS.pending_auth[idx].name = name
        STATUS.notify_watchers()

    elif action == 'auth_offer_guess':
        idx, ts, guess = args
        assert 0 <= idx < len(STATUS.pending_auth)
        STATUS.pending_auth[idx].totp = ts
        STATUS.pending_auth[idx].has_guess = 'x'*len(guess)
        STATUS._auth_guess[idx] = guess
        STATUS.notify_watchers()

    elif action == 'submit_psbt':
        # they want to sign it now
        expect_hash, send_immediately, finalize, wants_dl = args

        assert expect_hash == STATUS.psbt_hash, "hash mismatch"
        if send_immediately: assert finalize, "must finalize b4 send"

        logging.info("Starting to sign...")
        STATUS.busy_signing = True
        STATUS.notify_watchers()

        try:
            dev = Connection()

            # do auth steps first (no feedback given)
            for pa, guess in zip(STATUS.pending_auth, STATUS._auth_guess):
                if pa.name and guess:
                    await dev.user_auth(pa.name, guess, int(pa.totp), a2b_hex(STATUS.psbt_hash))

            STATUS.reset_pending_auth()

            try:
                result = await dev.sign_psbt(STATUS._pending_psbt, finalize=finalize)
                logging.info("Done signing")

                msg = "Transaction signed."

                if send_immediately:
                    msg += '<br><br>' + broadcast_txn(result)

                await send_json(show_modal=True, html=Markup(msg), selector='.js-api-success')

                result = (b2a_hex(result) if finalize else b64encode(result)).decode('ascii')
                fname = 'transaction.txt' if finalize else ('signed-%s.psbt'%STATUS.psbt_hash[-6:])

                if wants_dl:
                    await send_json(local_download=dict(data=result, filename=fname,
                                                        is_b64=(not finalize)))

                await dev.hsm_status()
            except CCUserRefused:
                logging.error("Coldcard refused to sign txn")
                await dev.hsm_status()
                r = STATUS.hsm.get('last_refusal', None)
                if not r: 
                    raise HTMLErroMsg('Refused by local user.')
                else:
                    raise HTMLErrorMsg(f"Rejected by Coldcard.<br><br>{r}")

        finally:
            STATUS.busy_signing = False
            STATUS.notify_watchers()

    elif action == 'shutdown_bunker':
        await send_json(show_flash_msg="Bunker is shutdown.")
        await asyncio.sleep(0.25)
        logging.warn("User-initiated shutdown")
        asyncio.get_running_loop().stop()
        sys.exit(0)

    elif action == 'leave_setup_mode':
        # During setup process, they want to go Tor mode; which I consider leaving
        # setup mode ... in particular, logins are required.
        # - button label is "Start Tor" tho ... so user doesn't see it that way
        assert STATUS.setup_mode, 'not in setup mode?'
        assert BP['tor_enabled'], 'Tor not enabled (need to save?)'
        addr = BP['onion_addr']
        assert addr and '.onion' in addr, "Bad address?"

        STATUS.setup_mode = False
        await send_json(show_flash_msg="Tor hidden service has been enabled. "
                            "It may take a few minutes for the website to become available")
        STATUS.notify_watchers()

        from torsion import TOR
        logging.info(f"Starting hidden service: %s" % addr)
        asyncio.create_task(TOR.start_tunnel())

    elif action == 'logout_everyone':
        # useful for running battles...
        # - changes crypto key for cookies, so they are all invalid immediately.
        from aiohttp_session.nacl_storage import NaClCookieStorage
        import nacl

        logging.warning("Logout of everyone!")

        # reset all session cookies
        storage = orig_request.get('aiohttp_session_storage')
        assert isinstance(storage, NaClCookieStorage)
        storage._secretbox = nacl.secret.SecretBox(os.urandom(32))

        # kick everyone off (bonus step)
        for w in web_sockets:
            try:
                await send_json(redirect='/logout', _ws=w)
                await w.close()
            except:
                pass

    else:
        raise NotImplementedError(action)