# -*- 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 is used used for importing SafeNet (former Aladdin)
XML files, that hold the OTP secrets for eToken PASS.
"""
import xml.etree.ElementTree as etree
import re
import binascii
from linotp.lib.util import modhex_decode
from linotp.lib.util import modhex_encode
from Crypto.Cipher import AES
import logging
log = logging.getLogger(__name__)
[docs]def getKnownTypes():
return ["feitian", "pskc", "dpw", 'dat', "vasco"]
[docs]def getImportText():
return { 'feitian' : 'Feitian XML',
'pskc' : 'OATH compliant PSKC',
'dpw' : 'Tagespasswort file',
'dat' : 'eToken dat file',
'vasco' : 'Vasco DPX' }
[docs]def create_static_password(key_hex):
'''
According to yubikey manual 5.5.5 the static-ticket is the same algorith with no moving factors.
The msg_hex that is encoded with the aes key is '000000000000ffffffffffffffff0f2e'
'''
msg_hex = "000000000000ffffffffffffffff0f2e"
msg_bin = binascii.unhexlify(msg_hex)
aes = AES.new(binascii.unhexlify(key_hex), AES.MODE_ECB)
password_bin = aes.encrypt(msg_bin)
password = modhex_encode(password_bin)
return password
[docs]class ImportException(Exception):
def __init__(self, description):
#self.auth_scope = scope
#self.auth_action = action
#self.auth_action_desc = action_desc
self.description = description
def __str__(self):
return ('%s' % self.description)
[docs]def getTagName(elem):
match = re.match("^({.*?})(.*)$", elem.tag)
if match:
return match.group(2)
else:
return elem.tag
[docs]def parseOATHcsv(csv):
'''
(#653)
This function parses CSV data for oath token.
The file format is
serial, key, [hotp,totp], [6,8], [30|60],
serial, key, ocra, [ocra-suite]
It imports sha1 hotp or totp token.
I can also import ocra token.
The default is hotp
if totp is set, the default seconds are 30
if ocra is set, an ocra-suite is required, otherwise the default ocra-suite is used.
It returns a dictionary:
{
serial: { 'type' : xxxx,
'hmac_key' : xxxx,
'timeStep' : xxxx,
'otplen' : xxx,
'ocrasuite' : xxx }
}
'''
TOKENS = {}
log.debug("[parseOATHcsv] starting to parse an oath csv file.")
#if type(csv) is str:
csv_array = csv.split('\n')
#else:
# csv_array = csv
log.debug("[parseOATHcsv] the file contains %i tokens." % len(csv_array))
for line in csv_array:
l = line.split(',')
serial = ""
key = ""
ttype = "hmac"
seconds = 30
otplen = 6
hashlib = "sha1"
ocrasuite = ""
if len(l) >= 1:
serial = l[0].strip()
else:
log.error("[parseOATHcsv] the line %s did not contain a serial number" % line)
continue
if len(l) >= 2:
key = l[1].strip()
if len(key) == 32:
hashlib = "sha256"
else:
log.error("[parseOATHcsv] the line %s did not contain a hmac key" % line)
continue
# ttype
if len(l) >= 3:
ttype = l[2].strip().lower()
if ttype == "hotp":
ttype = "hmac"
# otplen or ocrasuite
if len(l) >= 4:
if ttype in ["ocra", "ocra2"]:
ocrasuite = l[3].strip()
else:
otplen = int(l[3].strip())
# timeStep
if len(l) >= 5:
seconds = int(l[4].strip())
log.debug("[parseOATHcsv] read the line |%s|%s|%s|%i %s|%i|" % (serial, key, ttype, otplen, ocrasuite, seconds))
TOKENS[serial] = { 'type' : ttype,
'hmac_key' : key,
'timeStep' : seconds,
'otplen' : otplen,
'hashlib' : hashlib,
'ocrasuite' : ocrasuite
}
log.debug("[parseOATHcsv] read the following values: %s" % str(TOKENS))
return TOKENS
[docs]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)
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%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
[docs]def parseSafeNetXML(xml):
'''
This function parses XML data of a Aladdin/SafeNet XML
file for eToken PASS
It returns a dictionary of
serial : { hmac_key , counter, type }
'''
TOKENS = {}
elem_tokencontainer = etree.fromstring(xml)
if getTagName(elem_tokencontainer) != "Tokens":
raise ImportException("No toplevel element Tokens")
for elem_token in list(elem_tokencontainer):
SERIAL = None
COUNTER = None
HMAC = None
DESCRIPTION = None
if getTagName(elem_token) == "Token":
SERIAL = elem_token.get("serial")
log.debug("Found token with serial %s" % SERIAL)
for elem_tdata in list(elem_token):
tag = getTagName(elem_tdata)
if "ProductName" == tag:
DESCRIPTION = elem_tdata.text
log.debug("The Token with the serial %s has the productname %s" % (SERIAL, DESCRIPTION))
if "Applications" == tag:
for elem_apps in elem_tdata:
if getTagName(elem_apps) == "Application":
for elem_app in elem_apps:
tag = getTagName(elem_app)
if "Seed" == tag:
HMAC = elem_app.text
if "MovingFactor" == tag:
COUNTER = elem_app.text
if not SERIAL:
log.error("Found token without a serial")
else:
if HMAC:
hashlib = "sha1"
if len(HMAC) == 64:
hashlib = "sha256"
TOKENS[SERIAL] = {
'hmac_key' : HMAC,
'counter' : COUNTER,
'type' : 'HMAC',
'hashlib' : hashlib
}
else:
log.error("Found token %s without a element 'Seed'" % SERIAL)
return TOKENS