YubiOTP-1.0.0/0000755000076600000240000000000013715344434013603 5ustar psagersstaff00000000000000YubiOTP-1.0.0/CHANGES.rst0000644000076600000240000000140013715344246015401 0ustar psagersstaff00000000000000Change 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/LICENSE0000644000076600000240000000242113527557161014613 0ustar psagersstaff00000000000000Copyright (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.in0000644000076600000240000000014513530034557015336 0ustar psagersstaff00000000000000include CHANGES.rst LICENSE README.rst recursive-include docs *.rst *.py Makefile prune docs/build YubiOTP-1.0.0/PKG-INFO0000644000076600000240000000376213715344434014710 0ustar psagersstaff00000000000000Metadata-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.rst0000644000076600000240000000206613531027515015270 0ustar psagersstaff00000000000000.. 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/0000755000076600000240000000000013715344434016570 5ustar psagersstaff00000000000000YubiOTP-1.0.0/YubiOTP.egg-info/PKG-INFO0000644000076600000240000000376213715344434017675 0ustar psagersstaff00000000000000Metadata-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.txt0000644000076600000240000000075113715344434020457 0ustar psagersstaff00000000000000CHANGES.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.pyYubiOTP-1.0.0/YubiOTP.egg-info/dependency_links.txt0000644000076600000240000000000113715344434022636 0ustar psagersstaff00000000000000 YubiOTP-1.0.0/YubiOTP.egg-info/requires.txt0000644000076600000240000000001513715344434021164 0ustar psagersstaff00000000000000pycryptodome YubiOTP-1.0.0/YubiOTP.egg-info/top_level.txt0000644000076600000240000000001013715344434021311 0ustar psagersstaff00000000000000yubiotp YubiOTP-1.0.0/bin/0000755000076600000240000000000013715344434014353 5ustar psagersstaff00000000000000YubiOTP-1.0.0/bin/yubiclient0000755000076600000240000000637413527557161016466 0ustar psagersstaff00000000000000#!/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/yubikey0000755000076600000240000002505513527557161015775 0ustar psagersstaff00000000000000#!/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/0000755000076600000240000000000013715344434014533 5ustar psagersstaff00000000000000YubiOTP-1.0.0/docs/Makefile0000644000076600000240000001306413527557161016203 0ustar psagersstaff00000000000000# # 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/0000755000076600000240000000000013715344434016033 5ustar psagersstaff00000000000000YubiOTP-1.0.0/docs/source/changes.rst0000644000076600000240000000003713530033744020167 0ustar psagersstaff00000000000000.. include:: ../../CHANGES.rst YubiOTP-1.0.0/docs/source/client.rst0000644000076600000240000000101313527557161020042 0ustar psagersstaff00000000000000Validation 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.py0000644000076600000240000001727213715344427017345 0ustar psagersstaff00000000000000# -*- 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.rst0000644000076600000240000000022413530033744017664 0ustar psagersstaff00000000000000YubiOTP ======= .. include:: ../../README.rst .. toctree:: otp client utils changes License ======= .. include:: ../../LICENSE YubiOTP-1.0.0/docs/source/otp.rst0000644000076600000240000000012513527557161017371 0ustar psagersstaff00000000000000OTP API ======= yubiotp.otp ----------- .. automodule:: yubiotp.otp :members: YubiOTP-1.0.0/docs/source/utils.rst0000644000076600000240000000026713527557161017736 0ustar psagersstaff00000000000000YubiOTP Utilities ================= yubiotp.modhex -------------- .. automodule:: yubiotp.modhex :members: yubiotp.crc ----------- .. automodule:: yubiotp.crc :members: YubiOTP-1.0.0/setup.cfg0000644000076600000240000000021313715344434015420 0ustar psagersstaff00000000000000[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.py0000755000076600000240000000201713715344427015322 0ustar psagersstaff00000000000000#!/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/0000755000076600000240000000000013715344434015276 5ustar psagersstaff00000000000000YubiOTP-1.0.0/yubiotp/__init__.py0000644000076600000240000000000013527557161017401 0ustar psagersstaff00000000000000YubiOTP-1.0.0/yubiotp/client.py0000644000076600000240000002415013715343230017121 0ustar psagersstaff00000000000000from 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.py0000644000076600000240000000162713715343230016416 0ustar psagersstaff00000000000000""" 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.py0000644000076600000240000000544313715343230017133 0ustar psagersstaff00000000000000""" 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.py0000644000076600000240000001535613715343230016455 0ustar psagersstaff00000000000000""" 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.py0000644000076600000240000000047313531046553016630 0ustar psagersstaff00000000000000from 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