def checkYubikeyPass(self, passw): """ Checks the password of a yubikey in Yubico mode (44,48), where the first 12 or 16 characters are the tokenid :param passw: The password that consist of the static yubikey prefix and the otp :type passw: string :return: True/False and the User-Object of the token owner :rtype: dict """ audit = context['audit'] opt = None res = False tokenList = [] # strip the yubico OTP and the PIN modhex_serial = passw[:-32][-16:] try: hex_serial = modhex_decode(modhex_serial) serialnum = "UBAM" + binascii.unhexlify(hex_serial).decode('utf-8') except TypeError as exx: log.error("Failed to convert serialnumber: %r" % exx) return res, opt # build list of possible yubikey tokens serials = [serialnum] for i in range(1, 3): serials.append("%s_%s" % (serialnum, i)) for serial in serials: tokens = getTokens4UserOrSerial(serial=serial, read_for_update=True) tokenList.extend(tokens) if len(tokenList) == 0: audit['action_detail'] = ('The serial %s could not be found!' % serialnum) return res, opt # FIXME if the Token has set a PIN and the User does not want to enter # the PIN for authentication, we need to do something different here... # and avoid PIN checking in __checkToken. # We could pass an "option" to __checkToken. (res, opt) = self.checkTokenList(tokenList, passw) # Now we need to get the user if res is not False and 'serial' in audit: serial = audit.get('serial', None) if serial is not None: user = get_token_owner(tokenList[0]) audit['user'] = user.login audit['realm'] = user.realm opt = {'user': user.login, 'realm': user.realm} return res, opt
def checkYubikeyPass(self, passw): """ Checks the password of a yubikey in Yubico mode (44,48), where the first 12 or 16 characters are the tokenid :param passw: The password that consist of the static yubikey prefix and the otp :type passw: string :return: True/False and the User-Object of the token owner :rtype: dict """ audit = self.context['audit'] opt = None res = False tokenList = [] # strip the yubico OTP and the PIN modhex_serial = passw[:-32][-16:] try: serialnum = "UBAM" + modhex_decode(modhex_serial) except TypeError as exx: log.error("Failed to convert serialnumber: %r" % exx) return res, opt # build list of possible yubikey tokens serials = [serialnum] for i in range(1, 3): serials.append("%s_%s" % (serialnum, i)) for serial in serials: tokens = linotp.lib.token.getTokens4UserOrSerial( serial=serial, context=self.context) tokenList.extend(tokens) if len(tokenList) == 0: audit['action_detail'] = ('The serial %s could not be found!' % serialnum) return res, opt # FIXME if the Token has set a PIN and the User does not want to enter # the PIN for authentication, we need to do something different here... # and avoid PIN checking in __checkToken. # We could pass an "option" to __checkToken. (res, opt) = self.checkTokenList(tokenList, passw) # Now we need to get the user if res is not False and 'serial' in audit: serial = audit.get('serial', None) if serial is not None: user = self.getTokenOwner(serial) audit['user'] = user.login audit['realm'] = user.realm opt = {'user': user.login, 'realm': user.realm} return res, opt
def checkOtp(self, otpVal, counter=None, window=None, options=None): """ checkOtp - validate the token otp against a given otpvalue :param otpVal: the to be verified otpvalue :type otpVal: string :param counter: the counter state. It is not used by the YubiKey because the current counter value is sent encrypted inside the OTP value :type counter: int :param window: the counter +window, which is not used in the YubiKey because the current counter value is sent encrypted inside the OTP, allowing a simple comparison between the encrypted counter value and the stored counter value :type window: int :param options: the dict, which could contain token specific info :type options: dict :return: the counter state or an error code (< 0): -1 if the OTP is old (counter < stored counter) -2 if the private_uid sent in the OTP is wrong (different from the one stored with the token) -3 if the CRC verification fails :rtype: int From: http://www.yubico.com/wp-content/uploads/2013/04/YubiKey-Manual-v3_1.pdf 6 Implementation details """ res = -1 if len(otpVal) < self.getOtpLen(): return res serial = self.token.getSerial() secObj = self._get_secret_object() anOtpVal = otpVal.lower() # The prefix is the characters in front of the last 32 chars # We can also check the PREFIX! At the moment, we do not use it! yubi_prefix = anOtpVal[:-32] # verify the prefix if any enroll_prefix = self.getFromTokenInfo('public_uid', None) if enroll_prefix and enroll_prefix != yubi_prefix: return res # The variable otp val is the last 32 chars yubi_otp = anOtpVal[-32:] try: otp_bin = modhex_decode(yubi_otp) msg_bin = secObj.aes_decrypt(otp_bin) except KeyError: log.warning("failed to decode yubi_otp!") return res msg_hex = binascii.hexlify(msg_bin) uid = msg_hex[0:12] log.debug("[checkOtp] uid: %r" % uid) log.debug("[checkOtp] prefix: %r" % binascii.hexlify(modhex_decode(yubi_prefix))) # usage_counter can go from 1 – 0x7fff usage_counter = msg_hex[12:16] # TODO: We also could check the timestamp # - the timestamp. see http://www.yubico.com/wp-content/uploads/2013/04/YubiKey-Manual-v3_1.pdf timestamp = msg_hex[16:22] # session counter can go from 00 to 0xff session_counter = msg_hex[22:24] random = msg_hex[24:28] log.debug("[checkOtp] decrypted: usage_count: %r, session_count: %r" % (usage_counter, session_counter)) # The checksum is a CRC-16 (16-bit ISO 13239 1st complement) that # occupies the last 2 bytes of the decrypted OTP value. Calculating the # CRC-16 checksum of the whole decrypted OTP should give a fixed residual # of 0xf0b8 (see Yubikey-Manual - Chapter 6: Implementation details). crc = msg_hex[28:] log.debug("[checkOtp] calculated checksum (61624): %r" % checksum(msg_hex)) if checksum(msg_hex) != 0xf0b8: log.warning("[checkOtp] CRC checksum for token %r failed" % serial) return -3 # create the counter as integer # Note: The usage counter is stored LSB! count_hex = usage_counter[2:4] + usage_counter[0:2] + session_counter count_int = int(count_hex, 16) log.debug('[checkOtp] decrypted counter: %r' % count_int) tokenid = self.getFromTokenInfo("yubikey.tokenid") if not tokenid: log.debug("[checkOtp] Got no tokenid for %r. Setting to %r." % (serial, uid)) tokenid = uid self.addToTokenInfo("yubikey.tokenid", tokenid) if tokenid != uid: # wrong token! log.warning( "[checkOtp] The wrong token was presented for %r. Got %r, expected %r." % (serial, uid, tokenid)) return -2 log.debug('[checkOtp] compare counter to LinOtpCount: %r' % self.token.LinOtpCount) if count_int >= self.token.LinOtpCount: res = count_int return res
def parseYubicoCSV(csv): """ This function reads the CSV data as created by the Yubico personalization GUI. Traditional Format: Yubico OTP,12/11/2013 11:10,1,vvgutbiedkvi,ab86c04de6a3,d26a7c0f85fdda28bd816e406342b214,,,0,0,0,0,0,0,0,0,0,0 OATH-HOTP,11.12.13 18:55,1,cccccccccccc,,916821d3a138bf855e70069605559a206ba854cd,,,0,0,0,6,0,0,0,0,0,0 Static Password,11.12.13 19:08,1,,d5a3d50327dc,0e8e37b0e38b314a56748c030f58d21d,,,0,0,0,0,0,0,0,0,0,0 Yubico Format: # OATH mode 508326,,0,69cfb9202438ca68964ec3244bfa4843d073a43b,,2013-12-12T08:41:07, 1382042,,0,bf7efc1c8b6f23604930a9ce693bdd6c3265be00,,2013-12-12T08:41:17, # Yubico mode 508326,cccccccccccc,83cebdfb7b93,a47c5bf9c152202f577be6721c0113af,,2013-12-12T08:43:17, # static mode 508326,,,9e2fd386224a7f77e9b5aee775464033,,2013-12-12T08:44:34, column 0: serial column 1: public ID in yubico mode column 2: private ID in yubico mode, 0 in OATH mode, blank in static mode column 3: AES key BUMMER: The Yubico Format does not contain the information, which slot of the token was written. If now public ID or serial is given, we can not import the token, as the returned dictionary needs the token serial as a key. It returns a dictionary with the new tokens to be created: { serial: { 'type' : yubico, 'hmac_key' : xxxx, 'otplen' : xxx, 'description' : xxx } } """ TOKENS = {} log.debug("[parseYubicoCSV] starting to parse an yubico csv file.") csv_array = csv.split("\n") log.debug("[parseYubicoCSV] the file contains %i tokens." % len(csv_array)) for line in csv_array: l = line.split(",") serial = "" key = "" otplen = 32 public_id = "" slot = "" if len(l) >= 6: first_column = l[0].strip() if first_column.lower() in ["yubico otp", "oath-hotp", "static password"]: # traditional format typ = l[0].strip() slot = l[2].strip() public_id = l[3].strip() key = l[5].strip() if public_id == "": log.warning("No public ID in line %r" % line) serial_int = int(binascii.hexlify(os.urandom(4)), 16) else: serial_int = int(binascii.hexlify(modhex_decode(public_id)), 16) if typ.lower() == "yubico otp": ttype = "yubikey" otplen = 32 + len(public_id) serial = "UBAM%08d_%s" % (serial_int, slot) TOKENS[serial] = {"type": ttype, "hmac_key": key, "otplen": otplen, "description": public_id} elif typ.lower() == "oath-hotp": """ TODO: this does not work out at the moment, since the GUI either 1. creates a serial in the CSV, but then the serial is always prefixed! We can not authenticate with this! 2. if it does not prefix the serial there is no serial in the CSV! We can not import and assign the token! """ ttype = "hmac" otplen = 6 serial = "UBOM%08d_%s" % (serial_int, slot) TOKENS[serial] = {"type": ttype, "hmac_key": key, "otplen": otplen, "description": public_id} else: log.warning("[parseYubicoCSV] at the moment we do only support Yubico OTP and HOTP: %r" % line) continue elif first_column.isdigit(): # first column is a number, (serial number), so we are in the yubico format serial = first_column # the yubico format does not specify a slot slot = "X" key = l[3].strip() if l[2].strip() == "0": # HOTP typ = "hmac" serial = "UBOM%s_%s" % (serial, slot) otplen = 6 elif l[2].strip() == "": # Static typ = "pw" serial = "UBSM%s_%s" % (serial, slot) key = create_static_password(key) otplen = len(key) log.warning( "[parseYubcoCSV] We can not enroll a static mode, since we do not know" " the private identify and so we do not know the static password." ) continue else: # Yubico typ = "yubikey" serial = "UBAM%s_%s" % (serial, slot) public_id = l[1].strip() otplen = 32 + len(public_id) TOKENS[serial] = {"type": typ, "hmac_key": key, "otplen": otplen, "description": public_id} else: log.warning("[parseYubicoCSV] the line %r did not contain a enough values" % line) continue log.debug("[parseOATHcsv] read the following values: %s" % str(TOKENS)) return TOKENS
def parseYubicoCSV(csv): ''' This function reads the CSV data as created by the Yubico personalization GUI. Traditional Format: Yubico OTP,12/11/2013 11:10,1,vvgutbiedkvi,ab86c04de6a3,d26a7c0f85fdda28bd816e406342b214,,,0,0,0,0,0,0,0,0,0,0 OATH-HOTP,11.12.13 18:55,1,cccccccccccc,,916821d3a138bf855e70069605559a206ba854cd,,,0,0,0,6,0,0,0,0,0,0 Static Password,11.12.13 19:08,1,,d5a3d50327dc,0e8e37b0e38b314a56748c030f58d21d,,,0,0,0,0,0,0,0,0,0,0 Yubico Format: # OATH mode 508326,,0,69cfb9202438ca68964ec3244bfa4843d073a43b,,2013-12-12T08:41:07, 1382042,,0,bf7efc1c8b6f23604930a9ce693bdd6c3265be00,,2013-12-12T08:41:17, # Yubico mode 508326,cccccccccccc,83cebdfb7b93,a47c5bf9c152202f577be6721c0113af,,2013-12-12T08:43:17, # static mode 508326,,,9e2fd386224a7f77e9b5aee775464033,,2013-12-12T08:44:34, column 0: serial column 1: public ID in yubico mode column 2: private ID in yubico mode, 0 in OATH mode, blank in static mode column 3: AES key BUMMER: The Yubico Format does not contain the information, which slot of the token was written. If now public ID or serial is given, we can not import the token, as the returned dictionary needs the token serial as a key. It returns a dictionary with the new tokens to be created: { serial: { 'type' : yubico, 'hmac_key' : xxxx, 'otplen' : xxx, 'description' : xxx } } ''' TOKENS = {} log.debug("[parseYubicoCSV] starting to parse an yubico csv file.") csv_array = csv.split('\n') log.debug("[parseYubicoCSV] the file contains %i tokens." % len(csv_array)) for line in csv_array: l = line.split(',') serial = "" key = "" otplen = 32 public_id = "" slot = "" if len(l) >= 6: first_column = l[0].strip() if first_column.lower() in [ "yubico otp", "oath-hotp", "static password" ]: # traditional format typ = l[0].strip() slot = l[2].strip() public_id = l[3].strip() key = l[5].strip() if public_id == "": log.warning("No public ID in line %r" % line) serial_int = int(binascii.hexlify(os.urandom(4)), 16) else: serial_int = int( binascii.hexlify(modhex_decode(public_id)), 16) if typ.lower() == "yubico otp": ttype = "yubikey" otplen = 32 + len(public_id) serial = "UBAM%08d_%s" % (serial_int, slot) TOKENS[serial] = { 'type': ttype, 'hmac_key': key, 'otplen': otplen, 'description': public_id } elif typ.lower() == "oath-hotp": ''' TODO: this does not work out at the moment, since the GUI either 1. creates a serial in the CSV, but then the serial is always prefixed! We can not authenticate with this! 2. if it does not prefix the serial there is no serial in the CSV! We can not import and assign the token! ''' ttype = "hmac" otplen = 6 serial = "UBOM%08d_%s" % (serial_int, slot) TOKENS[serial] = { 'type': ttype, 'hmac_key': key, 'otplen': otplen, 'description': public_id } else: log.warning( "[parseYubicoCSV] at the moment we do only support Yubico OTP and HOTP: %r" % line) continue elif first_column.isdigit(): # first column is a number, (serial number), so we are in the yubico format serial = first_column # the yubico format does not specify a slot slot = "X" key = l[3].strip() if l[2].strip() == "0": # HOTP typ = "hmac" serial = "UBOM%s_%s" % (serial, slot) otplen = 6 elif l[2].strip() == "": # Static typ = "pw" serial = "UBSM%s_%s" % (serial, slot) key = create_static_password(key) otplen = len(key) log.warning( "[parseYubcoCSV] We can not enroll a static mode, since we do not know" " the private identify and so we do not know the static password." ) continue else: # Yubico typ = "yubikey" serial = "UBAM%s_%s" % (serial, slot) public_id = l[1].strip() otplen = 32 + len(public_id) TOKENS[serial] = { 'type': typ, 'hmac_key': key, 'otplen': otplen, 'description': public_id } else: log.warning( "[parseYubicoCSV] the line %r did not contain a enough values" % line) continue log.debug("[parseOATHcsv] read the following values: %s" % str(TOKENS)) return TOKENS
def checkOtp(self, otpVal, counter=None, window=None, options=None): """ checkOtp - validate the token otp against a given otpvalue :param otpVal: the to be verified otpvalue :type otpVal: string :param counter: the counter state. It is not used by the YubiKey because the current counter value is sent encrypted inside the OTP value :type counter: int :param window: the counter +window, which is not used in the YubiKey because the current counter value is sent encrypted inside the OTP, allowing a simple comparison between the encrypted counter value and the stored counter value :type window: int :param options: the dict, which could contain token specific info :type options: dict :return: the counter state or an error code (< 0): -1 if the OTP is old (counter < stored counter) -2 if the private_uid sent in the OTP is wrong (different from the one stored with the token) -3 if the CRC verification fails :rtype: int From: http://www.yubico.com/wp-content/uploads/2013/04/YubiKey-Manual-v3_1.pdf 6 Implementation details """ log.debug("[checkOtp] begin. Validate the token otp: otpVal: %r, counter: %r, options: %r " % (otpVal, counter, options)) res = -1 if len(otpVal) < self.getOtpLen(): return res serial = self.token.getSerial() secObj = self._get_secret_object() anOtpVal = otpVal.lower() # The prefix is the characters in front of the last 32 chars # We can also check the PREFIX! At the moment, we do not use it! yubi_prefix = anOtpVal[:-32] # verify the prefix if any enroll_prefix = self.getFromTokenInfo('public_uid', None) if enroll_prefix and enroll_prefix != yubi_prefix: return res # The variable otp val is the last 32 chars yubi_otp = anOtpVal[-32:] try: otp_bin = modhex_decode(yubi_otp) msg_bin = secObj.aes_decrypt(otp_bin) except KeyError: log.warning("failed to decode yubi_otp!") return res msg_hex = binascii.hexlify(msg_bin) uid = msg_hex[0:12] log.debug("[checkOtp] uid: %r" % uid) log.debug("[checkOtp] prefix: %r" % binascii.hexlify(modhex_decode(yubi_prefix))) # usage_counter can go from 1 – 0x7fff usage_counter = msg_hex[12:16] # TODO: We also could check the timestamp # - the timestamp. see http://www.yubico.com/wp-content/uploads/2013/04/YubiKey-Manual-v3_1.pdf timestamp = msg_hex[16:22] # session counter can go from 00 to 0xff session_counter = msg_hex[22:24] random = msg_hex[24:28] log.debug("[checkOtp] decrypted: usage_count: %r, session_count: %r" % (usage_counter, session_counter)) # The checksum is a CRC-16 (16-bit ISO 13239 1st complement) that # occupies the last 2 bytes of the decrypted OTP value. Calculating the # CRC-16 checksum of the whole decrypted OTP should give a fixed residual # of 0xf0b8 (see Yubikey-Manual - Chapter 6: Implementation details). crc = msg_hex[28:] log.debug("[checkOtp] calculated checksum (61624): %r" % checksum(msg_hex)) if checksum(msg_hex) != 0xf0b8: log.warning("[checkOtp] CRC checksum for token %r failed" % serial) return -3 # create the counter as integer # Note: The usage counter is stored LSB! count_hex = usage_counter[2:4] + usage_counter[0:2] + session_counter count_int = int(count_hex, 16) log.debug('[checkOtp] decrypted counter: %r' % count_int) tokenid = self.getFromTokenInfo("yubikey.tokenid") if not tokenid: log.debug("[checkOtp] Got no tokenid for %r. Setting to %r." % (serial, uid)) tokenid = uid self.addToTokenInfo("yubikey.tokenid", tokenid) if tokenid != uid: # wrong token! log.warning("[checkOtp] The wrong token was presented for %r. Got %r, expected %r." % (serial, uid, tokenid)) return -2 log.debug('[checkOtp] compare counter to LinOtpCount: %r' % self.token.LinOtpCount) if count_int >= self.token.LinOtpCount: res = count_int return res
def test_hex2mod(): m = 'fifjgjgkhchb' h = '474858596061' assert modhex_decode(m) == h