def test_28_yubikey_utils(self): self.assertEquals(modhex_encode(b'\x47'), 'fi') self.assertEquals(modhex_encode(b'\xba\xad\xf0\x0d'), 'nlltvcct') self.assertEquals(modhex_encode(binascii.unhexlify('0123456789abcdef')), 'cbdefghijklnrtuv') self.assertEquals(modhex_encode('Hallo'), 'fjhbhrhrhv') # and the other way around self.assertEquals(modhex_decode('fi'), b'\x47') self.assertEquals(modhex_decode('nlltvcct'), b'\xba\xad\xf0\x0d') self.assertEquals(modhex_decode('cbdefghijklnrtuv'), binascii.unhexlify('0123456789abcdef')) self.assertEquals(modhex_decode('fjhbhrhrhv'), b'Hallo') # now test the crc function self.assertEquals(checksum(b'\x01\x02\x03\x04'), 0xc66e) self.assertEquals(checksum(b'\x01\x02\x03\x04\x919'), 0xf0b8)
def check_yubikey_pass(passw): """ if the Token has set a PIN the user must also enter the PIN for authentication! This checks the output of a yubikey in AES mode without providing the serial number. The first 12 (of 44) or 16 of 48) characters are the tokenid, which is stored in the tokeninfo yubikey.tokenid or the prefix yubikey.prefix. :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 """ opt = {} res = False token_list = [] # strip the yubico OTP and the PIN prefix = passw[:-32][-16:] from privacyidea.lib.token import get_tokens from privacyidea.lib.token import check_token_list # See if the prefix matches the serial number if prefix[:2] != "vv" and prefix[:2] != "cc": try: # Keep the backward compatibility serialnum = "UBAM" + modhex_decode(prefix) for i in range(1, 3): s = "{0!s}_{1!s}".format(serialnum, i) toks = get_tokens(serial=s, tokentype='yubikey') token_list.extend(toks) except TypeError as exx: # pragma: no cover log.error("Failed to convert serialnumber: {0!r}".format(exx)) # Now, we see, if the prefix matches the new version if not token_list: # If we did not find the token via the serial number, we also # search for the yubikey.prefix in the tokeninfo. token_candidate_list = get_tokens( tokentype='yubikey', tokeninfo={"yubikey.prefix": prefix}) token_list.extend(token_candidate_list) if not token_list: opt['action_detail'] = ( "The prefix {0!s} could not be found!".format(prefix)) return res, opt (res, opt) = check_token_list(token_list, passw, allow_reset_all_tokens=True) return res, opt
def check_yubikey_pass(passw): """ if the Token has set a PIN the user must also enter the PIN for authentication! This checks the output of a yubikey in AES mode without providing the serial number. The first 12 (of 44) or 16 of 48) characters are the tokenid, which is stored in the tokeninfo yubikey.tokenid or the prefix yubikey.prefix. :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 """ opt = {} res = False token_list = [] # strip the yubico OTP and the PIN prefix = passw[:-32][-16:] from privacyidea.lib.token import get_tokens from privacyidea.lib.token import check_token_list # See if the prefix matches the serial number if prefix[:2] != "vv" and prefix[:2] != "cc": try: # Keep the backward compatibility serialnum = "UBAM" + modhex_decode(prefix) for i in range(1, 3): s = "{0!s}_{1!s}".format(serialnum, i) toks = get_tokens(serial=s, tokentype='yubikey') token_list.extend(toks) except TypeError as exx: # pragma: no cover log.error("Failed to convert serialnumber: {0!r}".format(exx)) # Now, we see, if the prefix matches the new version if not token_list: # If we did not find the token via the serial number, we also # search for the yubikey.prefix in the tokeninfo. token_candidate_list = get_tokens(tokentype='yubikey', tokeninfo={"yubikey.prefix": prefix}) token_list.extend(token_candidate_list) if not token_list: opt['action_detail'] = ("The prefix {0!s} could not be found!".format( prefix)) return res, opt (res, opt) = check_token_list(token_list, passw, allow_reset_all_tokens=True) return res, opt
def check_yubikey_pass(passw): """ if the Token has set a PIN the user must also enter the PIN for authentication! This checks the output of a yubikey in AES mode without providing the serial number. The first 12 (of 44) or 16 of 48) characters are the tokenid, which is stored in the tokeninfo. :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 """ opt = {} res = False token_list = [] # strip the yubico OTP and the PIN modhex_serial = passw[:-32][-16:] try: serialnum = "UBAM" + modhex_decode(modhex_serial) except TypeError as exx: # pragma: no cover 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)) from privacyidea.lib.token import get_tokens from privacyidea.lib.token import check_token_list for serial in serials: tokenobject_list = get_tokens(serial=serial) token_list.extend(tokenobject_list) if len(token_list) == 0: opt['action_detail'] = ("The serial %s could not be found!" % serialnum) return res, opt (res, opt) = check_token_list(token_list, passw) return res, opt
def check_yubikey_pass(passw): """ if the Token has set a PIN the user must also enter the PIN for authentication! This checks the output of a yubikey in AES mode without providing the serial number. The first 12 (of 44) or 16 of 48) characters are the tokenid, which is stored in the tokeninfo. :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 """ opt = {} res = False token_list = [] # strip the yubico OTP and the PIN modhex_serial = passw[:-32][-16:] try: serialnum = "UBAM" + modhex_decode(modhex_serial) except TypeError as exx: # pragma: no cover 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)) from privacyidea.lib.token import get_tokens from privacyidea.lib.token import check_token_list for serial in serials: tokenobject_list = get_tokens(serial=serial) token_list.extend(tokenobject_list) if not token_list: opt['action_detail'] = ("The serial %s could not be found!" % serialnum) return res, opt (res, opt) = check_token_list(token_list, passw) return res, opt
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, 'otpkey' : xxxx, 'otplen' : xxx, 'description' : xxx } } ''' TOKENS = {} csv_array = csv.split('\n') log.debug("the file contains {0:d} tokens.".format(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 == "": # Usually a "static password" does not have a public ID! # So we would bail out here for static passwords. log.warning("No public ID in line {0!r}".format(line)) continue serial_int = int(binascii.hexlify(modhex_decode(public_id)), 16) if typ.lower() == "yubico otp": ttype = "yubikey" otplen = 32 + len(public_id) serial = "UBAM{0:08d}_{1!s}".format(serial_int, slot) TOKENS[serial] = {'type': ttype, 'otpkey': key, 'otplen': otplen, 'description': public_id } elif typ.lower() == "oath-hotp": ''' WARNING: this does not work out at the moment, since the Yubico 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 = "hotp" otplen = 6 serial = "UBOM{0:08d}_{1!s}".format(serial_int, slot) TOKENS[serial] = {'type': ttype, 'otpkey': key, 'otplen': otplen, 'description': public_id } else: log.warning("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 = "hotp" serial = "UBOM{0!s}_{1!s}".format(serial, slot) otplen = 6 elif l[2].strip() == "": # Static typ = "pw" serial = "UBSM{0!s}_{1!s}".format(serial, slot) key = _create_static_password(key) otplen = len(key) log.warning("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{0!s}_{1!s}".format(serial, slot) public_id = l[1].strip() otplen = 32 + len(public_id) TOKENS[serial] = {'type': typ, 'otpkey': key, 'otplen': otplen, 'description': public_id } else: log.warning("the line {0!r} did not contain a enough values".format(line)) continue 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, 'otpkey' : xxxx, 'otplen' : xxx, 'description' : xxx } } ''' TOKENS = {} csv_array = csv.split('\n') log.debug("the file contains {0:d} tokens.".format(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 == "": # Usually a "static password" does not have a public ID! # So we would bail out here for static passwords. log.warning("No public ID in line {0!r}".format(line)) continue serial_int = int(binascii.hexlify(modhex_decode(public_id)), 16) if typ.lower() == "yubico otp": ttype = "yubikey" otplen = 32 + len(public_id) serial = "UBAM{0:08d}_{1!s}".format(serial_int, slot) TOKENS[serial] = { 'type': ttype, 'otpkey': key, 'otplen': otplen, 'description': public_id } elif typ.lower() == "oath-hotp": ''' WARNING: this does not work out at the moment, since the Yubico 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 = "hotp" otplen = 6 serial = "UBOM{0:08d}_{1!s}".format(serial_int, slot) TOKENS[serial] = { 'type': ttype, 'otpkey': key, 'otplen': otplen, 'description': public_id } else: log.warning("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 = "hotp" serial = "UBOM{0!s}_{1!s}".format(serial, slot) otplen = 6 elif l[2].strip() == "": # Static typ = "pw" serial = "UBSM{0!s}_{1!s}".format(serial, slot) key = _create_static_password(key) otplen = len(key) log.warning("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{0!s}_{1!s}".format(serial, slot) public_id = l[1].strip() otplen = 32 + len(public_id) TOKENS[serial] = { 'type': typ, 'otpkey': key, 'otplen': otplen, 'description': public_id } else: log.warning( "the line {0!r} did not contain a enough values".format(line)) continue return TOKENS
def check_otp(self, anOtpVal, counter=None, window=None, options=None): """ validate the token otp against a given otpvalue :param anOtpVal: the to be verified otpvalue :type anOtpVal: 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 """ res = -1 serial = self.token.serial secret = self.token.get_otpkey() # The prefix is the characters in front of the last 32 chars yubi_prefix = anOtpVal[:-32] # The variable otp val is the last 32 chars yubi_otp = anOtpVal[-32:] try: otp_bin = modhex_decode(yubi_otp) except KeyError: # The OTP value is no yubikey aes otp value and can not be decoded return -4 msg_bin = secret.aes_decrypt(otp_bin) msg_hex = binascii.hexlify(msg_bin) # 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). log.debug("calculated checksum (61624): {0!r}".format( checksum(msg_hex))) if checksum(msg_hex) != 0xf0b8: # pragma: no cover log.warning("CRC checksum for token {0!r} failed".format(serial)) return -3 uid = msg_hex[0:12] log.debug("uid: {0!r}".format(uid)) log.debug("prefix: {0!r}".format( binascii.hexlify(modhex_decode(yubi_prefix)))) # usage_counter can go from 1 – 0x7fff usage_counter = msg_hex[12:16] timestamp = msg_hex[16:22] # session counter can go from 00 to 0xff session_counter = msg_hex[22:24] random = msg_hex[24:28] crc = msg_hex[28:] log.debug("decrypted: usage_count: {0!r}, session_count: {1!r}".format( usage_counter, session_counter)) # 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('decrypted counter: {0!r}'.format(count_int)) tokenid = self.get_tokeninfo("yubikey.tokenid") if not tokenid: log.debug("Got no tokenid for {0!r}. Setting to {1!r}.".format( serial, uid)) tokenid = uid self.add_tokeninfo("yubikey.tokenid", tokenid) prefix = self.get_tokeninfo("yubikey.prefix") if not prefix: log.debug("Got no prefix for {0!r}. Setting to {1!r}.".format( serial, yubi_prefix)) self.add_tokeninfo("yubikey.prefix", yubi_prefix) if tokenid != uid: # wrong token! log.warning("The wrong token was presented for %r. " "Got %r, expected %r." % (serial, uid, tokenid)) return -2 # TODO: We also could check the timestamp # see http://www.yubico.com/wp-content/uploads/2013/04/YubiKey-Manual-v3_1.pdf log.debug('compare counter to database counter: {0!r}'.format( self.token.count)) if count_int >= self.token.count: res = count_int # on success we save the used counter self.inc_otp_counter(res) return res
def check_otp(self, anOtpVal, counter=None, window=None, options=None): """ validate the token otp against a given otpvalue :param anOtpVal: the to be verified otpvalue :type anOtpVal: 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 """ res = -1 serial = self.token.serial secret = self.token.get_otpkey() # The prefix is the characters in front of the last 32 chars yubi_prefix = anOtpVal[:-32] # The variable otp val is the last 32 chars yubi_otp = anOtpVal[-32:] # TODO: We can also check the PREFIX! At the moment, we do not use it! try: otp_bin = modhex_decode(yubi_otp) except KeyError: # The OTP value is no yubikey aes otp value and can not be decoded return -4 msg_bin = secret.aes_decrypt(otp_bin) msg_hex = binascii.hexlify(msg_bin) # 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). log.debug("calculated checksum (61624): %r" % checksum(msg_hex)) if checksum(msg_hex) != 0xf0b8: # pragma: no cover log.warning("CRC checksum for token %r failed" % serial) return -3 uid = msg_hex[0:12] log.debug("uid: %r" % uid) log.debug("prefix: %r" % binascii.hexlify(modhex_decode(yubi_prefix))) # usage_counter can go from 1 – 0x7fff usage_counter = msg_hex[12:16] timestamp = msg_hex[16:22] # session counter can go from 00 to 0xff session_counter = msg_hex[22:24] random = msg_hex[24:28] crc = msg_hex[28:] log.debug("decrypted: usage_count: %r, session_count: %r" % (usage_counter, session_counter)) # 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('decrypted counter: %r' % count_int) tokenid = self.get_tokeninfo("yubikey.tokenid") if not tokenid: log.debug("Got no tokenid for %r. Setting to %r." % (serial, uid)) tokenid = uid self.add_tokeninfo("yubikey.tokenid", tokenid) if tokenid != uid: # wrong token! log.warning("The wrong token was presented for %r. Got %r, expected %r." % (serial, uid, tokenid)) return -2 # TODO: We also could check the timestamp # see http://www.yubico.com/wp-content/uploads/2013/04/YubiKey-Manual-v3_1.pdf log.debug('compare counter to database counter: %r' % self.token.count) if count_int >= self.token.count: res = count_int # on success we save the used counter self.inc_otp_counter(res) return res
def check_yubikey_pass(passw): """ This only works without a PIN! This checks the output of a yubikey in AES mode without providing the serial number. The first 12 (of 44) or 16 of 48) characters are the tokenid, which is stored in the tokeninfo. :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 """ opt = {} res = False token_list = [] # strip the yubico OTP and the PIN modhex_serial = passw[:-32][-16:] try: serialnum = "UBAM" + modhex_decode(modhex_serial) except TypeError as exx: # pragma: no cover 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)) from privacyidea.lib.token import get_tokens from privacyidea.lib.token import check_token_list for serial in serials: tokenobject_list = get_tokens(serial=serial) token_list.extend(tokenobject_list) if len(token_list) == 0: opt['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) = check_token_list(token_list, passw) # Now we need to get the user # TODO: Migration #if res is not False and 'serial' in c.audit: # serial = c.audit.get('serial', None) # if serial is not None: # user = getTokenOwner(serial) # c.audit['user'] = user.login # c.audit['realm'] = user.realm # opt = {} # opt['user'] = user.login # opt['realm'] = user.realm # opt['serial'] = serial return res, opt