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
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
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
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