YubiOTP-1.0.0/ 0000755 0000766 0000024 00000000000 13715344434 013603 5 ustar psagers staff 0000000 0000000 YubiOTP-1.0.0/CHANGES.rst 0000644 0000766 0000024 00000001400 13715344246 015401 0 ustar psagers staff 0000000 0000000 Change Log
==========
v1.0.0 - August 13, 2020 - Drop Python 2 support
-------------------------------------------------------------------------------
- Dropped support for Python 2 and removed six.
v0.2.2 - July 20, 2018 - Switch to pycryptodome
-----------------------------------------------
- Switch from the deprecated pycrypto to pycryptodome.
- Update supported Python versions.
v0.2.1 - September 10, 2013 - Python 3 compatibility
----------------------------------------------------
- Updated for Python 2/3 compatibility.
v0.2.0 - July 21, 2012 - yubiclient
-----------------------------------
- Added yubiclient, the Yubico web service client.
v0.1.0 - July 12, 2012 - Initial release
----------------------------------------
Initial release
YubiOTP-1.0.0/LICENSE 0000644 0000766 0000024 00000002421 13527557161 014613 0 ustar psagers staff 0000000 0000000 Copyright (c) 2012, Peter Sagerson
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
YubiOTP-1.0.0/MANIFEST.in 0000644 0000766 0000024 00000000145 13530034557 015336 0 ustar psagers staff 0000000 0000000 include CHANGES.rst LICENSE README.rst
recursive-include docs *.rst *.py Makefile
prune docs/build
YubiOTP-1.0.0/PKG-INFO 0000644 0000766 0000024 00000003762 13715344434 014710 0 ustar psagers staff 0000000 0000000 Metadata-Version: 1.2
Name: YubiOTP
Version: 1.0.0
Summary: A library for verifying YubiKey OTP tokens, both locally and through a Yubico web service.
Home-page: https://github.com/django-otp/yubiotp
Author: Peter Sagerson
Author-email: psagers@ignorare.net
License: BSD
Project-URL: Documentation, https://yubiotp.readthedocs.io/
Project-URL: Source, https://github.com/django-otp/yubiotp
Description: .. image:: https://img.shields.io/pypi/v/yubiotp?color=blue
:target: https://pypi.org/project/yubiotp/
:alt: PyPI
.. image:: https://img.shields.io/readthedocs/yubiotp
:target: https://yubiotp.readthedocs.io/
:alt: Documentation
.. image:: https://img.shields.io/badge/github-yubiotp-green
:target: https://github.com/django-otp/yubiotp
:alt: Source
This is a library for verifying `YubiKey `_ OTP tokens.
It includes both the low-level implementation for verifying tokens locally and
clients for multiple versions of the Yubico validation web service. The primary
audience is developers who wish to verify YubiKey tokens in their applications,
presumably as part of a multi-factor authentication scheme.
For testing and experimenting, the included ``yubikey`` script simulates one or
more YubiKey devices using a config file. It also includes utility commands
such as a modhex converter. See ``yubikey -h`` for details.
This also includes a command-line web service client called ``yubiclient``. See
``yubiclient -h`` for details.
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
YubiOTP-1.0.0/README.rst 0000644 0000766 0000024 00000002066 13531027515 015270 0 ustar psagers staff 0000000 0000000 .. image:: https://img.shields.io/pypi/v/yubiotp?color=blue
:target: https://pypi.org/project/yubiotp/
:alt: PyPI
.. image:: https://img.shields.io/readthedocs/yubiotp
:target: https://yubiotp.readthedocs.io/
:alt: Documentation
.. image:: https://img.shields.io/badge/github-yubiotp-green
:target: https://github.com/django-otp/yubiotp
:alt: Source
This is a library for verifying `YubiKey `_ OTP tokens.
It includes both the low-level implementation for verifying tokens locally and
clients for multiple versions of the Yubico validation web service. The primary
audience is developers who wish to verify YubiKey tokens in their applications,
presumably as part of a multi-factor authentication scheme.
For testing and experimenting, the included ``yubikey`` script simulates one or
more YubiKey devices using a config file. It also includes utility commands
such as a modhex converter. See ``yubikey -h`` for details.
This also includes a command-line web service client called ``yubiclient``. See
``yubiclient -h`` for details.
YubiOTP-1.0.0/YubiOTP.egg-info/ 0000755 0000766 0000024 00000000000 13715344434 016570 5 ustar psagers staff 0000000 0000000 YubiOTP-1.0.0/YubiOTP.egg-info/PKG-INFO 0000644 0000766 0000024 00000003762 13715344434 017675 0 ustar psagers staff 0000000 0000000 Metadata-Version: 1.2
Name: YubiOTP
Version: 1.0.0
Summary: A library for verifying YubiKey OTP tokens, both locally and through a Yubico web service.
Home-page: https://github.com/django-otp/yubiotp
Author: Peter Sagerson
Author-email: psagers@ignorare.net
License: BSD
Project-URL: Documentation, https://yubiotp.readthedocs.io/
Project-URL: Source, https://github.com/django-otp/yubiotp
Description: .. image:: https://img.shields.io/pypi/v/yubiotp?color=blue
:target: https://pypi.org/project/yubiotp/
:alt: PyPI
.. image:: https://img.shields.io/readthedocs/yubiotp
:target: https://yubiotp.readthedocs.io/
:alt: Documentation
.. image:: https://img.shields.io/badge/github-yubiotp-green
:target: https://github.com/django-otp/yubiotp
:alt: Source
This is a library for verifying `YubiKey `_ OTP tokens.
It includes both the low-level implementation for verifying tokens locally and
clients for multiple versions of the Yubico validation web service. The primary
audience is developers who wish to verify YubiKey tokens in their applications,
presumably as part of a multi-factor authentication scheme.
For testing and experimenting, the included ``yubikey`` script simulates one or
more YubiKey devices using a config file. It also includes utility commands
such as a modhex converter. See ``yubikey -h`` for details.
This also includes a command-line web service client called ``yubiclient``. See
``yubiclient -h`` for details.
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
YubiOTP-1.0.0/YubiOTP.egg-info/SOURCES.txt 0000644 0000766 0000024 00000000751 13715344434 020457 0 ustar psagers staff 0000000 0000000 CHANGES.rst
LICENSE
MANIFEST.in
README.rst
setup.cfg
setup.py
YubiOTP.egg-info/PKG-INFO
YubiOTP.egg-info/SOURCES.txt
YubiOTP.egg-info/dependency_links.txt
YubiOTP.egg-info/requires.txt
YubiOTP.egg-info/top_level.txt
bin/yubiclient
bin/yubikey
docs/Makefile
docs/source/changes.rst
docs/source/client.rst
docs/source/conf.py
docs/source/index.rst
docs/source/otp.rst
docs/source/utils.rst
yubiotp/__init__.py
yubiotp/client.py
yubiotp/crc.py
yubiotp/modhex.py
yubiotp/otp.py
yubiotp/test.py YubiOTP-1.0.0/YubiOTP.egg-info/dependency_links.txt 0000644 0000766 0000024 00000000001 13715344434 022636 0 ustar psagers staff 0000000 0000000
YubiOTP-1.0.0/YubiOTP.egg-info/requires.txt 0000644 0000766 0000024 00000000015 13715344434 021164 0 ustar psagers staff 0000000 0000000 pycryptodome
YubiOTP-1.0.0/YubiOTP.egg-info/top_level.txt 0000644 0000766 0000024 00000000010 13715344434 021311 0 ustar psagers staff 0000000 0000000 yubiotp
YubiOTP-1.0.0/bin/ 0000755 0000766 0000024 00000000000 13715344434 014353 5 ustar psagers staff 0000000 0000000 YubiOTP-1.0.0/bin/yubiclient 0000755 0000766 0000024 00000006374 13527557161 016466 0 ustar psagers staff 0000000 0000000 #!/usr/bin/env python
from __future__ import print_function
import sys
from base64 import b64decode
from optparse import OptionParser
from yubiotp.client import YubiClient10, YubiClient11, YubiClient20
def main():
options, args = parse_args()
api_key = b64decode(options.api_key.encode()) if (options.api_key is not None) else None
if options.version == '1.0':
client = YubiClient10(options.api_id, api_key, options.ssl)
elif options.version == '1.1':
client = YubiClient11(options.api_id, api_key, options.ssl, options.timestamp)
elif options.version == '2.0':
client = YubiClient20(options.api_id, api_key, options.ssl, options.timestamp, options.sl, options.timeout)
if options.base_url:
client.base_url = options.base_url
is_valid = True
for token in args:
if options.noexec:
print(client.url(token))
else:
if not verify_otp(client, token, options.verbose):
is_valid = False
sys.exit(0 if is_valid else 2)
def parse_args():
parser = OptionParser(
usage='%prog [options] otp ...',
description="Verifies one or more YubiKey OTP tokens against a YubiCloud service. If you don't supply an API id and key, signatures will be ignored."
)
parser.add_option('-v', '--verbose', action='store_true', dest='verbose', help="Print request URL and full responst to stderr.")
parser.add_option('-n', '--noexec', action='store_true', dest='noexec', help="Don't send the request, just print the URL.")
parser.add_option('-V', '--version', dest='version', type='choice', choices=['1.0', '1.1', '2.0'], default='2.0', help="The API version to use (1.0, 1.1, 2.0). [%default]")
parser.add_option('-u', '--base-url', dest='base_url', help="Base URL of the api. Defaults to a version-appropriate URL on api.yubico.com.")
parser.add_option('-s', '--ssl', action='store_true', dest='ssl', help="Use an https url by default.")
parser.add_option('-i', '--api-id', dest='api_id', default='1', help="Your API ID. [%default]")
parser.add_option('-k', '--api-key', dest='api_key', help="Your base64-encoded API key.")
parser.add_option('-t', '--timestamp', action='store_true', dest='timestamp', help="(Version 1.1+) include timestamp and counter information in response.")
parser.add_option('--sl', dest='sl', help="(Version 2.0+) Request server syncing.")
parser.add_option('--timeout', dest='timeout', help="(Version 2.0+) Seconds to wait for sync response.")
options, args = parser.parse_args()
return options, args
def verify_otp(client, otp, verbose=False):
response = client.verify(otp)
if verbose:
print(client.url(otp), file=sys.stderr)
print(file=sys.stderr)
for line in filter(None, response.raw.splitlines()):
print(line, file=sys.stderr)
print(file=sys.stderr)
if response.public_id is not None:
print('public_id: {0}'.format(response.public_id), file=sys.stderr)
print(file=sys.stderr)
if response.is_ok():
is_valid = True
print('{0}: OK (strict)'.format(otp))
else:
is_valid = False
print('{0}: {1}'.format(otp, response.status()))
return is_valid
if __name__ == '__main__':
main()
YubiOTP-1.0.0/bin/yubikey 0000755 0000766 0000024 00000025055 13527557161 015775 0 ustar psagers staff 0000000 0000000 #!/usr/bin/env python
"""
This is a command-line interface to the yubiotp package. Its primary function
is to simulate a YubiKey device for testing purposes. It also includes some
utilities for things like converting to and from modhex.
"""
from __future__ import print_function
import sys
from os.path import expanduser
from optparse import OptionParser, OptionGroup, Option, OptionValueError
from random import choice
from binascii import hexlify, unhexlify
from six.moves import configparser
from yubiotp.modhex import modhex, unmodhex, modhex_to_hex, hex_to_modhex
from yubiotp.otp import YubiKey, decode_otp, encode_otp
def main():
parser = Handler.global_option_parser()
parser.disable_interspersed_args()
opts, args = parser.parse_args()
if len(args) == 0:
usage('You must choose an action', parser)
handlers = {
'list': ListHandler(),
'init': InitHandler(),
'delete': DeleteHandler(),
'gen': GenHandler(),
'parse': ParseHandler(),
'modhex': ModhexHandler(),
}
handler = handlers.get(args[0])
if handler is None:
usage('Unknown action: {0}'.format(args[0]), parser)
else:
handler.run()
def check_hex_value(option, opt, value):
try:
unhexlify(value.encode())
except TypeError as e:
raise OptionValueError(str(e))
return value
def check_modhex_value(option, opt, value):
try:
unmodhex(value.encode())
except ValueError as e:
raise OptionValueError(str(e))
return value
class YubiKeyOption(Option):
"""
Custom optparse Option class that adds the 'hex' value type. Values of this
type are expected to be strings of hex digits, which will be decoded into
binary strings before being stored.
"""
TYPES = Option.TYPES + ('hex', 'modhex')
TYPE_CHECKER = dict(Option.TYPE_CHECKER,
hex=check_hex_value,
modhex=check_modhex_value)
make_option = YubiKeyOption
class Handler(object):
name = ''
options = [
make_option('-f', '--config', dest='config', default='~/.yubikey', metavar='PATH', help='A config file to store device state. [%default]'),
make_option('-n', '--name', dest='device_name', default='0', metavar='NAME', help='The device number or name to operate on. [%default]'),
]
args = ''
description = 'Simulates one or more YubiKey devices from which you can generate tokens. Also parses tokens for verification. Choose an action for more information.'
def run(self):
parser = self.make_option_parser()
opts, args = parser.parse_args()
self.handle(opts, args)
def make_option_parser(self):
usage = '%prog [global opts] {0} [any opts]'.format(self.name)
if self.args:
usage = usage + ' ' + self.args
parser = OptionParser(usage=usage, description=self.description)
for option in Handler.options:
parser.add_option(option)
if len(self.options) > 0:
group = OptionGroup(parser, self.name)
for option in self.options:
group.add_option(option)
parser.add_option_group(group)
return parser
def handle(self, opts, args):
raise NotImplementedError()
@classmethod
def global_option_parser(cls):
parser = OptionParser(
usage='%prog [global opts] [any opts] [args]',
description=cls.description,
)
for option in cls.options:
parser.add_option(option)
return parser
@staticmethod
def random_hex(count):
return ''.join(choice('0123456789abcdef') for i in range(count * 2))
class ListHandler(Handler):
name = 'list'
options = []
args = ''
description = 'List all virtual YubiKey devices.'
def handle(self, opts, args):
config = configparser.SafeConfigParser()
config.read([expanduser(opts.config)])
config.write(sys.stdout)
class InitHandler(Handler):
name = 'init'
options = [
make_option('-p', '--public', dest='public_id', type='modhex', default='', help='A modhex-encoded public ID (up to 16 bytes)'),
make_option('-k', '--key', dest='key', type='hex', help='A hex-encoded 16-byte AES key. If omitted, one will be generated.'),
make_option('-u', '--uid', dest='uid', type='hex', help='A hex-encoded 6-byte private ID. If omitted, one will be generated.'),
make_option('-s', '--session', dest='session', type='int', default=0, help='The initial session counter. [%default]'),
]
description = 'Initialize a new virtual YubiKey.'
def handle(self, opts, args):
if opts.key is None:
opts.key = self.random_hex(16)
if opts.uid is None:
opts.uid = self.random_hex(6)
device = Device(opts.config, opts.device_name)
device.create(opts.public_id, opts.key, opts.uid, opts.session)
device.save()
class DeleteHandler(Handler):
name = 'delete'
options = []
args = ''
description = 'Remove a virtual YubiKey device.'
def handle(self, opts, args):
device = Device(opts.config, opts.device_name)
device.delete()
device.save()
class GenHandler(Handler):
name = 'gen'
options = [
make_option('-c', '--count', dest='count', type='int', default=1, help='Generate multiple tokens. [%default]'),
make_option('-i', '--interactive', action='store_true', dest='interactive', help='Generate a token for every line read from stdin until interrupted.'),
]
args = ''
description = 'Generate one or more tokens from the virtual device. This simulates pressing the YubiKey\'s button.'
def handle(self, opts, args):
device = Device(opts.config, opts.device_name)
for i in range(opts.count):
print(device.gen_token().decode())
if opts.interactive:
try:
while True:
sys.stdin.readline()
print(device.gen_token().decode())
except KeyboardInterrupt:
pass
device.save()
class ParseHandler(Handler):
name = 'parse'
options = []
args = 'token ...'
description = 'Parse tokens generated by the selected virtual device and display its fields.'
def handle(self, opts, args):
device = Device(opts.config, opts.device_name)
key = device.get_config('key', unhex=True)
for token in args[1:]:
try:
public_id, otp = decode_otp(token.encode(), key)
except ValueError as e:
print(e)
else:
print('public_id: {0}'.format(public_id.decode()))
print('uid: {0}'.format(hexlify(otp.uid).decode()))
print('session: {0}'.format(otp.session))
print('timestamp: 0x{0:x}'.format(otp.timestamp))
print('counter: {0}'.format(otp.counter))
print('random: 0x{0:x}'.format(otp.rand))
print()
class ModhexHandler(Handler):
name = 'modhex'
options = [
make_option('-d', '--decode', action='store_true', dest='decode', help='Decode from modhex. Default is to encode to modhex.'),
make_option('-H', '--hex', action='store_true', dest='hex', help='Encode to or decode from a string of hex digits. Default is a raw string.'),
]
args = 'input ...'
description = 'Encode (default) or decode a modhex string.'
def handle(self, opts, args):
for arg in args[1:]:
try:
if opts.decode:
if opts.hex:
print(modhex_to_hex(arg.encode()).decode())
else:
print(unmodhex(arg.encode()).decode())
else:
if opts.hex:
print(hex_to_modhex(arg.encode()).decode())
else:
print(modhex(arg.encode()).decode())
except ValueError as e:
print(e, file=sys.stderr)
def usage(message, parser=None):
print(message)
print()
if parser is not None:
parser.print_help()
sys.exit(1)
class Device(object):
def __init__(self, config_path, name):
self.config_path = expanduser(config_path)
self.name = name
self.section_name = 'device_{0}'.format(name)
self.config = self._load_config()
self.yubikey = None
def _load_config(self):
config = configparser.SafeConfigParser()
config.read([self.config_path])
return config
def create(self, public_id, key, uid, session):
if len(key) != 32:
raise ValueError('AES keys must be exactly 16 bytes')
try:
self.config.add_section(self.section_name)
except configparser.DuplicateSectionError:
usage('A device named "{0}" already exists.'.format(self.name))
else:
self.set_config('public_id', public_id)
self.set_config('key', key)
self.set_config('uid', uid)
self.set_config('session', session)
def delete(self):
try:
self.config.remove_section(self.section_name)
except configparser.NoSectionError:
usage('The device named "{0}" does not exist.'.format(self.name))
def gen_token(self):
self.ensure_yubikey()
otp = self.yubikey.generate()
key = self.get_config('key', unhex=True)
public_id = self.get_config('public_id').encode()
token = encode_otp(otp, key, public_id=public_id)
return token
def ensure_yubikey(self):
if self.yubikey is None:
try:
uid = self.get_config('uid', unhex=True)
session = int(self.get_config('session'))
self.get_config('key')
except Exception as e:
usage('The device named "{0}" does not exist or is corrupt. ({1})'.format(self.name, e))
else:
self.yubikey = YubiKey(uid=uid, session=session)
return self.yubikey
def save(self):
if self.yubikey is not None:
self.set_config('session', self.yubikey.session + 1)
with open(self.config_path, 'w') as f:
self.config.write(f)
def get_config(self, key, unhex=False):
value = self.config.get(self.section_name, key)
if unhex:
value = unhexlify(value.encode())
return value
def set_config(self, key, value):
self.config.set(self.section_name, key, str(value))
if __name__ == '__main__':
main()
YubiOTP-1.0.0/docs/ 0000755 0000766 0000024 00000000000 13715344434 014533 5 ustar psagers staff 0000000 0000000 YubiOTP-1.0.0/docs/Makefile 0000644 0000766 0000024 00000013064 13527557161 016203 0 ustar psagers staff 0000000 0000000 #
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make ' where is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/YubiOTP.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/YubiOTP.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/YubiOTP"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/YubiOTP"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
zip:
rm build/html.zip || true
cd build/html && zip -R ../html.zip '*' -x .buildinfo -x '_sources/*'
YubiOTP-1.0.0/docs/source/ 0000755 0000766 0000024 00000000000 13715344434 016033 5 ustar psagers staff 0000000 0000000 YubiOTP-1.0.0/docs/source/changes.rst 0000644 0000766 0000024 00000000037 13530033744 020167 0 ustar psagers staff 0000000 0000000 .. include:: ../../CHANGES.rst
YubiOTP-1.0.0/docs/source/client.rst 0000644 0000766 0000024 00000001013 13527557161 020042 0 ustar psagers staff 0000000 0000000 Validation Client
=================
.. automodule:: yubiotp.client
Protocol Version 2.0
--------------------
.. autoclass:: YubiClient20
:members: verify, url
Protocol Version 1.1
--------------------
.. autoclass:: YubiClient11
:members: verify, url
Protocol Version 1.0
--------------------
.. autoclass:: YubiClient10
:members: verify, url
Response
--------
.. autoclass:: YubiResponse
:members: is_ok, status, is_valid, is_signature_valid, is_token_valid,
is_nonce_valid, public_id
YubiOTP-1.0.0/docs/source/conf.py 0000644 0000766 0000024 00000017272 13715344427 017345 0 ustar psagers staff 0000000 0000000 # -*- coding: utf-8 -*-
#
# YubiOTP documentation build configuration file, created by
# sphinx-quickstart on Wed Jul 11 10:19:09 2012.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
# sys.path.insert(0, os.path.abspath('.'))
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.viewcode',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'YubiOTP'
copyright = u'2012, Peter Sagerson'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The full version, including alpha/beta/rc tags.
release = '1.0.0'
# The short X.Y version.
version = '.'.join(release.split('.')[:2])
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
# language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
# today = ''
# Else, today_fmt is used as the format for a strftime call.
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = []
# The reST default role (used for this markup: `text`) to use for all documents.
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
# html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# " v documentation".
# html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
# html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
# html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
# html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
# html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
# html_additional_pages = {}
# If false, no module index is generated.
# html_domain_indices = True
# If false, no index is generated.
# html_use_index = True
# If true, the index is split into individual pages for each letter.
# html_split_index = False
# If true, links to the reST sources are added to the pages.
# html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
# html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'YubiOTPdoc'
# -- Options for LaTeX output --------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
# 'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'YubiOTP.tex', u'YubiOTP Documentation',
'Peter Sagerson', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
# latex_use_parts = False
# If true, show page references after internal links.
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
# latex_show_urls = False
# Documents to append as an appendix to all manuals.
# latex_appendices = []
# If false, no module index is generated.
# latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'yubiotp', u'YubiOTP Documentation',
[u'Peter Sagerson'], 1)
]
# If true, show URL addresses after external links.
# man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'YubiOTP', u'YubiOTP Documentation',
'Peter Sagerson', 'YubiOTP', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
# texinfo_appendices = []
# If false, no module index is generated.
# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
# texinfo_show_urls = 'footnote'
YubiOTP-1.0.0/docs/source/index.rst 0000644 0000766 0000024 00000000224 13530033744 017664 0 ustar psagers staff 0000000 0000000 YubiOTP
=======
.. include:: ../../README.rst
.. toctree::
otp
client
utils
changes
License
=======
.. include:: ../../LICENSE
YubiOTP-1.0.0/docs/source/otp.rst 0000644 0000766 0000024 00000000125 13527557161 017371 0 ustar psagers staff 0000000 0000000 OTP API
=======
yubiotp.otp
-----------
.. automodule:: yubiotp.otp
:members:
YubiOTP-1.0.0/docs/source/utils.rst 0000644 0000766 0000024 00000000267 13527557161 017736 0 ustar psagers staff 0000000 0000000 YubiOTP Utilities
=================
yubiotp.modhex
--------------
.. automodule:: yubiotp.modhex
:members:
yubiotp.crc
-----------
.. automodule:: yubiotp.crc
:members:
YubiOTP-1.0.0/setup.cfg 0000644 0000766 0000024 00000000213 13715344434 015420 0 ustar psagers staff 0000000 0000000 [metadata]
long_description = file: README.rst
[flake8]
ignore = E501
[bdist_wheel]
universal = 1
[egg_info]
tag_build =
tag_date = 0
YubiOTP-1.0.0/setup.py 0000755 0000766 0000024 00000002017 13715344427 015322 0 ustar psagers staff 0000000 0000000 #!/usr/bin/env python
from setuptools import setup
setup(
name='YubiOTP',
version='1.0.0',
description='A library for verifying YubiKey OTP tokens, both locally and through a Yubico web service.',
author='Peter Sagerson',
author_email='psagers@ignorare.net',
url='https://github.com/django-otp/yubiotp',
project_urls={
"Documentation": 'https://yubiotp.readthedocs.io/',
"Source": 'https://github.com/django-otp/yubiotp',
},
license='BSD',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3 :: Only',
'Topic :: Security',
'Topic :: Software Development :: Libraries :: Python Modules',
],
packages=[
'yubiotp',
],
scripts=[
'bin/yubikey',
'bin/yubiclient',
],
install_requires=[
'pycryptodome',
],
)
YubiOTP-1.0.0/yubiotp/ 0000755 0000766 0000024 00000000000 13715344434 015276 5 ustar psagers staff 0000000 0000000 YubiOTP-1.0.0/yubiotp/__init__.py 0000644 0000766 0000024 00000000000 13527557161 017401 0 ustar psagers staff 0000000 0000000 YubiOTP-1.0.0/yubiotp/client.py 0000644 0000766 0000024 00000024150 13715343230 017121 0 ustar psagers staff 0000000 0000000 from base64 import b64decode, b64encode
from hashlib import sha1
import hmac
from random import choice
import string
from urllib.parse import urlencode
from urllib.request import urlopen
class YubiClient10(object):
"""
Client for the Yubico validation service, version 1.0.
http://code.google.com/p/yubikey-val-server-php/wiki/ValidationProtocolV10
:param int api_id: Your API id.
:param bytes api_key: Your base64-encoded API key.
:param bool ssl: ``True`` if we should use https URLs by default.
.. attribute:: base_url
The base URL of the validation service. Set this if you want to use a
custom validation service. Defaults to
``'http[s]://api.yubico.com/wsapi/verify'``.
"""
_NONCE_CHARS = string.ascii_letters + string.digits
def __init__(self, api_id=1, api_key=None, ssl=False):
self.api_id = api_id
self.api_key = api_key
self.ssl = ssl
def verify(self, token):
"""
Verify a single Yubikey OTP against the validation service.
:param str token: A modhex-encoded YubiKey OTP, as generated by a YubiKey
device.
:returns: A response from the validation service.
:rtype: :class:`YubiResponse`
"""
nonce = self.nonce()
url = self.url(token, nonce)
stream = urlopen(url)
response = YubiResponse(stream.read().decode('utf-8'), self.api_key, token, nonce)
stream.close()
return response
def url(self, token, nonce=None):
"""
Generates the validation URL without sending a request.
:param str token: A modhex-encoded YubiKey OTP, as generated by a
YubiKey.
:param str nonce: A nonce string, or ``None`` to generate a random one.
:returns: The URL that we would use to validate the token.
:rtype: str
"""
if nonce is None:
nonce = self.nonce()
return '{0}?{1}'.format(self.base_url, self.param_string(token, nonce))
_base_url = None
@property
def base_url(self):
if self._base_url is None:
self._base_url = self.default_base_url()
return self._base_url
@base_url.setter
def base_url(self, url):
self._base_url = url
@base_url.deleter
def base_url(self):
delattr(self, '_base_url')
def default_base_url(self):
if self.ssl:
return 'https://api.yubico.com/wsapi/verify'
else:
return 'http://api.yubico.com/wsapi/verify'
def nonce(self):
return ''.join(choice(self._NONCE_CHARS) for i in range(32))
def param_string(self, token, nonce):
params = self.params(token, nonce)
if self.api_key is not None:
signature = param_signature(params, self.api_key)
params.append(('h', b64encode(signature)))
return urlencode(params)
def params(self, token, nonce):
return [
('id', self.api_id),
('otp', token),
]
class YubiClient11(YubiClient10):
"""
Client for the Yubico validation service, version 1.1.
http://code.google.com/p/yubikey-val-server-php/wiki/ValidationProtocolV11
:param int api_id: Your API id.
:param bytes api_key: Your base64-encoded API key.
:param bool ssl: ``True`` if we should use https URLs by default.
:param bool timestamp: ``True`` if we want the server to include timestamp
and counter information in the response.
.. attribute:: base_url
The base URL of the validation service. Set this if you want to use a
custom validation service. Defaults to
``'http[s]://api.yubico.com/wsapi/verify'``.
"""
def __init__(self, api_id=1, api_key=None, ssl=False, timestamp=False):
super(YubiClient11, self).__init__(api_id, api_key, ssl)
self.timestamp = timestamp
def params(self, token, nonce):
params = super(YubiClient11, self).params(token, nonce)
if self.timestamp:
params.append(('timestamp', '1'))
return params
class YubiClient20(YubiClient11):
"""
Client for the Yubico validation service, version 2.0.
http://code.google.com/p/yubikey-val-server-php/wiki/ValidationProtocolV20
:param int api_id: Your API id.
:param bytes api_key: Your base64-encoded API key.
:param bool ssl: ``True`` if we should use https URLs by default.
:param bool timestamp: ``True`` if we want the server to include timestamp
and counter information in the response.
:param sl: See protocol spec.
:param timeout: See protocol spec.
.. attribute:: base_url
The base URL of the validation service. Set this if you want to use a
custom validation service. Defaults to
``'http[s]://api.yubico.com/wsapi/2.0/verify'``.
"""
def __init__(self, api_id=1, api_key=None, ssl=False, timestamp=False, sl=None, timeout=None):
super(YubiClient20, self).__init__(api_id, api_key, ssl, timestamp)
self.sl = sl
self.timeout = timeout
def default_base_url(self):
if self.ssl:
return 'https://api.yubico.com/wsapi/2.0/verify'
else:
return 'http://api.yubico.com/wsapi/2.0/verify'
def params(self, token, nonce):
params = super(YubiClient20, self).params(token, nonce)
params.append(('nonce', nonce))
if self.sl is not None:
params.append(('sl', self.sl))
if self.timeout is not None:
params.append(('timeout', self.timeout))
return params
class YubiResponse(object):
"""
A response from the Yubico validation service.
.. attribute:: fields
A dictionary of the response fields (excluding 'h').
"""
def __init__(self, raw, api_key, token, nonce):
self.raw = raw
self.api_key = api_key
self.token = token
self.nonce = nonce
self.fields = {}
self.signature = None
self._parse_response()
def _parse_response(self):
self.fields = dict(tuple(line.split('=', 1)) for line in self.raw.splitlines() if '=' in line)
if 'h' in self.fields:
self.signature = b64decode(self.fields['h'].encode())
del self.fields['h']
def is_ok(self):
"""
Returns true if all validation checks pass and the status is 'OK'.
:rtype: bool
"""
return self.is_valid() and (self.fields.get('status') == 'OK')
def status(self):
"""
If the response is valid, this returns the value of the status field.
Otherwise, it returns the special status ``'BAD_RESPONSE'``
"""
status = self.fields.get('status')
if status == 'BAD_SIGNATURE' or self.is_valid(strict=False):
return status
else:
return 'BAD_RESPONSE'
def is_valid(self, strict=True):
"""
Performs all validity checks (signature, token, and nonce).
:param bool strict: If ``True``, all validity checks must pass
unambiguously. Otherwise, this only requires that no validity check
fails.
:returns: ``True`` if none of the validity checks fail.
:rtype: bool
"""
results = [
self.is_signature_valid(),
self.is_token_valid(),
self.is_nonce_valid(),
]
if strict:
is_valid = all(results)
else:
is_valid = False not in results
return is_valid
def is_signature_valid(self):
"""
Validates the response signature.
:returns: ``True`` if the signature is valid or if we did not sign the
request. ``False`` if the signature is invalid.
:rtype: bool
"""
if self.api_key is not None:
signature = param_signature(self.fields.items(), self.api_key)
is_valid = (signature == self.signature)
else:
is_valid = True
return is_valid
def is_token_valid(self):
"""
Validates the otp token sent in the response.
:returns: ``True`` if the token in the response is the same as the one
in the request; ``False`` if not; ``None`` if the response does not
contain a token.
:rtype: bool for a positive result or ``None`` for an ambiguous result.
"""
if 'otp' in self.fields:
is_valid = (self.fields['otp'] == self.token)
else:
is_valid = None
return is_valid
def is_nonce_valid(self):
"""
Validates the nonce value sent in the response.
:returns: ``True`` if the nonce in the response matches the one we sent
(or didn't send). ``False`` if the two do not match. ``None`` if we
sent a nonce and did not receive one in the response: this is often
true of error responses.
:rtype: bool for a positive result or ``None`` for an ambiguous result.
"""
reply = self.fields.get('nonce')
if (self.nonce is not None) and (reply is None):
is_valid = None
else:
is_valid = (reply == self.nonce)
return is_valid
@property
def public_id(self):
"""
Returns the public id of the response token as a modhex string.
:rtype: str or ``None``.
"""
try:
public_id = self.fields['otp'][:-32]
except KeyError:
public_id = None
return public_id
def param_signature(params, api_key):
"""
Returns the signature over a list of Yubico validation service parameters.
Note that the signature algorithm packs the paramters into a form similar
to URL parameters, but without any escaping.
:param params: An association list of parameters, such as you would give to
urllib.urlencode.
:type params: list of 2-tuples
:param bytes api_key: The Yubico API key (raw, not base64-encoded).
:returns: The parameter signature (raw, not base64-encoded).
:rtype: bytes
"""
param_string = '&'.join('{0}={1}'.format(k, v) for k, v in sorted(params))
signature = hmac.new(api_key, param_string.encode('utf-8'), sha1).digest()
return signature
YubiOTP-1.0.0/yubiotp/crc.py 0000644 0000766 0000024 00000001627 13715343230 016416 0 ustar psagers staff 0000000 0000000 """
CRC16 implementation for Yubico OTP.
"""
def crc16(data):
"""
Generate the crc-16 value for a byte string.
>>> from binascii import unhexlify
>>> c = crc16(unhexlify(b'8792ebfe26cc130030c20011c89f'))
>>> hex(~c & 0xffff)
'0xc823'
>>> v = crc16(unhexlify(b'8792ebfe26cc130030c20011c89f23c8'))
>>> hex(v)
'0xf0b8'
"""
crc = 0xffff
for byte in iter(data):
crc ^= byte
for i in range(8):
lsb = crc & 1
crc >>= 1
if lsb == 1:
crc ^= 0x8408
return crc
def verify_crc16(data):
"""
Return true if this given byte string has a valid crc-16 residual.
>>> from binascii import unhexlify
>>> verify_crc16(unhexlify(b'8792ebfe26cc130030c20011c89f23c8'))
True
>>> verify_crc16(unhexlify(b'0792ebfe26cc130030c20011c89f23c8'))
False
"""
return crc16(data) == 0xf0b8
YubiOTP-1.0.0/yubiotp/modhex.py 0000644 0000766 0000024 00000005443 13715343230 017133 0 ustar psagers staff 0000000 0000000 """
Implementation of `modhex encoding `_,
which uses keyboard-independent characters.
::
hex digit: 0123456789abcdef
modhex digit: cbdefghijklnrtuv
"""
from binascii import hexlify, unhexlify
from functools import partial
import struct
__all__ = ['modhex', 'unmodhex', 'is_modhex', 'hex_to_modhex', 'modhex_to_hex']
def modhex(data):
"""
Encode a string of bytes as modhex.
>>> modhex(b'abcdefghijklmnop') == b'hbhdhehfhghhhihjhkhlhnhrhthuhvic'
True
"""
return hex_to_modhex(hexlify(data))
def unmodhex(encoded):
"""
Decode a modhex string to its binary form.
>>> unmodhex(b'hbhdhehfhghhhihjhkhlhnhrhthuhvic') == b'abcdefghijklmnop'
True
"""
return unhexlify(modhex_to_hex(encoded))
def is_modhex(encoded):
"""
Returns ``True`` iff the given string is valid modhex.
>>> is_modhex(b'cbdefghijklnrtuv')
True
>>> is_modhex(b'cbdefghijklnrtuvv')
False
>>> is_modhex(b'cbdefghijklnrtuvyy')
False
"""
if any(c not in modhex_chars for c in encoded):
return False
elif len(encoded) % 2 != 0:
return False
else:
return True
def hex_to_modhex(hex_str):
"""
Convert a string of hex digits to a string of modhex digits.
>>> hex_to_modhex(b'69b6481c8baba2b60e8f22179b58cd56') == b'hknhfjbrjnlnldnhcujvddbikngjrtgh'
True
>>> hex_to_modhex(b'6j')
Traceback (most recent call last):
...
ValueError: Illegal hex character in input
"""
try:
return b''.join(int2byte(hex_to_modhex_char(b))
for b in iter(hex_str.lower()))
except ValueError:
raise ValueError('Illegal hex character in input')
def modhex_to_hex(modhex_str):
"""
Convert a string of modhex digits to a string of hex digits.
>>> modhex_to_hex(b'hknhfjbrjnlnldnhcujvddbikngjrtgh') == b'69b6481c8baba2b60e8f22179b58cd56'
True
>>> modhex_to_hex(b'hbhdxx')
Traceback (most recent call last):
...
ValueError: Illegal modhex character in input
"""
try:
return b''.join(int2byte(modhex_to_hex_char(b))
for b in iter(modhex_str.lower()))
except ValueError:
raise ValueError('Illegal modhex character in input')
#
# Internals
#
def int2byte(i):
return struct.Struct(">B").pack(i)
def lookup(alist, key):
try:
return next(v for (k, v) in alist if k == key)
except StopIteration:
raise ValueError()
hex_chars = b'0123456789abcdef'
modhex_chars = b'cbdefghijklnrtuv'
hex_to_modhex_map = list(zip(iter(hex_chars), iter(modhex_chars)))
modhex_to_hex_map = list(zip(iter(modhex_chars), iter(hex_chars)))
hex_to_modhex_char = partial(lookup, hex_to_modhex_map)
modhex_to_hex_char = partial(lookup, modhex_to_hex_map)
YubiOTP-1.0.0/yubiotp/otp.py 0000644 0000766 0000024 00000015356 13715343230 016455 0 ustar psagers staff 0000000 0000000 """
Implementation of the Yubico OTP algorithm. This can generate and parse OTP
structures.
>>> from binascii import unhexlify
>>> key = b'0123456789abcdef'
>>> otp = OTP(unhexlify(b'0123456789ab'), 5, 0x0153f8, 0, 0x1234)
>>> _ = repr(otp) # coverage
>>> _ = str(otp) # coverage
>>> token = encode_otp(otp, key, b'cclngiuv')
>>> token == b'cclngiuvttkhthcilurtkerbjnnkljfkjccklkhl'
True
>>> public_id, otp2 = decode_otp(token, key)
>>> public_id == b'cclngiuv'
True
>>> otp2 == otp
True
"""
from binascii import hexlify
from datetime import datetime
from random import randrange
from struct import pack, unpack
from Crypto.Cipher import AES
from .crc import crc16, verify_crc16
from .modhex import is_modhex, modhex, unmodhex
__all__ = ['decode_otp', 'encode_otp', 'OTP', 'YubiKey', 'CRCError']
class CRCError(ValueError):
"""
Raised when a decrypted token has an invalid checksum.
"""
pass
def decode_otp(token, key):
"""
Decodes a modhex-encoded Yubico OTP token and returns the public ID and the
unpacked :class:`OTP` object.
:param bytes token: A modhex-encoded buffer, as generated by a YubiKey
device. Decoded, this should consist of 0-16 bytes of public ID
followed by 16 bytes of encrypted OTP data.
:param bytes key: A 16-byte AES key as a binary string.
:returns: The public ID in its modhex-encoded form and the OTP structure.
:rtype: (bytes, :class:`OTP`)
:raises: ``ValueError`` if the string can not be decoded.
:raises: :exc:`CRCError` if the checksum on the decrypted data is
incorrect.
"""
if len(key) != 16:
raise ValueError('Key must be exactly 16 bytes')
public_id, token = token[:-32], token[-32:]
buf = unmodhex(token)
buf = AES.new(key, AES.MODE_ECB).decrypt(buf)
otp = OTP.unpack(buf)
return (public_id, otp)
def encode_otp(otp, key, public_id=b''):
"""
Encodes an :class:`OTP` structure, encrypts it with the given key and
returns the modhex-encoded token.
:param otp: The OTP structure.
:type otp: :class:`OTP`
:param bytes key: A 16-byte AES key as a binary string.
:param bytes public_id: An optional public id, modhex-encoded. This can be
at most 32 bytes.
:raises: ValueError if any parameters are out of range.
"""
if len(key) != 16:
raise ValueError('Key must be exactly 16 bytes')
if not is_modhex(public_id):
raise ValueError('public_id must be a valid modhex string')
if len(public_id) > 32:
raise ValueError('public_id may be no longer than 32 modhex characters')
buf = otp.pack()
buf = AES.new(key, AES.MODE_ECB).encrypt(buf)
token = modhex(buf)
return public_id + token
class OTP(object):
"""
A single YubiKey OTP. This is typically instantiated by parsing an encoded
OTP.
:param bytes uid: The private ID as a 6-byte binary string.
:param int session: The non-volatile usage counter.
:param int timestamp: An integer in [0..2^24].
:param int counter: The volatile usage counter.
:param int rand: An arbitrary number in [0..2^16].
"""
def __init__(self, uid, session, timestamp, counter, rand):
self.uid = uid
self.session = session
self.timestamp = timestamp
self.counter = counter
self.rand = rand
def __repr__(self):
return 'OTP({self.uid!r}, {self.session!r}, {self.timestamp!r}, {self.counter!r}, {self.rand!r})'.format(self=self)
def __str__(self):
return 'OTP: {0} {1}/{2} (0x{3:x}/0x{4:x})'.format(hexlify(self.uid), self.session, self.counter, self.timestamp, self.rand)
def __eq__(self, other):
if self.__class__ is not other.__class__:
return False
self_props = (self.uid, self.session, self.timestamp, self.counter, self.rand)
other_props = (other.uid, other.session, other.timestamp, other.counter, other.rand)
return (self_props == other_props)
def pack(self):
"""
Returns the OTP packed into a binary string, ready to be encrypted and
encoded.
"""
fields = (
self.uid,
self.session,
self.timestamp & 0xff, (self.timestamp >> 8) & 0xffff,
self.counter,
self.rand,
)
buf = pack('<6s H BH B H', *fields)
crc = ~crc16(buf) & 0xffff
buf += pack('= 0xff:
self._increment_session()
self.counter = 0
else:
self.counter += 1
def _increment_session(self):
self.session = min(self.session + 1, 0x7fff)
YubiOTP-1.0.0/yubiotp/test.py 0000644 0000766 0000024 00000000473 13531046553 016630 0 ustar psagers staff 0000000 0000000 from doctest import DocTestSuite
import unittest
from . import crc, modhex, otp
def load_tests(loader, tests, pattern):
suite = unittest.TestSuite()
suite.addTests(tests)
suite.addTest(DocTestSuite(crc))
suite.addTest(DocTestSuite(modhex))
suite.addTest(DocTestSuite(otp))
return suite