# -*- coding: utf-8 -*-
#
# LinOTP - the open source solution for two factor authentication
# Copyright (C) 2010 - 2014 LSE Leading Security Experts GmbH
#
# This file is part of LinOTP server.
#
# This program is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public
# License, version 3, as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the
# GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#
# E-mail: linotp@lsexperts.de
# Contact: www.linotp.org
# Support: www.lsexperts.de
#
""" This file contains the Yubikey token class where the Yubikey is
run in Yubico Mode"""
import logging
import traceback
from Crypto.Cipher import AES
import binascii
optional = True
required = False
from linotp.lib.validate import check_pin
from linotp.lib.tokenclass import TokenClass
from linotp.lib.util import modhex_decode
from linotp.lib.util import checksum
log = logging.getLogger(__name__)
###############################################
[docs]class YubikeyTokenClass(TokenClass):
"""
The Yubikey Token in the Yubico AES mode
"""
def __init__(self, aToken):
TokenClass.__init__(self, aToken)
self.setType(u"yubikey")
self.hKeyRequired = True
return
@classmethod
[docs] def getClassType(cls):
return "yubikey"
@classmethod
[docs] def getClassPrefix(cls):
return "UBAM"
@classmethod
[docs] def getClassInfo(cls, key=None, ret='all'):
"""
getClassInfo - returns a subtree of the token definition
:param key: subsection identifier
:type key: string
:param ret: default return value, if nothing is found
:type ret: user defined
:return: subsection if key exists or user defined
:rtype: s.o.
"""
log.debug("[getClassInfo] begin. Get class render info for section: key %r, ret %r " %
(key, ret))
res = {
'type': 'yubikey',
'title': 'Yubikey in Yubico Mode',
'description': ('Yubico token to run the AES OTP mode.'),
'init': {},
'config': {},
'selfservice': {},
'policy': {},
}
if key is not None and key in res:
ret = res.get(key)
else:
if ret == 'all':
ret = res
log.debug("[getClassInfo] end. Returned the configuration section: ret %r " % (ret))
return ret
[docs] def check_otp_exist(self, otp, window=None):
'''
checks if the given OTP value is/are values of this very token.
This is used to autoassign and to determine the serial number of
a token.
'''
res = -1
if window is None:
window = self.getOtpCountWindow()
counter = self.getOtpCount()
res = self.checkOtp(otp, counter=counter, window=window, options=None)
if res >= 0:
# As usually the counter is increased in lib.token.checkUserPass, we
# need to do this manually here:
self.incOtpCounter(res)
return res
[docs] def is_challenge_request(self, passw, user, options=None):
'''
This method checks, if this is a request, that triggers a challenge.
:param passw: password, which might be pin or pin+otp
:type passw: string
:param user: The user from the authentication request
:type user: User object
:param options: dictionary of additional request parameters
:type options: dict
:return: true or false
'''
request_is_valid = False
pin_match = check_pin(self, passw, user=user, options=options)
if pin_match is True:
request_is_valid = True
return request_is_valid
[docs] def resync(self, otp1, otp2, options=None):
"""
resyc the yubikey token
this is done by checking two subsequent otp values for their counter
:param otp1: first otp value
:param otp2: second otp value
:return: boolean
"""
ret = False
syncWindow = self.token.getSyncWindow()
counter = self.token.getOtpCounter()
counter1 = self.checkOtp(otp1, window=syncWindow, options=options)
if counter1 < counter:
return ret
counter2 = self.checkOtp(otp2, counter=counter1, options=options)
if counter1 + 1 == counter2:
ret = True
self.incOtpCounter(counter2, True)
return ret
[docs] def resetTokenInfo(self):
"""
resetTokenInfo - hook called during token init/update
in yubikey we have to reset the tokeninfo as it preserves the
tokenid, which changes with an token update
"""
info = self.getTokenInfo()
if info and "yubikey.tokenid" in info:
del info["yubikey.tokenid"]
self.setTokenInfo(info)
return
[docs] 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()
secret = self.token.getHOtpKey()
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]
# The variable otp val is the last 32 chars
yubi_otp = anOtpVal[-32:]
try:
otp_bin = modhex_decode(yubi_otp)
msg_bin = secret.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