Exemple #1
0
    def create_user_token_by_pairing_url(self, pairing_url, pin='1234'):
        """
        parses the pairing url and saves the extracted data in
        the fake token database of this test class.

        :param pairing_url: the pairing url received from the server
        :param pin: the pin of the token (default: '1234')

        :returns: user_token_id of newly created token
        """

        # extract metadata and the public key

        data_encoded = pairing_url[len(self.uri + '://pair/'):]
        data = decode_base64_urlsafe(data_encoded)
        version, token_type, flags = struct.unpack('<bbI', data[0:6])
        partition = struct.unpack('<I', data[6:10])[0]

        server_public_key = data[10:10 + 32]

        # validate protocol versions and type id

        assert token_type == TYPE_PUSHTOKEN
        assert version == PAIRING_URL_VERSION

        # ------------------------------------------------------------------ --

        # extract custom data that may or may not be present
        # (depending on flags)

        custom_data = data[10 + 32:]

        token_serial = None
        if flags & FLAG_PAIR_SERIAL:
            token_serial, __, custom_data = custom_data.partition(b'\x00')

        callback_url = None
        if flags & FLAG_PAIR_CBURL:
            callback_url, __, custom_data = custom_data.partition(b'\x00')
        else:
            raise NotImplementedError(
                'Callback URL is mandatory for PushToken')

        # ------------------------------------------------------------------ --

        # save token data for later use

        user_token_id = len(self.tokens)
        self.tokens[user_token_id] = {
            'serial': token_serial.decode(),
            'server_public_key': server_public_key,
            'partition': partition,
            'callback_url': callback_url.decode(),
            'pin': pin
        }

        # ------------------------------------------------------------------ --

        return user_token_id
    def create_user_token_by_pairing_url(pairing_url: str, pin: str) -> Dict:
        """Parses the pairing url and return the extracted token data as dict.

        :param pairing_url: the pairing url received from the server
        :param pin: the pin of the token

        :returns: token info dict
        """

        # extract metadata and the public key

        data_encoded = pairing_url[len(Push_Token_Validation.uri_schema +
                                       '://pair/'):]

        data = decode_base64_urlsafe(data_encoded)
        version, token_type, flags = struct.unpack('<bbI', data[0:6])
        partition = struct.unpack('<I', data[6:10])[0]

        server_public_key = data[10:10 + 32]

        # validate protocol versions and type id

        assert token_type == TYPE_PUSHTOKEN
        assert version == PAIRING_URL_VERSION

        # ------------------------------------------------------------------ --

        # extract custom data that may or may not be present
        # (depending on flags)

        custom_data = data[10 + 32:]

        assert flags & FLAG_PAIR_SERIAL
        token_serial, __, custom_data = custom_data.partition(b'\x00')

        callback_url = None
        if flags & FLAG_PAIR_CBURL:
            callback_url, __, custom_data = custom_data.partition(b'\x00')
        else:
            raise NotImplementedError(
                'Callback URL is mandatory for PushToken')

        # ------------------------------------------------------------------- --

        # save token data for later use

        token_info = {
            'serial': token_serial.decode(),
            'server_public_key': server_public_key,
            'partition': partition,
            'callback_url': callback_url.decode(),
            'token_id': 1,
            'pin': pin
        }

        return token_info
Exemple #3
0
    def checkOtp(self, passwd, counter, window, options=None):
        """
        checks if the supplied challenge response is correct.

        :param passwd: The challenge response

        :param options: A dictionary of parameters passed by the upper
            layer (used for transaction_id in this context)

        :param counter: legacy API (unused)

        :param window: legacy API (unused)

        :raises TokenStateError: If token state is not 'active' or
            'pairing_challenge_sent'

        :returns: -1 for failure, 1 for success
        """

        valid_states = ['pairing_challenge_sent', 'active']

        self.ensure_state_is_in(valid_states)

        # ------------------------------------------------------------------ --

        # new pushtoken protocoll supports the keyword based accept or deny.
        # the old 'passwd' argument is not supported anymore

        try:

            signature_accept = passwd.get('accept', None)
            signature_reject = passwd.get('reject', None)

        except AttributeError:  # will be raised with a get() on a str object

            raise Exception('Pushtoken version %r requires "accept" or'
                            ' "reject" as parameter' % CHALLENGE_URL_VERSION)

        if signature_accept is not None and signature_reject is not None:

            raise Exception('Pushtoken version %r requires "accept" or'
                            ' "reject" as parameter' % CHALLENGE_URL_VERSION)

        # ------------------------------------------------------------------ --

        filtered_challenges = []
        serial = self.getSerial()

        if options is None:
            options = {}

        max_fail = int(getFromConfig('PushMaxChallenges', '3'))

        # ------------------------------------------------------------------ --

        if 'transactionid' in options:

            # -------------------------------------------------------------- --

            # fetch all challenges that match the transaction id or serial

            transaction_id = options.get('transactionid')

            challenges = Challenges.lookup_challenges(serial=serial,
                                                      transid=transaction_id,
                                                      filter_open=True)

            # -------------------------------------------------------------- --

            # filter into filtered_challenges

            for challenge in challenges:

                (received_tan, tan_is_valid) = challenge.getTanStatus()
                fail_counter = challenge.getTanCount()

                # if we iterate over matching challenges (that is: challenges
                # with the correct transaction id) we either find a fresh
                # challenge, that didn't receive a TAN at all (first case)
                # or a challenge, that already received a number of wrong
                # TANs but still has tries left (second case).

                if not received_tan:
                    filtered_challenges.append(challenge)
                elif not tan_is_valid and fail_counter <= max_fail:
                    filtered_challenges.append(challenge)

        # ------------------------------------------------------------------ --

        if not filtered_challenges:
            return -1

        if len(filtered_challenges) > 1:
            log.error('multiple challenges for one transaction and for one'
                      ' token found!')
            return -1

        # for the serial and the transaction id there could always be only
        # at max one challenge matching. This is even true for sub transactions

        challenge = filtered_challenges[0]

        # client verifies the challenge by signing the challenge
        # plaintext. we retrieve the original plaintext (saved
        # in createChallenge) and check for a match

        data = challenge.getData()
        data_to_verify = b64decode(data['sig_base'])

        b64_dsa_public_key = self.getFromTokenInfo('user_dsa_public_key')
        user_dsa_public_key = b64decode(b64_dsa_public_key)

        # -------------------------------------------------------------- --

        # handle the accept case

        if signature_accept is not None:

            accept_signature_as_bytes = decode_base64_urlsafe(signature_accept)

            accept_data_to_verify_as_bytes = (
                struct.pack('<b', CHALLENGE_URL_VERSION) + b'ACCEPT\0' +
                data_to_verify)

            try:
                verify_sig(accept_signature_as_bytes,
                           accept_data_to_verify_as_bytes, user_dsa_public_key)

                challenge.add_session_info({'accept': True})

                return 1

            except ValueError:

                challenge.add_session_info({'accept': False})
                log.error("accept signature mismatch!")

                return -1

        # -------------------------------------------------------------- --

        # handle the reject case

        elif signature_reject is not None:

            reject_signature_as_bytes = decode_base64_urlsafe(signature_reject)

            reject_data_to_verify_as_bytes = (
                struct.pack('<b', CHALLENGE_URL_VERSION) + b'DENY\0' +
                data_to_verify)

            try:
                verify_sig(reject_signature_as_bytes,
                           reject_data_to_verify_as_bytes, user_dsa_public_key)

                challenge.add_session_info({'reject': True})

                return 1

            except ValueError:

                challenge.add_session_info({'reject': False})
                log.error("reject signature mismatch!")

                return -1

        return -1
Exemple #4
0
def decrypt_pairing_response(enc_pairing_response):
    """
    Parses and decrypts a pairing response into a named tuple PairingResponse
    consisting of

    * user_public_key - the user's public key
    * user_token_id   - an id for the client to uniquely identify the token.
                        this id is necessary, because the client could
                        communicate with more than one linotp, so serials
                        could overlap.
    * serial - the serial identifying the token in linotp
    * user_login - the user login name

    It is possible that either user_login or serial is None. Both
    being None is a valid response according to this function but
    will be considered an error in the calling method.

    The following parameters are needed:

    :param enc_pairing_response:
        The urlsafe-base64 encoded string received from the client

    The following exceptions can be raised:

    :raises ParameterError:
        If the pairing response has an invalid format

    :raises ValueError:
        If the pairing response has a different version
        than this implementation (currently hardcoded)

    :raises ValueError:
        If the pairing response indicates a different
        token type than QRToken (also hardcoded)

    :raises ValueError:
        If the pairing response field "partition" is not
        identical to the field "token_type"
        ("partition" is currently used for the token
        type id. It is reserved for multiple key usage
        in a future implementation.)

    :raises ValueError:
        If the MAC of the response didn't match

    :return:
        Parsed/decrypted PairingReponse
    """

    data = decode_base64_urlsafe(enc_pairing_response)

    # ---------------------------------------------------------------------- --

    #            ------------------------------------------- --
    #  fields   | version | partition | R  | ciphertext | MAC |
    #            ------------------------------------------- --
    #  size     |    1    |     4     | 32 |      ?     | 16  |
    #            ------------------------------------------- --

    if len(data) < 1 + 4 + 32 + 16:
        raise ParameterError('Malformed pairing response')

    # ---------------------------------------------------------------------- --

    # parse header

    header = data[0:5]
    version, partition = struct.unpack('<bI', header)

    if version != PAIR_RESPONSE_VERSION:
        raise ValueError('Unexpected pair-response version, '
                         'expected: %d, got: %d' %
                         (PAIR_RESPONSE_VERSION, version))

    # ---------------------------------------------------------------------- --

    R = data[5:32 + 5]
    ciphertext = data[32 + 5:-16]
    mac = data[-16:]

    # ---------------------------------------------------------------------- --

    # calculate the shared secret

    # - --

    secret_key = get_dh_secret_key(partition)
    ss = calc_dh(secret_key, R)

    # derive encryption key and nonce from the shared secret
    # zero the values from memory when they are not longer needed
    U = SHA256.new(ss).digest()
    zerome(ss)
    encryption_key = U[0:16]
    nonce = U[16:32]
    zerome(U)

    # decrypt response
    cipher = AES.new(encryption_key, AES.MODE_EAX, nonce)
    cipher.update(header)
    plaintext = cipher.decrypt_and_verify(ciphertext, mac)
    zerome(encryption_key)

    # ---------------------------------------------------------------------- --

    # check format boundaries for type peaking
    # (token type specific length boundaries are checked
    #  in the appropriate functions)

    plaintext_min_length = 1
    if len(data) < plaintext_min_length:
        raise ParameterError('Malformed pairing response')

    # ---------------------------------------------------------------------- --

    # get token type and parse decrypted response

    #            -------------------- --
    #  fields   | token type |   ...   |
    #            -------------------- --
    #  size     |     1      |    ?    |
    #            -------------------- --

    token_type = struct.unpack('<b', plaintext[0])[0]

    if token_type not in SUPPORTED_TOKEN_TYPES:
        raise ValueError('unsupported token type %d, supported types '
                         'are %s' % (token_type, SUPPORTED_TOKEN_TYPES))

    # ---------------------------------------------------------------------- --

    # delegate the data parsing of the plaintext
    # to the appropriate function and return the result

    data_parser = get_pairing_data_parser(token_type)
    pairing_data = data_parser(plaintext)
    zerome(plaintext)

    # get the appropriate high level type

    try:
        token_type_as_str = INV_TOKEN_TYPES[token_type]
    except KeyError:
        raise ProgrammingError(
            'token_type %d is in SUPPORTED_TOKEN_TYPES',
            'however an appropriate mapping entry in '
            'TOKEN_TYPES is missing' % token_type)

    return PairingResponse(token_type_as_str, pairing_data)
    def create_user_token_by_pairing_url(pairing_url, pin='1234'):
        """
        parses the pairing url and saves the extracted data in
        the fake token database of this test class.

        :param pairing_url: the pairing url received from the server
        :returns: dict with all information 
                return {
                    'serial': token_serial.decode(),
                    'server_public_key': server_public_key,
                    'partition': partition,
                    'callback_url': callback_url.decode(),
                    'callback_sms': callback_sms.decode(),
                    'pin': pin}
        """

        # extract metadata and the public key

        data_encoded = pairing_url[len(QR_Token_Validation.uri + '://pair/'):]
        data = decode_base64_urlsafe(data_encoded)
        version, token_type, flags = struct.unpack('<bbI', data[0:6])
        partition = struct.unpack('<I', data[6:10])[0]

        server_public_key_dsa = data[10:10 + 32]
        server_public_key = dsa_to_dh_public(server_public_key_dsa)

        # validate protocol versions and type id

        assert token_type == TYPE_QRTOKEN
        assert version == PAIRING_URL_VERSION

        # ------------------------------------------------------------------- --

        # extract custom data that may or may not be present
        # (depending on flags)

        custom_data = data[10 + 32:]

        token_serial = None
        if flags & FLAG_PAIR_SERIAL:
            token_serial, __, custom_data = custom_data.partition(b'\x00')

        callback_url = None
        if flags & FLAG_PAIR_CBURL:
            callback_url, __, custom_data = custom_data.partition(b'\x00')
        else:
            raise NotImplementedError('SMS is not implemented. Callback URL'
                                      'is mandatory.')

        callback_sms = None
        if flags & FLAG_PAIR_CBSMS:
            callback_sms, __, custom_data = custom_data.partition(b'\x00')

        # ------------------------------------------------------------------- --

        # save token data for later use

        ret = {
            'serial': token_serial.decode(),
            'server_public_key': server_public_key,
            'partition': partition,
            'pin': pin}

        if callback_sms:
            ret['callback_sms'] = callback_sms.decode()

        if callback_url:
            ret['callback_url'] = callback_url.decode()

        return ret
    def decrypt_and_verify_challenge(challenge_url, token_info, secret_key):
        """
        Decrypts the data packed in the challenge url, verifies
        its content, returns the parsed data as a dictionary,
        calculates and returns the signature and TAN.

        The calling method must then send the signature/TAN
        back to the server. (The reason for this control flow
        is that the challenge data must be checked in different
        scenarios, e.g. when we have a pairing the data must be
        checked by the method that simulates the pairing)

        :param challenge_url: the challenge url as sent by the server

        :returns: (challenge, signature, tan)

            challenge has the keys

                * message - the signed message sent from the server
                * content_type - one of the three values QRTOKEN_CT_PAIR,
                    QRTOKEN_CT_FREE or QRTOKEN_CT_AUTH
                    (all defined in this module
                * callback_url (optional) - the url to which the challenge
                    response should be set
                * callback_sms (optional) - the sms number the challenge
                    can be sent to (typicall used as a fallback)
                * transaction_id - used to identify the challenge
                    on the server
                * user_token_id - used to identify the token in the
                    user database for which this challenge was created

            signature is the generated user signature used to
            respond to the challenge

            tan is the TAN-Number used as a substitute if the signature
            cant' be sent be the server (is generated from signature)
        """

        challenge_data_encoded = challenge_url[len(QR_Token_Validation.uri + '://chal/'):]
        challenge_data = decode_base64_urlsafe(challenge_data_encoded)

        # ------------------------------------------------------------------- --

        # parse and verify header information in the
        # encrypted challenge data

        header = challenge_data[0:5]
        version, user_token_id = struct.unpack('<bI', header)
        assert version == QRTOKEN_VERSION

        # ------------------------------------------------------------------- --

        # get token from client token database

        # ------------------------------------------------------------------- --

        # prepare decryption by seperating R from
        # ciphertext and tag

        R = challenge_data[5:5 + 32]
        ciphertext = challenge_data[5 + 32:-16]
        tag = challenge_data[-16:]

        # ------------------------------------------------------------------- --

        # key derivation

        ss = calc_dh(secret_key, R)
        U1 = SHA256.new(ss).digest()
        U2 = SHA256.new(U1).digest()

        skA = U1[0:16]
        skB = U2[0:16]
        nonce = U2[16:32]

        # ------------------------------------------------------------------- --

        # decrypt and verify challenge

        cipher = AES.new(skA, AES.MODE_EAX, nonce)
        cipher.update(header)
        plaintext = cipher.decrypt_and_verify(ciphertext, tag)

        # ------------------------------------------------------------------- --

        # parse/check plaintext header

        pt_header = plaintext[0:10]
        content_type, flags, transaction_id = struct.unpack('<bbQ', pt_header)
        transaction_id = QR_Token_Validation.u64_to_transaction_id(transaction_id)

        # make sure a flag for the server signature is
        # present, if the content type is 'pairing'

        if content_type == QRTOKEN_CT_PAIR:
            assert flags & FLAG_QR_SRVSIG

        # ------------------------------------------------------------------- --

        # retrieve plaintext data depending on flags

        if flags & FLAG_QR_SRVSIG:

            # plaintext has a server signature as a header
            # extract it and check if it is correct

            server_signature = plaintext[10:10 + 32]
            data = plaintext[10 + 32:]

            # calculate secret

            server_public_key = token_info['server_public_key']
            secret = calc_dh(secret_key, server_public_key)

            # check hmac

            message = nonce + pt_header + data
            signed = HMAC.new(secret, msg=message, digestmod=SHA256).digest()
            assert server_signature == signed

        else:

            # no server signature found - just remove
            # the plaintext header

            data = plaintext[10:]

            # we have to define an empty server signature in
            # here because we need it later to create the
            # client signature

            server_signature = b''

        # ------------------------------------------------------------------- --

        # extract message and (optional) callback
        # parameters from data

        message, _, suffix = data.partition(b'\x00')

        callback_url = token_info.get('callback_url')
        if flags & FLAG_QR_HAVE_URL:
            callback_url, _, suffix = suffix.partition(b'\x00')

        callback_sms = token_info.get('callback_sms')
        if flags & FLAG_QR_HAVE_SMS:
            callback_sms, _, suffix = suffix.partition(b'\x00')

        # ------------------------------------------------------------------- --

        # prepare the parsed challenge data

        challenge = {}
        challenge['message'] = message.decode('utf-8')
        challenge['content_type'] = content_type
        challenge['transaction_id'] = transaction_id
        challenge['user_token_id'] = user_token_id

        if callback_url:
            challenge['callback_url'] = callback_url.decode('utf-8')
        if callback_sms:
            challenge['callback_sms'] = callback_sms.decode('utf-8')


        # calculate signature and tan

        message_bin = nonce + pt_header + server_signature + data
        sig_hmac = HMAC.new(skB, message_bin, digestmod=SHA256)
        sig = sig_hmac.digest()

        tan = extract_tan(sig, QR_Token_Validation.tan_length)
        encoded_sig = encode_base64_urlsafe(sig)

        return challenge, encoded_sig, tan
Exemple #7
0
    def checkOtp(self, passwd, counter, window, options=None):

        valid_states = ['pairing_challenge_sent', 'pairing_complete']

        self.ensure_state_is_in(valid_states)

        # ------------------------------------------------------------------- --

        filtered_challenges = []
        serial = self.getSerial()

        if options is None:
            options = {}

        max_fail = int(getFromConfig('QRMaxChallenges', '3'))

        # ------------------------------------------------------------------- --

        # TODO: from which point is checkOtp called, when there
        # is no challenge response in the request?

        if 'transactionid' in options:

            # --------------------------------------------------------------- --

            # fetch all challenges that match the transaction id or serial

            transaction_id = options.get('transaction_id')

            challenges = Challenges.lookup_challenges(serial, transaction_id)

            # --------------------------------------------------------------- --

            # filter into filtered_challenges

            for challenge in challenges:

                (received_tan, tan_is_valid) = challenge.getTanStatus()
                fail_counter = challenge.getTanCount()

                # if we iterate over matching challenges (that is: challenges
                # with the correct transaction id) we either find a fresh
                # challenge, that didn't receive a TAN at all (first case)
                # or a challenge, that already received a number of wrong
                # TANs but still has tries left (second case).

                if not received_tan:
                    filtered_challenges.append(challenge)
                elif not tan_is_valid and fail_counter <= max_fail:
                    filtered_challenges.append(challenge)

            # --------------------------------------------------------------- --

        if not filtered_challenges:
            return -1

        for challenge in filtered_challenges:

            data = challenge.getData()
            correct_passwd = data['user_sig']

            # compare values with python's native constant
            # time comparison

            if compare_digest(correct_passwd, passwd):

                return 1

            else:

                # maybe we got a tan instead of a signature

                correct_passwd_as_bytes = decode_base64_urlsafe(correct_passwd)
                tan_length = self.getOtpLen()
                correct_tan = extract_tan(correct_passwd_as_bytes, tan_length)

                if compare_digest(correct_tan, passwd):
                    return 1

        # return the token counter which is at least 0, -1 indicates an error
        return -1
    def decrypt_and_verify_challenge(challenge_url: str, token_info: Dict,
                                     secret_key: bytes,
                                     action: str) -> Tuple[Dict, str]:
        """Decrypts the data packed in the challenge url, verifies the content.

        Returns the parsed data as a dictionary, calculates and returns the
        signature.

        The calling method must then send the signature
        back to the server. (The reason for this control flow
        is that the challenge data must be checked in different
        scenarios, e.g. when we have a pairing the data must be
        checked by the method that simulates the pairing)

        :param challenge_url: the challenge url as sent by the server
        :param action: a string identifier for the verification action
            (at the moment 'ACCEPT' or 'DENY')

        :returns: (challenge, signature)

            challenge has the keys

                * content_type - one of the three values CONTENT_TYPE_SIGNREQ,
                    CONTENT_TYPE_PAIRING or CONTENT_TYPE_LOGIN)
                    (all defined in this module)
                * transaction_id - used to identify the challenge
                    on the server
                * callback_url (optional) - the url to which the challenge
                    response should be set
                * user_token_id - used to identify the token in the
                    user database for which this challenge was created

            depending on the content type additional keys are present

                * for CONTENT_TYPE_PAIRING: serial
                * for CONTENT_TYPE_SIGNREQ: message
                * for CONTENT_TYPE_LOGIN: login, host

            signature is the generated user signature used to
            respond to the challenge
        """

        challenge_data_encoded = challenge_url[len(Push_Token_Validation.
                                                   uri_schema + '://chal/'):]
        challenge_data = decode_base64_urlsafe(challenge_data_encoded)

        # ------------------------------------------------------------------ --

        # parse and verify header information in the
        # encrypted challenge data

        header = challenge_data[0:5]
        version, user_token_id = struct.unpack('<bI', header)
        assert version == CHALLENGE_URL_VERSION

        # ------------------------------------------------------------------ --

        # get token from client token database

        server_public_key = token_info['server_public_key']

        # ------------------------------------------------------------------ --

        # prepare decryption by seperating R from
        # ciphertext and server signature

        R = challenge_data[5:5 + 32]
        ciphertext = challenge_data[5 + 32:-64]
        server_signature = challenge_data[-64:]

        # check signature

        data = challenge_data[0:-64]
        crypto_sign_verify_detached(server_signature, data, server_public_key)

        # ------------------------------------------------------------------ --

        # key derivation

        secret_key_dh = dsa_to_dh_secret(secret_key)
        ss = calc_dh(secret_key_dh, R)
        U = SHA256.new(ss).digest()

        sk = U[0:16]
        nonce = U[16:32]

        # ------------------------------------------------------------------ --

        # decrypt and verify challenge

        nonce_as_int = int_from_bytes(nonce, byteorder='big')
        ctr = Counter.new(128, initial_value=nonce_as_int)
        cipher = AES.new(sk, AES.MODE_CTR, counter=ctr)
        plaintext = cipher.decrypt(ciphertext)

        # ------------------------------------------------------------------ --

        # parse/check plaintext header

        # 1 - for content type
        # 8 - for transaction id
        # 8 - for time stamp
        offset = 1 + 8 + 8

        pt_header = plaintext[0:offset]
        (content_type, transaction_id,
         _time_stamp) = struct.unpack('<bQQ', pt_header)

        transaction_id = Push_Token_Validation.u64_to_transaction_id(
            transaction_id)

        # ------------------------------------------------------------------ --

        # prepare the parsed challenge data

        challenge = {}
        challenge['content_type'] = content_type

        # ------------------------------------------------------------------ --

        # retrieve plaintext data depending on content_type

        if content_type == CONTENT_TYPE_PAIRING:

            serial, callback_url, __ = plaintext[offset:].split(b'\x00')
            challenge['serial'] = serial.decode()

        elif content_type == CONTENT_TYPE_SIGNREQ:

            message, callback_url, __ = plaintext[offset:].split(b'\x00')
            challenge['message'] = message.decode()

        elif content_type == CONTENT_TYPE_LOGIN:

            login, host, callback_url, __ = plaintext[offset:].split(b'\x00')
            challenge['login'] = login.decode()
            challenge['host'] = host.decode()

        # ------------------------------------------------------------------ --

        # prepare the parsed challenge data

        challenge['callback_url'] = callback_url.decode()
        challenge['transaction_id'] = transaction_id
        challenge['user_token_id'] = user_token_id

        # calculate signature

        sig_base = (struct.pack('<b', CHALLENGE_URL_VERSION) +
                    b'%s\0' % action.encode('utf-8') + server_signature +
                    plaintext)

        sig = crypto_sign_detached(sig_base, secret_key)
        encoded_sig = encode_base64_urlsafe(sig)

        return challenge, encoded_sig
def parse_challenge_url(challenge_url):
    """ Parses a challenge url and prints its data """

    challenge_data_encoded = challenge_url[len('lseqr://chal/'):]
    challenge_data = decode_base64_urlsafe(challenge_data_encoded)

    # ----------------------------------------------------------------------

    # parse and verify header information in the
    # encrypted challenge data

    header = challenge_data[0:5]
    version, user_token_id = struct.unpack('<bI', header)
    if not version == QRTOKEN_VERSION:
        raise Exception('wrong qrtoken version')

    # ----------------------------------------------------------------------

    # get token from client token database

    token = token_db[user_token_id]

    # ----------------------------------------------------------------------

    # prepare decryption by seperating R from
    # ciphertext and tag

    R = challenge_data[5:5 + 32]
    ciphertext = challenge_data[5 + 32:-16]
    tag = challenge_data[-16:]

    # ----------------------------------------------------------------------

    # key derivation

    ss = calc_dh(secret_key, R)
    U1 = SHA256.new(ss).digest()
    U2 = SHA256.new(U1).digest()

    skA = U1[0:16]
    skB = U2[0:16]
    nonce = U2[16:32]

    # ----------------------------------------------------------------------

    # decrypt and verify challenge

    cipher = AES.new(skA, AES.MODE_EAX, nonce)
    cipher.update(header)
    plaintext = cipher.decrypt_and_verify(ciphertext, tag)

    # ----------------------------------------------------------------------

    # parse/check plaintext header

    pt_header = plaintext[0:10]
    content_type, flags, transaction_id = struct.unpack('<bbQ', pt_header)
    transaction_id = u64_to_transaction_id(transaction_id)

    # make sure a flag for the server signature is
    # present, if the content type is 'pairing'

    if content_type == QRTOKEN_CT_PAIR and not flags & FLAG_QR_SRVSIG:
        raise Exception('Ill formatted callenge url')

    # ----------------------------------------------------------------------

    # retrieve plaintext data depending on flags

    if flags & FLAG_QR_SRVSIG:

        # plaintext has a server signature as a header
        # extract it and check if it is correct

        server_signature = plaintext[10:10 + 32]
        data = plaintext[10 + 32:]

        # calculate secret

        server_public_key = token['server_public_key']
        secret = calc_dh(secret_key, server_public_key)

        # check hmac

        message = nonce + pt_header + data
        signed = HMAC.new(secret, msg=message, digestmod=SHA256).digest()

        if not server_signature == signed:
            raise Exception('HMAC signature check failed')

    else:

        # no server signature found - just remove
        # the plaintext header

        data = plaintext[10:]

        # we have to define an empty server signature in
        # here because we need it later to create the
        # client signature

        server_signature = b''

    # ----------------------------------------------------------------------

    # extract message and (optional) callback
    # parameters from data

    message, _, suffix = data.partition(b'\x00')

    callback_url = token['callback_url']
    if flags & FLAG_QR_HAVE_URL:
        callback_url, _, suffix = suffix.partition(b'\x00')

    callback_sms = token['callback_sms']
    if flags & FLAG_QR_HAVE_SMS:
        callback_sms, _, suffix = suffix.partition(b'\x00')

    # ----------------------------------------------------------------------

    # prepare the parsed challenge data

    challenge = {}
    challenge['message'] = message
    challenge['content_type'] = content_type
    challenge['callback_url'] = callback_url
    challenge['callback_sms'] = callback_sms
    challenge['transaction_id'] = transaction_id
    challenge['user_token_id'] = user_token_id

    # calculate signature and tan

    message = nonce + pt_header + data
    sig_hmac = HMAC.new(skB, message, digestmod=SHA256)
    sig = sig_hmac.digest()

    encoded_sig = encode_base64_urlsafe(sig)

    print('Data in URL:')

    for key, value in list(challenge.items()):
        print(('%s\n    %s\n' % (key, value)))

    return challenge, encoded_sig
def parse_pairing_url(pairing_url):
    """
    parses the pairing url and saves the extracted data in
    the fake token database

    :param pairing_url: the pairing url received from the server
    :returns: user_token_id of newly created token
    """

    # extract metadata and the public key

    data_encoded = pairing_url[len('lseqr://pair/'):]
    data = decode_base64_urlsafe(data_encoded)
    version, token_type, flags = struct.unpack('<bbI', data[0:6])
    server_public_key = data[6:6 + 32]

    # validate protocol versions and type id

    if not token_type == TYPE_QRTOKEN:
        raise Exception("wrong token type in url")

    if not version == RESPONSE_VERSION:
        raise Exception('wrong pairing version')

    # --------------------------------------------------------------------------

    # extract custom data that may or may not be present
    # (depending on flags)

    custom_data = data[6 + 32:]

    token_serial = None
    if flags & FLAG_PAIR_SERIAL:
        token_serial, __, custom_data = custom_data.partition(b'\x00')

    callback_url = None
    if flags & FLAG_PAIR_CBURL:
        callback_url, __, custom_data = custom_data.partition(b'\x00')
    else:
        raise NotImplementedError('SMS is not implemented. Callback URL'
                                  'is mandatory.')

    callback_sms = None
    if flags & FLAG_PAIR_CBSMS:
        callback_sms, __, custom_data = custom_data.partition(b'\x00')

    # ----------------------------------------------------------------------

    # save token data for later use

    user_token_id = len(token_db)
    token_db[user_token_id] = {
        'serial': token_serial,
        'server_public_key': server_public_key,
        'callback_url': callback_url,
        'callback_sms': callback_sms
    }

    # ----------------------------------------------------------------------

    print('Data in URL:')

    for key, value in list(token_db[user_token_id].items()):
        if key == 'server_public_key':
            value = value.encode('hex')
        print(('%s\n    %s\n' % (key, value)))

    return user_token_id