# (c) Copyright 2021 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# web2fa.py -- Bounce a shared secret off a Coinkite server to allow mobile app 2FA.
#
#
import ngu, ndef, aes256ctr
from utils import b2a_base64url, url_quote, B2A
from version import has_qr
from ux import show_qr_code, ux_show_story, X

# Only Coldcard.com server knows private key for this pubkey. It protects
# the privacy of the values we send to the server.
#
# = 0231301ec4acec08c1c7d0181f4ffb8be70d693acccc86cccb8f00bf2e00fcabfd
SERVER_PUBKEY = b'\x02\x31\x30\x1e\xc4\xac\xec\x08\xc1\xc7\xd0\x18\x1f\x4f\xfb\x8b\xe7\x0d\x69\x3a\xcc\xcc\x86\xcc\xcb\x8f\x00\xbf\x2e\x00\xfc\xab\xfd'

def encrypt_details(qs):
    # encryption and base64 here
    # - pick single-use ephemeral secp256k1 keypair
    # - do ECDH to generate a shared secret based on known pubkey of server
    # - AES-256-CTR encryption based on that
    # - base64url encode result

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

    session_key = pair.ecdh_multiply(SERVER_PUBKEY)
    del pair

    enc = aes256ctr.new(session_key).cipher

    return b2a_base64url(my_pubkey + enc(qs.encode('ascii')))

async def perform_web2fa(label, shared_secret):

    # send them to web, prompt for valid response. Return True if it all worked.
    expect = await nfc_share_2fa_link(label, shared_secret)
    if not expect:
        # aborted at NFC step
        return False

    if has_qr:
        # Make them scan the result, for example:
        # 
        #   CCC-AUTH:E902B3DAF2D98040F3A5F556D7CCC7C22BF3D455C146C4D4C0F7CF8B7937C530
        #
        from ux_q1 import QRScannerInteraction
        from exceptions import QRDecodeExplained

        prefix = 'CCC-AUTH:'
        scanner = QRScannerInteraction()

        def validate(got):
            if not got.startswith(prefix):
                raise QRDecodeExplained("QR isn't from our site")
            if got != prefix+expect:
                # probably attempted replay
                raise QRDecodeExplained("Incorrect code?")
            return got

        data = await scanner.scan_general('Scan QR shown from Web', validate)
        if not data:
            return False    # pressed cancel

        # only one legal response possible, and already validated above
        return data == (prefix+expect)

    else:
        #
        # Mk4 and other devices w/o QR scanner, require user to enter 8 digits
        #
        from ux_mk4 import ux_input_digits

        while 1:
            got = await ux_input_digits('', maxlen=8,
                                        prompt="8-digits From Web")

            if not got:
                # abort if empty entry
                return False

            if got == expect:
                # good match
                return True

            ch = await ux_show_story("You entered an incorrect code. You must"
                                     " enter the digits shown after the correct"
                                     " 2FA code is provided to the website."
                                     " Try again or %s to stop." % X)
            if ch == 'x':
                return False

    # not reached
    return False
    

async def web2fa_enroll(ss=None):
    #
    # Enroll: Pick a secret and test they have loaded it into their phone.
    #

    # must have NFC tho
    from flow import feature_requires_nfc
    if not await feature_requires_nfc():
        # they don't want to proceed
        return None

    # Pick a shared secret; 10 bytes, so encodes to 16 base32 chars
    ss = ss or ngu.codecs.b32_encode(ngu.random.bytes(10))

    # show a QR that app know how to use
    # - problem: on Mk4, not really enough space:
    #  - can only show up to 42 chars, and secret is 16, required overhead is 23 => 39 min
    #  - can't fit any metadata, like username or our serial # in there
    # - better on Q1 where no limitations for this size of QR

    nm = 'COLDCARD' if has_qr else 'CC'     # must be url-safe
    qr = 'otpauth://totp/{nm}?secret={ss}'.format(ss=ss, nm=nm)

    while 1:
        # show QR for enroll
        await show_qr_code(qr, is_alnum=False, msg="Import into 2FA Mobile App",
                           force_msg=True)

        # important: force them to prove they stored it correctly
        ok = await perform_web2fa('Enroll: COLDCARD', ss)
        if ok: break

        ch = await ux_show_story("That isn't correct. Please re-import and/or "
                                 "try again or %s to give up." % X)
        if ch == 'x':
            return None

    return ss

def make_web2fa_url(wallet_name, shared_secret):
    # Build complex URL into our server w/ encrypted data
    # - picking a nonce in the process
    prefix = 'coldcard.com/2fa?'

    # random nonce: if we get this back, then server approves of TOTP answer
    if has_qr:
        # data for a QR
        nonce = B2A(ngu.random.bytes(32)).upper()
    else:
        # 8 digits for human entry
        nonce = '%08d' % ngu.random.uniform(1_0000_0000)

    # compose URL
    qs = 'g=%s&ss=%s&nm=%s&q=%d' % (nonce, shared_secret, url_quote(wallet_name), has_qr)

    # encrypt that
    qs = encrypt_details(qs)

    return nonce, prefix + qs

async def nfc_share_2fa_link(wallet_name, shared_secret):
    #
    # Share complex NFC deeplink into 2fa backend; returns expected response-code.
    # Next step is to prompt for that 8-digit code (mk4) or scan QR (Q)
    #
    from glob import NFC
    assert NFC

    nonce, url = make_web2fa_url(wallet_name, shared_secret)

    n = ndef.ndefMaker()
    n.add_url(url, https=True)

    aborted = await NFC.share_start(n, prompt="Tap for 2FA Authentication",
                                    line2="Wallet: " + wallet_name)

    return None if aborted else nonce

# EOF
