From c30449c4e0dce5d6bd17df995663f80de1fcb2e9 Mon Sep 17 00:00:00 2001 From: Felipe Date: Thu, 4 Jul 2013 10:48:37 -0400 Subject: [PATCH] Added initial objects --- .gitignore | 36 +----------- MANIFEST.in | 1 + README.md | 32 ++++++++++ requirements.txt | 1 + setup.py | 37 ++++++++++++ yubikey.py | 150 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 222 insertions(+), 35 deletions(-) create mode 100644 MANIFEST.in create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 yubikey.py diff --git a/.gitignore b/.gitignore index d2d6f36..98d45c9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,35 +1 @@ -*.py[cod] - -# C extensions -*.so - -# Packages -*.egg -*.egg-info -dist -build -eggs -parts -bin -var -sdist -develop-eggs -.installed.cfg -lib -lib64 - -# Installer logs -pip-log.txt - -# Unit test / coverage reports -.coverage -.tox -nosetests.xml - -# Translations -*.mo - -# Mr Developer -.mr.developer.cfg -.project -.pydevproject +.c9revisions/ \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..540b720 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include requirements.txt \ No newline at end of file diff --git a/README.md b/README.md index 2026553..ebd5cc0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,34 @@ python-yubikey ============== + +A simple wrapper to use your yubikey with the yubico API. + + +## Install the lib + +``` +pip install python-yubikey +``` + + +## Register for an API key + +``` +import yubikey + +yubi = Yubikey() +yubi.register('') +# yubi.id and yubi.key are now set +``` + +## Check valid OTP + +``` +result = yubi.verify('') +# True / False +``` + +# NO WARRANTY +THE PROGRAM IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, BUT WITHOUT ANY WARRANTY. IT IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW THE AUTHOR WILL BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d7b8d23 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +requests==1.2.5 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..e69a9dc --- /dev/null +++ b/setup.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from setuptools import setup, find_packages +import re +import os +import sys + + +if sys.argv[-1] == 'publish': + os.system("python setup.py sdist upload") + args = {'version': version} + print "You probably want to also tag the version now:" + print " git tag -a %(version)s -m 'version %(version)s'" % args + print " git push --tags" + sys.exit() + + +setup( + name='python-yubikey', + version='0.1.0', + url='http://github.com/fmartingr/python-yubikey', + license='GPLv2', + description='Simple Yubico API Wrappers', + author='Felipe Martin', + author_email='fmartingr@me.com', + py_modules=['yubikey'], + include_package_data=True, + zip_safe=False, + install_requires=open('requirements.txt').read().split('\n'), + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + ] +) \ No newline at end of file diff --git a/yubikey.py b/yubikey.py new file mode 100644 index 0000000..bbb48c0 --- /dev/null +++ b/yubikey.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python +import requests +import string +from random import choice + + +class YubicoWS(object): + register_ws = 'https://upgrade.yubico.com/getapikey/?format=json' + _api_ws = 'http://%s/wsapi/2.0/' + api_ws = None + + _servers = [ + 'api.yubico.com', + 'api2.yubico.com', + 'api3.yubico.com', + 'api4.yubico.com', + 'api5.yubico.com', + ] + + _errors = { + 'BAD_OTP': 'The OTP is invalid format.', + 'REPLAYED_OTP': 'The OTP has already been seen by the service.', + 'BAD_SIGNATURE': 'The HMAC signature verification failed.', + 'MISSING_PARAMETER': 'The request lacks a parameter.', + 'NO_SUCH_CLIENT': 'The request id does not exist.', + 'OPERATION_NOT_ALLOWED': 'The request id is not allowed to verify OTPs.', + 'BACKEND_ERROR': 'Unexpected error in our server. Please contact us if you see this error.', + 'NOT_ENOUGH_ANSWERS': 'Server could not get requested number of syncs during before timeout', + 'REPLAYED_REQUEST': 'Server has seen the OTP/Nonce combination before', + } + + def __init__(self): + self.select_random_server() + + def select_random_server(self): + "Select random API Server" + self.api_ws = self._api_ws % choice(self._servers) + + def register_api_key(self, email, otp): + data = { + 'email': email, + 'otp': str.lower(otp) + } + response = requests.post(self.register_ws, data) + ws_response = response.json() + if not ws_response['status']: + raise WSError(ws_response['error']) + + return ws_response + + def verify(self, yubikey_id, otp): + endpoint = 'verify' + url = self.api_ws + endpoint + + # Check otp format + if not (len(otp) > 32 and len(otp) < 48): + raise OTPIncorrectFormat() + + nonce = self.generate_nonce() + + data = { + 'id': int(yubikey_id), + 'otp': str.lower(otp), + 'nonce': nonce + } + + response = requests.get(url, params=data) + + ws_response = self.parse_ws_response(response.text) + print(ws_response) + if ws_response['status'] == 'OK': + # Check if response is valid + if not (ws_response['nonce'] == nonce \ + and ws_response['otp'] != otp \ + and True): + raise WSInvalidResponse() + else: + raise WSError(self._errors[ws_response['status']]) + + return ws_response + + def parse_ws_response(self, text): + data = {} + for line in text.strip().split('\n'): + key, value = line.split('=', 1) + data[key] = value + return data + + def generate_nonce(self): + chars = string.ascii_lowercase + string.digits + return ''.join(choice(chars) for x in range(40)) + + +class Yubikey(object): + id = None + key = None + prefix = None + + _last_result = False + + def __init__(self, yubikey_id=None): + self.ws = YubicoWS() + if yubikey_id: + self.id = yubikey_id + + def register(self, email, otp): + result = False + if not self.id: + credentials = self.ws.register_api_key(email, otp) + if credentials['status']: + self.id = credentials['id'] + self.key = credentials['key'] + result = True + + return result + + def verify(self, otp): + result = False + if self.id: + self.get_prefix(otp) + result = self.ws.verify(self.id, otp) + if result == 'OK': + result = True + + self._last_result = result + return result + + def get_prefix(self, otp): + if len(otp) > 32: + self.prefix = str.lower(otp[:-32]) + + +class WSError(Exception): + def __init__(self, message=None): + self.msg = "Web Service responded with an error: %s" % message + + def __str__(self): + return repr(self.msg) + + +class WSInvalidResponse(Exception): + msg = 'Response from the server is invalid' + + +class WSResponseError(Exception): + def __str__(self): + return repr(self.msg) + +class OTPIncorrectFormat(Exception): + pass