pyotp-2.2.6/0000755000175000017500000000000013117015311010767 5ustar hlehlepyotp-2.2.6/README.rst0000644000175000017500000001253513117015052012466 0ustar hlehlePyOTP - The Python One-Time Password Library ============================================ PyOTP is a Python library for generating and verifying one-time passwords. It can be used to implement two-factor (2FA) or multi-factor (MFA) authentication methods in web applications and in other systems that require users to log in. Open MFA standards are defined in `RFC 4226 `_ (HOTP: An HMAC-Based One-Time Password Algorithm) and in `RFC 6238 `_ (TOTP: Time-Based One-Time Password Algorithm). PyOTP implements server-side support for both of these standards. Client-side support can be enabled by sending authentication codes to users over SMS or email (HOTP) or, for TOTP, by instructing users to use `Google Authenticator `_, `Authy `_, or another compatible app. Users can set up auth tokens in their apps easily by using their phone camera to scan `otpauth:// `_ QR codes provided by PyOTP. We recommend that implementers read the `OWASP Authentication Cheat Sheet `_ and `NIST SP 800-63-3: Digital Authentication Guideline `_ for a high level overview of authentication best practices. Quick overview of using One Time Passwords on your phone -------------------------------------------------------- * OTPs involve a shared secret, stored both on the phone and the server * OTPs can be generated on a phone without internet connectivity * OTPs should always be used as a second factor of authentication (if your phone is lost, you account is still secured with a password) * Google Authenticator and other OTP client apps allow you to store multiple OTP secrets and provision those using a QR Code Installation ------------ :: pip install pyotp Usage ----- Time-based OTPs ~~~~~~~~~~~~~~~ :: totp = pyotp.TOTP('base32secret3232') totp.now() # => 492039 # OTP verified for current time totp.verify(492039) # => True time.sleep(30) totp.verify(492039) # => False Counter-based OTPs ~~~~~~~~~~~~~~~~~~ :: hotp = pyotp.HOTP('base32secret3232') hotp.at(0) # => 260182 hotp.at(1) # => 55283 hotp.at(1401) # => 316439 # OTP verified with a counter hotp.verify(316439, 1401) # => True hotp.verify(316439, 1402) # => False Generating a base32 Secret Key ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: pyotp.random_base32() # returns a 16 character base32 secret. Compatible with Google Authenticator and other OTP apps Google Authenticator Compatible ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyOTP works with the Google Authenticator iPhone and Android app, as well as other OTP apps like Authy. PyOTP includes the ability to generate provisioning URIs for use with the QR Code scanner built into these MFA client apps:: pyotp.totp.TOTP('JBSWY3DPEHPK3PXP').provisioning_uri("alice@google.com", issuer_name="Secure App") >>> 'otpauth://totp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App' pyotp.hotp.HOTP('JBSWY3DPEHPK3PXP').provisioning_uri("alice@google.com", initial_count=0, issuer_name="Secure App") >>> 'otpauth://hotp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App&counter=0' This URL can then be rendered as a QR Code (for example, using https://github.com/neocotic/qrious) which can then be scanned and added to the users list of OTP credentials. Working example ~~~~~~~~~~~~~~~ Scan the following barcode with your phone's OTP app (e.g. Google Authenticator): .. image:: http://chart.apis.google.com/chart?cht=qr&chs=250x250&chl=otpauth%3A%2F%2Ftotp%2Falice%40google.com%3Fsecret%3DJBSWY3DPEHPK3PXP Now run the following and compare the output:: import pyotp totp = pyotp.TOTP("JBSWY3DPEHPK3PXP") print("Current OTP:", totp.now()) Links ~~~~~ * `Project home page (GitHub) `_ * `Documentation (Read the Docs) `_ * `Package distribution (PyPI) `_ * `Change log `_ * `RFC 4226: HOTP: An HMAC-Based One-Time Password `_ * `RFC 6238: TOTP: Time-Based One-Time Password Algorithm `_ * `ROTP `_ - Original Ruby OTP library by `Mark Percival `_ * `OTPHP `_ - PHP port of ROTP by `Le Lag `_ * `OWASP Authentication Cheat Sheet `_ * `NIST SP 800-63-3: Digital Authentication Guideline `_ .. image:: https://img.shields.io/travis/pyotp/pyotp.svg :target: https://travis-ci.org/pyotp/pyotp .. image:: https://img.shields.io/codecov/c/github/pyotp/pyotp/master.svg :target: https://codecov.io/github/pyotp/pyotp?branch=master .. image:: https://img.shields.io/pypi/v/pyotp.svg :target: https://pypi.python.org/pypi/pyotp .. image:: https://img.shields.io/pypi/l/pyotp.svg :target: https://pypi.python.org/pypi/pyotp .. image:: https://readthedocs.org/projects/pyotp/badge/?version=latest :target: https://pyotp.readthedocs.io/ pyotp-2.2.6/setup.cfg0000644000175000017500000000021713117015311012610 0ustar hlehle[bdist_wheel] universal = 1 [flake8] max-line-length = 120 ignore = E301, E302, E305, E401, E226, F841 [egg_info] tag_build = tag_date = 0 pyotp-2.2.6/test.py0000755000175000017500000002751013117015172012335 0ustar hlehle#!/usr/bin/env python # coding: utf-8 from __future__ import absolute_import, division, print_function, unicode_literals import base64, datetime, hashlib, os, sys, unittest from warnings import warn try: from urllib.parse import urlparse, parse_qsl except ImportError: from urlparse import urlparse, parse_qsl sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) import pyotp # noqa class HOTPExampleValuesFromTheRFC(unittest.TestCase): def test_match_rfc(self): # 12345678901234567890 in Bas32 # GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ hotp = pyotp.HOTP('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ') self.assertEqual(hotp.at(0), '755224') self.assertEqual(hotp.at(1), '287082') self.assertEqual(hotp.at(2), '359152') self.assertEqual(hotp.at(3), '969429') self.assertEqual(hotp.at(4), '338314') self.assertEqual(hotp.at(5), '254676') self.assertEqual(hotp.at(6), '287922') self.assertEqual(hotp.at(7), '162583') self.assertEqual(hotp.at(8), '399871') self.assertEqual(hotp.at(9), '520489') def test_invalid_input(self): hotp = pyotp.HOTP('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ') with self.assertRaises(ValueError): hotp.at(-1) def test_verify_otp_reuse(self): hotp = pyotp.HOTP('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ') self.assertTrue(hotp.verify('520489', 9)) self.assertFalse(hotp.verify('520489', 10)) self.assertFalse(hotp.verify('520489', 10)) def test_provisioning_uri(self): hotp = pyotp.HOTP('wrn3pqx5uqxqvnqr') url = urlparse( hotp.provisioning_uri('mark@percival')) self.assertEqual(url.scheme, 'otpauth') self.assertEqual(url.netloc, 'hotp') self.assertEqual(url.path, '/mark%40percival') self.assertEqual(dict(parse_qsl(url.query)), {'secret': 'wrn3pqx5uqxqvnqr', 'counter': '0'}) url = urlparse( hotp.provisioning_uri('mark@percival', initial_count=12)) self.assertEqual(url.scheme, 'otpauth') self.assertEqual(url.netloc, 'hotp') self.assertEqual(url.path, '/mark%40percival') self.assertEqual(dict(parse_qsl(url.query)), {'secret': 'wrn3pqx5uqxqvnqr', 'counter': '12'}) url = urlparse( hotp.provisioning_uri('mark@percival', issuer_name='FooCorp!')) self.assertEqual(url.scheme, 'otpauth') self.assertEqual(url.netloc, 'hotp') self.assertEqual(url.path, '/FooCorp%21:mark%40percival') self.assertEqual(dict(parse_qsl(url.query)), {'secret': 'wrn3pqx5uqxqvnqr', 'counter': '0', 'issuer': 'FooCorp!'}) key = 'c7uxuqhgflpw7oruedmglbrk7u6242vb' hotp = pyotp.HOTP(key, digits=8, digest=hashlib.sha256) url = urlparse( hotp.provisioning_uri('baco@peperina', issuer_name='FooCorp')) self.assertEqual(url.scheme, 'otpauth') self.assertEqual(url.netloc, 'hotp') self.assertEqual(url.path, '/FooCorp:baco%40peperina') self.assertEqual(dict(parse_qsl(url.query)), {'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb', 'counter': '0', 'issuer': 'FooCorp', 'digits': '8', 'algorithm': 'SHA256'}) hotp = pyotp.HOTP(key, digits=8) url = urlparse( hotp.provisioning_uri('baco@peperina', issuer_name='Foo Corp', initial_count=10)) self.assertEqual(url.scheme, 'otpauth') self.assertEqual(url.netloc, 'hotp') self.assertEqual(url.path, '/Foo%20Corp:baco%40peperina') self.assertEqual(dict(parse_qsl(url.query)), {'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb', 'counter': '10', 'issuer': 'Foo Corp', 'digits': '8'}) def test_other_secret(self): hotp = pyotp.HOTP( 'N3OVNIBRERIO5OHGVCMDGS4V4RJ3AUZOUN34J6FRM4P6JIFCG3ZA') self.assertEqual(hotp.at(0), '737863') self.assertEqual(hotp.at(1), '390601') self.assertEqual(hotp.at(2), '363354') self.assertEqual(hotp.at(3), '936780') self.assertEqual(hotp.at(4), '654019') class TOTPExampleValuesFromTheRFC(unittest.TestCase): RFC_VALUES = { (hashlib.sha1, b'12345678901234567890'): ( (59, '94287082'), (1111111109, '07081804'), (1111111111, '14050471'), (1234567890, '89005924'), (2000000000, '69279037'), (20000000000, '65353130'), ), (hashlib.sha256, b'12345678901234567890123456789012'): ( (59, 46119246), (1111111109, '68084774'), (1111111111, '67062674'), (1234567890, '91819424'), (2000000000, '90698825'), (20000000000, '77737706'), ), (hashlib.sha512, b'1234567890123456789012345678901234567890123456789012345678901234'): ( (59, 90693936), (1111111109, '25091201'), (1111111111, '99943326'), (1234567890, '93441116'), (2000000000, '38618901'), (20000000000, '47863826'), ), } def test_match_rfc(self): for digest, secret in self.RFC_VALUES: totp = pyotp.TOTP(base64.b32encode(secret), 8, digest) for utime, code in self.RFC_VALUES[(digest, secret)]: if utime > sys.maxsize: warn("32-bit platforms use native functions to handle timestamps, so they fail this test" + " (and will fail after 19 January 2038)") continue value = totp.at(utime) msg = "%s != %s (%s, time=%d)" msg %= (value, code, digest().name, utime) self.assertEqual(value, str(code), msg) def test_match_rfc_digit_length(self): totp = pyotp.TOTP('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ') self.assertEqual(totp.at(1111111111), '050471') self.assertEqual(totp.at(1234567890), '005924') self.assertEqual(totp.at(2000000000), '279037') def test_match_google_authenticator_output(self): totp = pyotp.TOTP('wrn3pqx5uqxqvnqr') with Timecop(1297553958): self.assertEqual(totp.now(), '102705') def test_validate_totp(self): totp = pyotp.TOTP('wrn3pqx5uqxqvnqr') with Timecop(1297553958): self.assertTrue(totp.verify('102705')) self.assertTrue(totp.verify('102705')) with Timecop(1297553958 + 30): self.assertFalse(totp.verify('102705')) def test_input_before_epoch(self): totp = pyotp.TOTP('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ') # -1 and -29.5 round down to 0 (epoch) self.assertEqual(totp.at(-1), '755224') self.assertEqual(totp.at(-29.5), '755224') with self.assertRaises(ValueError): totp.at(-30) def test_validate_totp_with_digit_length(self): totp = pyotp.TOTP('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ') with Timecop(1111111111): self.assertTrue(totp.verify('050471')) with Timecop(1297553958 + 30): self.assertFalse(totp.verify('050471')) def test_provisioning_uri(self): totp = pyotp.TOTP('wrn3pqx5uqxqvnqr') url = urlparse( totp.provisioning_uri('mark@percival')) self.assertEqual(url.scheme, 'otpauth') self.assertEqual(url.netloc, 'totp') self.assertEqual(url.path, '/mark%40percival') self.assertEqual(dict(parse_qsl(url.query)), {'secret': 'wrn3pqx5uqxqvnqr'}) url = urlparse( totp.provisioning_uri('mark@percival', issuer_name='FooCorp!')) self.assertEqual(url.scheme, 'otpauth') self.assertEqual(url.netloc, 'totp') self.assertEqual(url.path, '/FooCorp%21:mark%40percival') self.assertEqual(dict(parse_qsl(url.query)), {'secret': 'wrn3pqx5uqxqvnqr', 'issuer': 'FooCorp!'}) key = 'c7uxuqhgflpw7oruedmglbrk7u6242vb' totp = pyotp.TOTP(key, digits=8, interval=60, digest=hashlib.sha256) url = urlparse(totp.provisioning_uri('baco@peperina', issuer_name='FooCorp')) self.assertEqual(url.scheme, 'otpauth') self.assertEqual(url.netloc, 'totp') self.assertEqual(url.path, '/FooCorp:baco%40peperina') self.assertEqual(dict(parse_qsl(url.query)), {'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb', 'issuer': 'FooCorp', 'digits': '8', 'period': '60', 'algorithm': 'SHA256'}) totp = pyotp.TOTP(key, digits=8, interval=60) url = urlparse(totp.provisioning_uri('baco@peperina', issuer_name='FooCorp')) self.assertEqual(url.scheme, 'otpauth') self.assertEqual(url.netloc, 'totp') self.assertEqual(url.path, '/FooCorp:baco%40peperina') self.assertEqual(dict(parse_qsl(url.query)), {'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb', 'issuer': 'FooCorp', 'digits': '8', 'period': '60'}) totp = pyotp.TOTP(key, digits=8) url = urlparse(totp.provisioning_uri('baco@peperina', issuer_name='FooCorp')) self.assertEqual(url.scheme, 'otpauth') self.assertEqual(url.netloc, 'totp') self.assertEqual(url.path, '/FooCorp:baco%40peperina') self.assertEqual(dict(parse_qsl(url.query)), {'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb', 'issuer': 'FooCorp', 'digits': '8'}) def test_random_key_generation(self): self.assertEqual(len(pyotp.random_base32()), 16) self.assertEqual(len(pyotp.random_base32(length=20)), 20) class CompareDigestTest(unittest.TestCase): method = staticmethod(pyotp.utils.compare_digest) def test_comparisons(self): self.assertTrue(self.method("", "")) self.assertTrue(self.method("a", "a")) self.assertTrue(self.method("a" * 1000, "a" * 1000)) self.assertFalse(self.method("", "a")) self.assertFalse(self.method("a", "")) self.assertFalse(self.method("a" * 999 + "b", "a" * 1000)) class FallBackCompareDigestTest(CompareDigestTest): method = staticmethod(pyotp.utils._compare_digest) class StringComparisonTest(CompareDigestTest): method = staticmethod(pyotp.utils.strings_equal) def test_fullwidth_input(self): self.assertTrue(self.method("xs12345", "xs12345")) class CounterOffsetTest(unittest.TestCase): def test_counter_offset(self): totp = pyotp.TOTP("ABCDEFGH") self.assertEqual(totp.at(200), "028307") self.assertTrue(totp.at(200, 1), "681610") class ValidWindowTest(unittest.TestCase): def test_valid_window(self): totp = pyotp.TOTP("ABCDEFGH") self.assertTrue(totp.verify("451564", 200, 1)) self.assertTrue(totp.verify("028307", 200, 1)) self.assertTrue(totp.verify("681610", 200, 1)) self.assertFalse(totp.verify("195979", 200, 1)) class Timecop(object): """ Half-assed clone of timecop.rb, just enough to pass our tests. """ def __init__(self, freeze_timestamp): self.freeze_timestamp = freeze_timestamp def __enter__(self): self.real_datetime = datetime.datetime datetime.datetime = self.frozen_datetime() def __exit__(self, type, value, traceback): datetime.datetime = self.real_datetime def frozen_datetime(self): class FrozenDateTime(datetime.datetime): @classmethod def now(cls, **kwargs): return cls.fromtimestamp(timecop.freeze_timestamp) timecop = self return FrozenDateTime if __name__ == '__main__': unittest.main() pyotp-2.2.6/requirements.txt0000644000175000017500000000000013062566407014261 0ustar hlehlepyotp-2.2.6/setup.py0000755000175000017500000000211213117015264012507 0ustar hlehle#!/usr/bin/env python import os from setuptools import setup install_requires = [line.rstrip() for line in open(os.path.join(os.path.dirname(__file__), "requirements.txt"))] setup( name="pyotp", version="2.2.6", url="https://github.com/pyotp/pyotp", license="BSD License", author="PyOTP contributors", author_email="kislyuk@gmail.com", description="Python One Time Password Library", long_description=open("README.rst").read(), install_requires=install_requires, packages=["pyotp"], package_dir={"": "src"}, platforms=["MacOS X", "Posix"], zip_safe=False, test_suite="test", classifiers=[ "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: MacOS :: MacOS X", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Topic :: Software Development :: Libraries :: Python Modules" ] ) pyotp-2.2.6/LICENSE0000644000175000017500000000216213062566407012015 0ustar hlehleCopyright (C) 2011-2016 Mark Percival , Nathan Reynolds , and PyOTP contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pyotp-2.2.6/MANIFEST.in0000644000175000017500000000012113062566407012537 0ustar hlehleinclude LICENSE include README.markdown include requirements.txt include test.py pyotp-2.2.6/PKG-INFO0000644000175000017500000001572213117015311012073 0ustar hlehleMetadata-Version: 1.1 Name: pyotp Version: 2.2.6 Summary: Python One Time Password Library Home-page: https://github.com/pyotp/pyotp Author: PyOTP contributors Author-email: kislyuk@gmail.com License: BSD License Description: PyOTP - The Python One-Time Password Library ============================================ PyOTP is a Python library for generating and verifying one-time passwords. It can be used to implement two-factor (2FA) or multi-factor (MFA) authentication methods in web applications and in other systems that require users to log in. Open MFA standards are defined in `RFC 4226 `_ (HOTP: An HMAC-Based One-Time Password Algorithm) and in `RFC 6238 `_ (TOTP: Time-Based One-Time Password Algorithm). PyOTP implements server-side support for both of these standards. Client-side support can be enabled by sending authentication codes to users over SMS or email (HOTP) or, for TOTP, by instructing users to use `Google Authenticator `_, `Authy `_, or another compatible app. Users can set up auth tokens in their apps easily by using their phone camera to scan `otpauth:// `_ QR codes provided by PyOTP. We recommend that implementers read the `OWASP Authentication Cheat Sheet `_ and `NIST SP 800-63-3: Digital Authentication Guideline `_ for a high level overview of authentication best practices. Quick overview of using One Time Passwords on your phone -------------------------------------------------------- * OTPs involve a shared secret, stored both on the phone and the server * OTPs can be generated on a phone without internet connectivity * OTPs should always be used as a second factor of authentication (if your phone is lost, you account is still secured with a password) * Google Authenticator and other OTP client apps allow you to store multiple OTP secrets and provision those using a QR Code Installation ------------ :: pip install pyotp Usage ----- Time-based OTPs ~~~~~~~~~~~~~~~ :: totp = pyotp.TOTP('base32secret3232') totp.now() # => 492039 # OTP verified for current time totp.verify(492039) # => True time.sleep(30) totp.verify(492039) # => False Counter-based OTPs ~~~~~~~~~~~~~~~~~~ :: hotp = pyotp.HOTP('base32secret3232') hotp.at(0) # => 260182 hotp.at(1) # => 55283 hotp.at(1401) # => 316439 # OTP verified with a counter hotp.verify(316439, 1401) # => True hotp.verify(316439, 1402) # => False Generating a base32 Secret Key ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: pyotp.random_base32() # returns a 16 character base32 secret. Compatible with Google Authenticator and other OTP apps Google Authenticator Compatible ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyOTP works with the Google Authenticator iPhone and Android app, as well as other OTP apps like Authy. PyOTP includes the ability to generate provisioning URIs for use with the QR Code scanner built into these MFA client apps:: pyotp.totp.TOTP('JBSWY3DPEHPK3PXP').provisioning_uri("alice@google.com", issuer_name="Secure App") >>> 'otpauth://totp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App' pyotp.hotp.HOTP('JBSWY3DPEHPK3PXP').provisioning_uri("alice@google.com", initial_count=0, issuer_name="Secure App") >>> 'otpauth://hotp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App&counter=0' This URL can then be rendered as a QR Code (for example, using https://github.com/neocotic/qrious) which can then be scanned and added to the users list of OTP credentials. Working example ~~~~~~~~~~~~~~~ Scan the following barcode with your phone's OTP app (e.g. Google Authenticator): .. image:: http://chart.apis.google.com/chart?cht=qr&chs=250x250&chl=otpauth%3A%2F%2Ftotp%2Falice%40google.com%3Fsecret%3DJBSWY3DPEHPK3PXP Now run the following and compare the output:: import pyotp totp = pyotp.TOTP("JBSWY3DPEHPK3PXP") print("Current OTP:", totp.now()) Links ~~~~~ * `Project home page (GitHub) `_ * `Documentation (Read the Docs) `_ * `Package distribution (PyPI) `_ * `Change log `_ * `RFC 4226: HOTP: An HMAC-Based One-Time Password `_ * `RFC 6238: TOTP: Time-Based One-Time Password Algorithm `_ * `ROTP `_ - Original Ruby OTP library by `Mark Percival `_ * `OTPHP `_ - PHP port of ROTP by `Le Lag `_ * `OWASP Authentication Cheat Sheet `_ * `NIST SP 800-63-3: Digital Authentication Guideline `_ .. image:: https://img.shields.io/travis/pyotp/pyotp.svg :target: https://travis-ci.org/pyotp/pyotp .. image:: https://img.shields.io/codecov/c/github/pyotp/pyotp/master.svg :target: https://codecov.io/github/pyotp/pyotp?branch=master .. image:: https://img.shields.io/pypi/v/pyotp.svg :target: https://pypi.python.org/pypi/pyotp .. image:: https://img.shields.io/pypi/l/pyotp.svg :target: https://pypi.python.org/pypi/pyotp .. image:: https://readthedocs.org/projects/pyotp/badge/?version=latest :target: https://pyotp.readthedocs.io/ Platform: MacOS X Platform: Posix Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: MacOS :: MacOS X Classifier: Operating System :: POSIX Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Topic :: Software Development :: Libraries :: Python Modules pyotp-2.2.6/src/0000755000175000017500000000000013117015311011556 5ustar hlehlepyotp-2.2.6/src/pyotp/0000755000175000017500000000000013117015311012731 5ustar hlehlepyotp-2.2.6/src/pyotp/__init__.py0000644000175000017500000000074513114572535015065 0ustar hlehlefrom __future__ import (absolute_import, division, print_function, unicode_literals) import random as _random from pyotp.hotp import HOTP # noqa from pyotp.otp import OTP # noqa from pyotp.totp import TOTP # noqa from . import utils # noqa def random_base32(length=16, random=_random.SystemRandom(), chars=list('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')): return ''.join( random.choice(chars) for _ in range(length) ) pyotp-2.2.6/src/pyotp/utils.py0000644000175000017500000000664713117015052014462 0ustar hlehlefrom __future__ import absolute_import, division, print_function, unicode_literals import unicodedata try: from itertools import izip_longest except ImportError: from itertools import zip_longest as izip_longest try: from urllib.parse import quote, urlencode except ImportError: from urllib import quote, urlencode def build_uri(secret, name, initial_count=None, issuer_name=None, algorithm=None, digits=None, period=None): """ Returns the provisioning URI for the OTP; works for either TOTP or HOTP. This can then be encoded in a QR Code and used to provision the Google Authenticator app. For module-internal use. See also: https://github.com/google/google-authenticator/wiki/Key-Uri-Format :param secret: the hotp/totp secret used to generate the URI :type secret: str :param name: name of the account :type name: str :param initial_count: starting counter value, defaults to None. If none, the OTP type will be assumed as TOTP. :type initial_count: int :param issuer_name: the name of the OTP issuer; this will be the organization title of the OTP entry in Authenticator :type issuer_name: str :param algorithm: the algorithm used in the OTP generation. :type algorithm: str :param digits: the length of the OTP generated code. :type digits: int :param period: the number of seconds the OTP generator is set to expire every code. :type period: int :returns: provisioning uri :rtype: str """ # initial_count may be 0 as a valid param is_initial_count_present = (initial_count is not None) # Handling values different from defaults is_algorithm_set = (algorithm is not None and algorithm != 'sha1') is_digits_set = (digits is not None and digits != 6) is_period_set = (period is not None and period != 30) otp_type = 'hotp' if is_initial_count_present else 'totp' base_uri = 'otpauth://{0}/{1}?{2}' url_args = {'secret': secret} label = quote(name) if issuer_name is not None: label = quote(issuer_name) + ':' + label url_args['issuer'] = issuer_name if is_initial_count_present: url_args['counter'] = initial_count if is_algorithm_set: url_args['algorithm'] = algorithm.upper() if is_digits_set: url_args['digits'] = digits if is_period_set: url_args['period'] = period uri = base_uri.format(otp_type, label, urlencode(url_args).replace("+", "%20")) return uri def _compare_digest(s1, s2): differences = 0 for c1, c2 in izip_longest(s1, s2): if c1 is None or c2 is None: differences = 1 continue differences |= ord(c1) ^ ord(c2) return differences == 0 try: # Python 3.3+ and 2.7.7+ include a timing-attack-resistant # comparison function, which is probably more reliable than ours. # Use it if available. from hmac import compare_digest except ImportError: compare_digest = _compare_digest def strings_equal(s1, s2): """ Timing-attack resistant string comparison. Normal comparison using == will short-circuit on the first mismatching character. This avoids that by scanning the whole string, though we still reveal to a timing attack whether the strings are the same length. """ s1 = unicodedata.normalize('NFKC', s1) s2 = unicodedata.normalize('NFKC', s2) return compare_digest(s1, s2) pyotp-2.2.6/src/pyotp/totp.py0000644000175000017500000000614013114604016014275 0ustar hlehlefrom __future__ import absolute_import, division, print_function, unicode_literals import datetime import time from . import utils from .otp import OTP from .compat import str class TOTP(OTP): """ Handler for time-based OTP counters. """ def __init__(self, *args, **kwargs): """ :param interval: the time interval in seconds for OTP. This defaults to 30. :type interval: int """ self.interval = kwargs.pop('interval', 30) super(TOTP, self).__init__(*args, **kwargs) def at(self, for_time, counter_offset=0): """ Accepts either a Unix timestamp integer or a datetime object. :param for_time: the time to generate an OTP for :type for_time: int or datetime :param counter_offset: the amount of ticks to add to the time counter :returns: OTP value :rtype: str """ if not isinstance(for_time, datetime.datetime): for_time = datetime.datetime.fromtimestamp(int(for_time)) return self.generate_otp(self.timecode(for_time) + counter_offset) def now(self): """ Generate the current time OTP :returns: OTP value :rtype: str """ return self.generate_otp(self.timecode(datetime.datetime.now())) def verify(self, otp, for_time=None, valid_window=0): """ Verifies the OTP passed in against the current time OTP. :param otp: the OTP to check against :type otp: str :param for_time: Time to check OTP at (defaults to now) :type for_time: int or datetime :param valid_window: extends the validity to this many counter ticks before and after the current one :type valid_window: int :returns: True if verification succeeded, False otherwise :rtype: bool """ if for_time is None: for_time = datetime.datetime.now() if valid_window: for i in range(-valid_window, valid_window + 1): if utils.strings_equal(str(otp), str(self.at(for_time, i))): return True return False return utils.strings_equal(str(otp), str(self.at(for_time))) def provisioning_uri(self, name, issuer_name=None): """ Returns the provisioning URI for the OTP. This can then be encoded in a QR Code and used to provision an OTP app like Google Authenticator. See also: https://github.com/google/google-authenticator/wiki/Key-Uri-Format :param name: name of the user account :type name: str :param issuer_name: the name of the OTP issuer; this will be the organization title of the OTP entry in Authenticator :returns: provisioning URI :rtype: str """ return utils.build_uri(self.secret, name, issuer_name=issuer_name, algorithm=self.digest().name, digits=self.digits, period=self.interval) def timecode(self, for_time): i = time.mktime(for_time.timetuple()) return int(i / self.interval) pyotp-2.2.6/src/pyotp/hotp.py0000644000175000017500000000335113114604032014260 0ustar hlehlefrom __future__ import absolute_import, division, print_function, unicode_literals from . import utils from .otp import OTP from .compat import str class HOTP(OTP): """ Handler for HMAC-based OTP counters. """ def at(self, count): """ Generates the OTP for the given count. :param count: the OTP HMAC counter :type count: int :returns: OTP :rtype: str """ return self.generate_otp(count) def verify(self, otp, counter): """ Verifies the OTP passed in against the current time OTP. :param otp: the OTP to check against :type otp: str :param count: the OTP HMAC counter :type count: int """ return utils.strings_equal(str(otp), str(self.at(counter))) def provisioning_uri(self, name, initial_count=0, issuer_name=None): """ Returns the provisioning URI for the OTP. This can then be encoded in a QR Code and used to provision an OTP app like Google Authenticator. See also: https://github.com/google/google-authenticator/wiki/Key-Uri-Format :param name: name of the user account :type name: str :param initial_count: starting HMAC counter value, defaults to 0 :type initial_count: int :param issuer_name: the name of the OTP issuer; this will be the organization title of the OTP entry in Authenticator :returns: provisioning URI :rtype: str """ return utils.build_uri( self.secret, name, initial_count=initial_count, issuer_name=issuer_name, algorithm=self.digest().name, digits=self.digits ) pyotp-2.2.6/src/pyotp/compat.py0000644000175000017500000000033613062566407014610 0ustar hlehlefrom __future__ import absolute_import, division, print_function, unicode_literals import sys USING_PYTHON2 = True if sys.version_info < (3, 0) else False if USING_PYTHON2: str = unicode # noqa else: str = str pyotp-2.2.6/src/pyotp/otp.py0000644000175000017500000000445013114577545014133 0ustar hlehlefrom __future__ import absolute_import, division, print_function, unicode_literals import base64 import hashlib import hmac from .compat import str class OTP(object): """ Base class for OTP handlers. """ def __init__(self, s, digits=6, digest=hashlib.sha1): """ :param s: secret in base32 format :type s: str :param digits: number of integers in the OTP. Some apps expect this to be 6 digits, others support more. :type digits: int :param digest: digest function to use in the HMAC (expected to be sha1) :type digest: callable """ self.digits = digits self.digest = digest self.secret = s def generate_otp(self, input): """ :param input: the HMAC counter value to use as the OTP input. Usually either the counter, or the computed integer based on the Unix timestamp :type input: int """ if input < 0: raise ValueError('input must be positive integer') hasher = hmac.new(self.byte_secret(), self.int_to_bytestring(input), self.digest) hmac_hash = bytearray(hasher.digest()) offset = hmac_hash[-1] & 0xf code = ((hmac_hash[offset] & 0x7f) << 24 | (hmac_hash[offset + 1] & 0xff) << 16 | (hmac_hash[offset + 2] & 0xff) << 8 | (hmac_hash[offset + 3] & 0xff)) str_code = str(code % 10 ** self.digits) while len(str_code) < self.digits: str_code = '0' + str_code return str_code def byte_secret(self): missing_padding = len(self.secret) % 8 if missing_padding != 0: self.secret += '=' * (8 - missing_padding) return base64.b32decode(self.secret, casefold=True) @staticmethod def int_to_bytestring(i, padding=8): """ Turns an integer to the OATH specified bytestring, which is fed to the HMAC along with the secret """ result = bytearray() while i != 0: result.append(i & 0xFF) i >>= 8 # It's necessary to convert the final result from bytearray to bytes # because the hmac functions in python 2.6 and 3.3 don't work with # bytearray return bytes(bytearray(reversed(result)).rjust(padding, b'\0')) pyotp-2.2.6/src/pyotp.egg-info/0000755000175000017500000000000013117015311014423 5ustar hlehlepyotp-2.2.6/src/pyotp.egg-info/top_level.txt0000644000175000017500000000000613117015311017151 0ustar hlehlepyotp pyotp-2.2.6/src/pyotp.egg-info/dependency_links.txt0000644000175000017500000000000113117015311020471 0ustar hlehle pyotp-2.2.6/src/pyotp.egg-info/not-zip-safe0000644000175000017500000000000113062566446016674 0ustar hlehle pyotp-2.2.6/src/pyotp.egg-info/SOURCES.txt0000644000175000017500000000054013117015311016306 0ustar hlehleLICENSE MANIFEST.in README.rst requirements.txt setup.cfg setup.py test.py src/pyotp/__init__.py src/pyotp/compat.py src/pyotp/hotp.py src/pyotp/otp.py src/pyotp/totp.py src/pyotp/utils.py src/pyotp.egg-info/PKG-INFO src/pyotp.egg-info/SOURCES.txt src/pyotp.egg-info/dependency_links.txt src/pyotp.egg-info/not-zip-safe src/pyotp.egg-info/top_level.txtpyotp-2.2.6/src/pyotp.egg-info/PKG-INFO0000644000175000017500000001572213117015311015527 0ustar hlehleMetadata-Version: 1.1 Name: pyotp Version: 2.2.6 Summary: Python One Time Password Library Home-page: https://github.com/pyotp/pyotp Author: PyOTP contributors Author-email: kislyuk@gmail.com License: BSD License Description: PyOTP - The Python One-Time Password Library ============================================ PyOTP is a Python library for generating and verifying one-time passwords. It can be used to implement two-factor (2FA) or multi-factor (MFA) authentication methods in web applications and in other systems that require users to log in. Open MFA standards are defined in `RFC 4226 `_ (HOTP: An HMAC-Based One-Time Password Algorithm) and in `RFC 6238 `_ (TOTP: Time-Based One-Time Password Algorithm). PyOTP implements server-side support for both of these standards. Client-side support can be enabled by sending authentication codes to users over SMS or email (HOTP) or, for TOTP, by instructing users to use `Google Authenticator `_, `Authy `_, or another compatible app. Users can set up auth tokens in their apps easily by using their phone camera to scan `otpauth:// `_ QR codes provided by PyOTP. We recommend that implementers read the `OWASP Authentication Cheat Sheet `_ and `NIST SP 800-63-3: Digital Authentication Guideline `_ for a high level overview of authentication best practices. Quick overview of using One Time Passwords on your phone -------------------------------------------------------- * OTPs involve a shared secret, stored both on the phone and the server * OTPs can be generated on a phone without internet connectivity * OTPs should always be used as a second factor of authentication (if your phone is lost, you account is still secured with a password) * Google Authenticator and other OTP client apps allow you to store multiple OTP secrets and provision those using a QR Code Installation ------------ :: pip install pyotp Usage ----- Time-based OTPs ~~~~~~~~~~~~~~~ :: totp = pyotp.TOTP('base32secret3232') totp.now() # => 492039 # OTP verified for current time totp.verify(492039) # => True time.sleep(30) totp.verify(492039) # => False Counter-based OTPs ~~~~~~~~~~~~~~~~~~ :: hotp = pyotp.HOTP('base32secret3232') hotp.at(0) # => 260182 hotp.at(1) # => 55283 hotp.at(1401) # => 316439 # OTP verified with a counter hotp.verify(316439, 1401) # => True hotp.verify(316439, 1402) # => False Generating a base32 Secret Key ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: pyotp.random_base32() # returns a 16 character base32 secret. Compatible with Google Authenticator and other OTP apps Google Authenticator Compatible ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyOTP works with the Google Authenticator iPhone and Android app, as well as other OTP apps like Authy. PyOTP includes the ability to generate provisioning URIs for use with the QR Code scanner built into these MFA client apps:: pyotp.totp.TOTP('JBSWY3DPEHPK3PXP').provisioning_uri("alice@google.com", issuer_name="Secure App") >>> 'otpauth://totp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App' pyotp.hotp.HOTP('JBSWY3DPEHPK3PXP').provisioning_uri("alice@google.com", initial_count=0, issuer_name="Secure App") >>> 'otpauth://hotp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App&counter=0' This URL can then be rendered as a QR Code (for example, using https://github.com/neocotic/qrious) which can then be scanned and added to the users list of OTP credentials. Working example ~~~~~~~~~~~~~~~ Scan the following barcode with your phone's OTP app (e.g. Google Authenticator): .. image:: http://chart.apis.google.com/chart?cht=qr&chs=250x250&chl=otpauth%3A%2F%2Ftotp%2Falice%40google.com%3Fsecret%3DJBSWY3DPEHPK3PXP Now run the following and compare the output:: import pyotp totp = pyotp.TOTP("JBSWY3DPEHPK3PXP") print("Current OTP:", totp.now()) Links ~~~~~ * `Project home page (GitHub) `_ * `Documentation (Read the Docs) `_ * `Package distribution (PyPI) `_ * `Change log `_ * `RFC 4226: HOTP: An HMAC-Based One-Time Password `_ * `RFC 6238: TOTP: Time-Based One-Time Password Algorithm `_ * `ROTP `_ - Original Ruby OTP library by `Mark Percival `_ * `OTPHP `_ - PHP port of ROTP by `Le Lag `_ * `OWASP Authentication Cheat Sheet `_ * `NIST SP 800-63-3: Digital Authentication Guideline `_ .. image:: https://img.shields.io/travis/pyotp/pyotp.svg :target: https://travis-ci.org/pyotp/pyotp .. image:: https://img.shields.io/codecov/c/github/pyotp/pyotp/master.svg :target: https://codecov.io/github/pyotp/pyotp?branch=master .. image:: https://img.shields.io/pypi/v/pyotp.svg :target: https://pypi.python.org/pypi/pyotp .. image:: https://img.shields.io/pypi/l/pyotp.svg :target: https://pypi.python.org/pypi/pyotp .. image:: https://readthedocs.org/projects/pyotp/badge/?version=latest :target: https://pyotp.readthedocs.io/ Platform: MacOS X Platform: Posix Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: MacOS :: MacOS X Classifier: Operating System :: POSIX Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Topic :: Software Development :: Libraries :: Python Modules