Added initial objects

This commit is contained in:
Felipe 2013-07-04 10:48:37 -04:00
parent fb7bad1a96
commit c30449c4e0
6 changed files with 222 additions and 35 deletions

36
.gitignore vendored
View File

@ -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/

1
MANIFEST.in Normal file
View File

@ -0,0 +1 @@
include requirements.txt

View File

@ -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('<INSERT OTP HERE>')
# yubi.id and yubi.key are now set
```
## Check valid OTP
```
result = yubi.verify('<INSERT ANOHTER OTP HERE>')
# 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.

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
requests==1.2.5

37
setup.py Normal file
View File

@ -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',
]
)

150
yubikey.py Normal file
View File

@ -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