Hello community, here is the log from the commit of package python-pyotp for openSUSE:Factory checked in at 2020-08-17 14:39:45 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-pyotp (Old) and /work/SRC/openSUSE:Factory/.python-pyotp.new.3399 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pyotp" Mon Aug 17 14:39:45 2020 rev:3 rq:824895 version:2.4.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-pyotp/python-pyotp.changes 2019-07-31 14:29:33.746073441 +0200 +++ /work/SRC/openSUSE:Factory/.python-pyotp.new.3399/python-pyotp.changes 2020-08-17 14:39:50.622568939 +0200 @@ -1,0 +2,11 @@ +Fri Aug 7 13:13:17 UTC 2020 - Marketa Calabkova <[email protected]> + +- update to 2.4.0 + * Fix data type for at(for_time) (#85) + * Add support for parsing provisioning URIs (#84) + * Raise error when trying to generate secret that is too short (The + secret must be at least 128 bits) + * Add random_hex function (#82) + * Drop Python 2.7 + +------------------------------------------------------------------- Old: ---- pyotp-2.3.0.tar.gz New: ---- pyotp-2.4.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-pyotp.spec ++++++ --- /var/tmp/diff_new_pack.6MMnmd/_old 2020-08-17 14:39:51.922569477 +0200 +++ /var/tmp/diff_new_pack.6MMnmd/_new 2020-08-17 14:39:51.926569479 +0200 @@ -1,7 +1,7 @@ # # spec file for package python-pyotp # -# Copyright (c) 2019 SUSE LINUX GmbH, Nuernberg, Germany. +# Copyright (c) 2020 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -17,13 +17,14 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} +%define skip_python2 1 Name: python-pyotp -Version: 2.3.0 +Version: 2.4.0 Release: 0 Summary: Python One Time Password Library License: MIT Group: Development/Languages/Python -Url: https://github.com/pyotp/pyotp +URL: https://github.com/pyotp/pyotp Source: https://files.pythonhosted.org/packages/source/p/pyotp/pyotp-%{version}.tar.gz BuildRequires: %{python_module devel} BuildRequires: %{python_module setuptools} @@ -48,7 +49,7 @@ %python_expand %fdupes %{buildroot}%{$python_sitelib} %check -%python_exec setup.py test +%pyunittest %files %{python_files} %license LICENSE ++++++ pyotp-2.3.0.tar.gz -> pyotp-2.4.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyotp-2.3.0/MANIFEST.in new/pyotp-2.4.0/MANIFEST.in --- old/pyotp-2.3.0/MANIFEST.in 2017-03-16 20:52:39.000000000 +0100 +++ new/pyotp-2.4.0/MANIFEST.in 2020-07-29 22:05:34.000000000 +0200 @@ -1,4 +1,4 @@ include LICENSE -include README.markdown +include README.rst include requirements.txt include test.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyotp-2.3.0/PKG-INFO new/pyotp-2.4.0/PKG-INFO --- old/pyotp-2.3.0/PKG-INFO 2019-07-26 19:00:12.000000000 +0200 +++ new/pyotp-2.4.0/PKG-INFO 2020-07-29 22:06:57.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: pyotp -Version: 2.3.0 +Version: 2.4.0 Summary: Python One Time Password Library Home-page: https://github.com/pyotp/pyotp Author: PyOTP contributors @@ -84,11 +84,15 @@ hotp.verify('316439', 1401) # => True hotp.verify('316439', 1402) # => False - Generating a base32 Secret Key - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - :: + Generating a Secret Key + ~~~~~~~~~~~~~~~~~~~~~~~ + A helper function is provided to generate a 16 character base32 secret, compatible with Google Authenticator and other OTP apps:: + + pyotp.random_base32() - pyotp.random_base32() # returns a 16 character base32 secret. Compatible with Google Authenticator and other OTP apps + Some applications want the secret key to be formatted as a hex-encoded string:: + + pyotp.random_hex() # returns a 32-character hex-encoded secret Google Authenticator Compatible ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -96,17 +100,27 @@ 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("[email protected]", issuer_name="Secure App") + pyotp.totp.TOTP('JBSWY3DPEHPK3PXP').provisioning_uri(name='[email protected]', issuer_name='Secure App') >>> 'otpauth://totp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App' - pyotp.hotp.HOTP('JBSWY3DPEHPK3PXP').provisioning_uri("[email protected]", initial_count=0, issuer_name="Secure App") + pyotp.hotp.HOTP('JBSWY3DPEHPK3PXP').provisioning_uri(name="[email protected]", issuer_name="Secure App", initial_count=0) >>> '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. + Parsing these URLs is also supported:: + + pyotp.parse_uri('otpauth://totp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App') + + >>> <pyotp.totp.TOTP object at 0xFFFFFFFF> + + pyotp.parse_uri('otpauth://hotp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App&counter=0' + + >>> <pyotp.totp.HOTP object at 0xFFFFFFFF> + Working example ~~~~~~~~~~~~~~~ @@ -123,10 +137,10 @@ Links ~~~~~ - * `Project home page (GitHub) <https://github.com/pyotp/pyotp>`_ + * `Project home page (GitHub) <https://github.com/pyauth/pyotp>`_ * `Documentation (Read the Docs) <https://pyotp.readthedocs.io/en/latest/>`_ * `Package distribution (PyPI) <https://pypi.python.org/pypi/pyotp>`_ - * `Change log <https://github.com/pyotp/pyotp/blob/master/Changes.rst>`_ + * `Change log <https://github.com/pyauth/pyotp/blob/master/Changes.rst>`_ * `RFC 4226: HOTP: An HMAC-Based One-Time Password <https://tools.ietf.org/html/rfc4226>`_ * `RFC 6238: TOTP: Time-Based One-Time Password Algorithm <https://tools.ietf.org/html/rfc6238>`_ * `ROTP <https://github.com/mdp/rotp>`_ - Original Ruby OTP library by `Mark Percival <https://github.com/mdp>`_ @@ -139,10 +153,10 @@ * `WebAuthn <https://www.w3.org/TR/webauthn/>`_ * `PyWARP <https://github.com/pyauth/pywarp>`_ - .. 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://github.com/pyauth/pyotp/workflows/Python%20package/badge.svg + :target: https://github.com/pyauth/pyotp/actions + .. image:: https://img.shields.io/codecov/c/github/pyauth/pyotp/master.svg + :target: https://codecov.io/github/pyauth/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 @@ -157,7 +171,6 @@ 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.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyotp-2.3.0/README.rst new/pyotp-2.4.0/README.rst --- old/pyotp-2.3.0/README.rst 2019-07-26 18:44:42.000000000 +0200 +++ new/pyotp-2.4.0/README.rst 2020-07-29 22:05:34.000000000 +0200 @@ -76,11 +76,15 @@ hotp.verify('316439', 1401) # => True hotp.verify('316439', 1402) # => False -Generating a base32 Secret Key -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -:: +Generating a Secret Key +~~~~~~~~~~~~~~~~~~~~~~~ +A helper function is provided to generate a 16 character base32 secret, compatible with Google Authenticator and other OTP apps:: - pyotp.random_base32() # returns a 16 character base32 secret. Compatible with Google Authenticator and other OTP apps + pyotp.random_base32() + +Some applications want the secret key to be formatted as a hex-encoded string:: + + pyotp.random_hex() # returns a 32-character hex-encoded secret Google Authenticator Compatible ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -88,17 +92,27 @@ 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("[email protected]", issuer_name="Secure App") + pyotp.totp.TOTP('JBSWY3DPEHPK3PXP').provisioning_uri(name='[email protected]', issuer_name='Secure App') >>> 'otpauth://totp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App' - pyotp.hotp.HOTP('JBSWY3DPEHPK3PXP').provisioning_uri("[email protected]", initial_count=0, issuer_name="Secure App") + pyotp.hotp.HOTP('JBSWY3DPEHPK3PXP').provisioning_uri(name="[email protected]", issuer_name="Secure App", initial_count=0) >>> '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. +Parsing these URLs is also supported:: + + pyotp.parse_uri('otpauth://totp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App') + + >>> <pyotp.totp.TOTP object at 0xFFFFFFFF> + + pyotp.parse_uri('otpauth://hotp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App&counter=0' + + >>> <pyotp.totp.HOTP object at 0xFFFFFFFF> + Working example ~~~~~~~~~~~~~~~ @@ -115,10 +129,10 @@ Links ~~~~~ -* `Project home page (GitHub) <https://github.com/pyotp/pyotp>`_ +* `Project home page (GitHub) <https://github.com/pyauth/pyotp>`_ * `Documentation (Read the Docs) <https://pyotp.readthedocs.io/en/latest/>`_ * `Package distribution (PyPI) <https://pypi.python.org/pypi/pyotp>`_ -* `Change log <https://github.com/pyotp/pyotp/blob/master/Changes.rst>`_ +* `Change log <https://github.com/pyauth/pyotp/blob/master/Changes.rst>`_ * `RFC 4226: HOTP: An HMAC-Based One-Time Password <https://tools.ietf.org/html/rfc4226>`_ * `RFC 6238: TOTP: Time-Based One-Time Password Algorithm <https://tools.ietf.org/html/rfc6238>`_ * `ROTP <https://github.com/mdp/rotp>`_ - Original Ruby OTP library by `Mark Percival <https://github.com/mdp>`_ @@ -131,10 +145,10 @@ * `WebAuthn <https://www.w3.org/TR/webauthn/>`_ * `PyWARP <https://github.com/pyauth/pywarp>`_ -.. 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://github.com/pyauth/pyotp/workflows/Python%20package/badge.svg + :target: https://github.com/pyauth/pyotp/actions +.. image:: https://img.shields.io/codecov/c/github/pyauth/pyotp/master.svg + :target: https://codecov.io/github/pyauth/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 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyotp-2.3.0/setup.cfg new/pyotp-2.4.0/setup.cfg --- old/pyotp-2.3.0/setup.cfg 2019-07-26 19:00:12.000000000 +0200 +++ new/pyotp-2.4.0/setup.cfg 2020-07-29 22:06:57.000000000 +0200 @@ -3,7 +3,7 @@ [flake8] max-line-length = 120 -ignore = E301, E302, W504 +ignore = E401, W504 [egg_info] tag_build = diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyotp-2.3.0/setup.py new/pyotp-2.4.0/setup.py --- old/pyotp-2.3.0/setup.py 2019-07-26 18:59:59.000000000 +0200 +++ new/pyotp-2.4.0/setup.py 2020-07-29 22:05:54.000000000 +0200 @@ -7,7 +7,7 @@ setup( name="pyotp", - version="2.3.0", + version="2.4.0", url="https://github.com/pyotp/pyotp", license="MIT License", author="PyOTP contributors", @@ -17,6 +17,7 @@ install_requires=install_requires, packages=["pyotp"], package_dir={"": "src"}, + package_data={"pyotp": ["py.typed"]}, platforms=["MacOS X", "Posix"], zip_safe=False, test_suite="test", @@ -26,7 +27,6 @@ "Operating System :: MacOS :: MacOS X", "Operating System :: POSIX", "Programming Language :: Python", - "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyotp-2.3.0/src/pyotp/__init__.py new/pyotp-2.4.0/src/pyotp/__init__.py --- old/pyotp-2.3.0/src/pyotp/__init__.py 2018-11-06 01:18:08.000000000 +0100 +++ new/pyotp-2.4.0/src/pyotp/__init__.py 2020-07-29 22:05:34.000000000 +0200 @@ -1,23 +1,102 @@ -from __future__ import (absolute_import, division, - print_function, unicode_literals) +from typing import Any, Dict, Sequence -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=None, - chars=list('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')): - - # Use secrets module if available (Python version >= 3.6) per PEP 506 - try: - import secrets - random = secrets.SystemRandom() - except ImportError: - import random as _random - random = _random.SystemRandom() +import hashlib +from re import split +from urllib.parse import unquote, urlparse, parse_qsl + +from .compat import random +from .hotp import HOTP as HOTP +from .otp import OTP as OTP +from .totp import TOTP as TOTP + + +def random_base32( + length: int = 16, + chars: Sequence[str] = list('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')) -> str: + if length < 16: + raise Exception("Secrets should be at least 128 bits") return ''.join( random.choice(chars) for _ in range(length) ) + + +def random_hex( + length: int = 32, + chars: Sequence[str] = list('ABCDEF0123456789')) -> str: + if length < 32: + raise Exception("Secrets should be at least 128 bits") + return random_base32(length=length, chars=chars) + + +def parse_uri(uri: str) -> OTP: + """ + Parses the provisioning URI for the OTP; works for either TOTP or HOTP. + + See also: + https://github.com/google/google-authenticator/wiki/Key-Uri-Format + + :param uri: the hotp/totp URI to parse + :returns: OTP object + """ + + # Secret (to be filled in later) + secret = None + + # Data we'll parse to the correct constructor + otp_data = {} # type: Dict[str, Any] + + # Parse with URLlib + parsed_uri = urlparse(unquote(uri)) + + if parsed_uri.scheme != 'otpauth': + raise ValueError('Not an otpauth URI') + + # Parse issuer/accountname info + accountinfo_parts = split(':|%3A', parsed_uri.path[1:], maxsplit=1) + if len(accountinfo_parts) == 1: + otp_data['name'] = accountinfo_parts[0] + else: + otp_data['issuer'] = accountinfo_parts[0] + otp_data['name'] = accountinfo_parts[1] + + # Parse values + for key, value in parse_qsl(parsed_uri.query): + if key == 'secret': + secret = value + elif key == 'issuer': + if 'issuer' in otp_data and otp_data['issuer'] is not None and otp_data['issuer'] != value: + raise ValueError('If issuer is specified in both label and parameters, it should be equal.') + otp_data['issuer'] = value + elif key == 'algorithm': + if value == 'SHA1': + otp_data['digest'] = hashlib.sha1 + elif value == 'SHA256': + otp_data['digest'] = hashlib.sha256 + elif value == 'SHA512': + otp_data['digest'] = hashlib.sha512 + else: + raise ValueError('Invalid value for algorithm, must be SHA1, SHA256 or SHA512') + elif key == 'digits': + digits = int(value) + if digits not in [6, 8]: + raise ValueError('Digits may only be 6 or 8') + otp_data['digits'] = digits + elif key == 'period': + otp_data['interval'] = value + elif key == 'counter': + otp_data['initial_count'] = value + else: + raise ValueError('{} is not a valid parameter'.format(key)) + + if not secret: + raise ValueError('No secret found in URI') + + # Create objects + if parsed_uri.netloc == 'totp': + return TOTP(secret, **otp_data) + elif parsed_uri.netloc == 'hotp': + return HOTP(secret, **otp_data) + + raise ValueError('Not a supported OTP type') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyotp-2.3.0/src/pyotp/compat.py new/pyotp-2.4.0/src/pyotp/compat.py --- old/pyotp-2.3.0/src/pyotp/compat.py 2017-03-16 20:52:39.000000000 +0100 +++ new/pyotp-2.4.0/src/pyotp/compat.py 2020-07-29 22:05:34.000000000 +0200 @@ -1,10 +1,7 @@ -from __future__ import absolute_import, division, print_function, unicode_literals +# Use secrets module if available (Python version >= 3.6) per PEP 506 +try: + from secrets import SystemRandom # type: ignore +except ImportError: + from random import SystemRandom -import sys - -USING_PYTHON2 = True if sys.version_info < (3, 0) else False - -if USING_PYTHON2: - str = unicode # noqa -else: - str = str +random = SystemRandom() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyotp-2.3.0/src/pyotp/hotp.py new/pyotp-2.4.0/src/pyotp/hotp.py --- old/pyotp-2.3.0/src/pyotp/hotp.py 2018-11-06 00:44:51.000000000 +0100 +++ new/pyotp-2.4.0/src/pyotp/hotp.py 2020-07-29 22:05:34.000000000 +0200 @@ -1,36 +1,43 @@ -from __future__ import absolute_import, division, print_function, unicode_literals +from typing import Any, Optional from . import utils from .otp import OTP -from .compat import str + class HOTP(OTP): """ Handler for HMAC-based OTP counters. """ - def at(self, count): + def __init__(self, *args: Any, initial_count: int = 0, **kwargs: Any) -> None: + """ + :param initial_count: starting HMAC counter value, defaults to 0 + """ + self.initial_count = initial_count + super(HOTP, self).__init__(*args, **kwargs) + + def at(self, count: int) -> str: """ 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): + def verify(self, otp: str, counter: int) -> bool: """ Verifies the OTP passed in against the current counter OTP. :param otp: the OTP to check against - :type otp: str - :param count: the OTP HMAC counter - :type count: int + :param counter: the OTP HMAC counter """ return utils.strings_equal(str(otp), str(self.at(counter))) - def provisioning_uri(self, name, initial_count=0, issuer_name=None): + def provisioning_uri( + self, + name: Optional[str] = None, + initial_count: Optional[int] = None, + issuer_name: Optional[str] = None) -> str: """ Returns the provisioning URI for the OTP. This can then be encoded in a QR Code and used to provision an OTP app like @@ -40,19 +47,16 @@ 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, + name=name if name else self.name, + initial_count=initial_count if initial_count else self.initial_count, + issuer=issuer_name if issuer_name else self.issuer, algorithm=self.digest().name, digits=self.digits ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyotp-2.3.0/src/pyotp/otp.py new/pyotp-2.4.0/src/pyotp/otp.py --- old/pyotp-2.3.0/src/pyotp/otp.py 2017-06-03 20:26:45.000000000 +0200 +++ new/pyotp-2.4.0/src/pyotp/otp.py 2020-07-29 22:05:34.000000000 +0200 @@ -1,32 +1,39 @@ -from __future__ import absolute_import, division, print_function, unicode_literals +from typing import Any, Optional 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): + def __init__( + self, + s: str, + digits: int = 6, + digest: Any = hashlib.sha1, + name: Optional[str] = None, + issuer: Optional[str] = None + ) -> None: """ :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 + :param name: account name + :param issuer: issuer """ self.digits = digits self.digest = digest self.secret = s + self.name = name or 'Secret' + self.issuer = issuer - def generate_otp(self, input): + def generate_otp(self, input: int) -> str: """ :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') @@ -43,14 +50,14 @@ return str_code - def byte_secret(self): + def byte_secret(self) -> bytes: 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): + def int_to_bytestring(i: int, padding: int = 8) -> bytes: """ Turns an integer to the OATH specified bytestring, which is fed to the HMAC diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyotp-2.3.0/src/pyotp/totp.py new/pyotp-2.4.0/src/pyotp/totp.py --- old/pyotp-2.3.0/src/pyotp/totp.py 2017-06-03 21:03:42.000000000 +0200 +++ new/pyotp-2.4.0/src/pyotp/totp.py 2020-07-29 22:05:34.000000000 +0200 @@ -1,60 +1,57 @@ -from __future__ import absolute_import, division, print_function, unicode_literals +from typing import Any, Union, Optional 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): + def __init__(self, *args: Any, interval: int = 30, **kwargs: Any) -> None: """ :param interval: the time interval in seconds for OTP. This defaults to 30. - :type interval: int """ - self.interval = kwargs.pop('interval', 30) + self.interval = interval super(TOTP, self).__init__(*args, **kwargs) - def at(self, for_time, counter_offset=0): + def at(self, for_time: Union[int, datetime.datetime], counter_offset: int = 0) -> str: """ Accepts either a Unix timestamp integer or a datetime object. + To get the time until the next timecode change (seconds until the current OTP expires), use this instead:: + + totp = pyotp.TOTP(...) + time_remaining = totp.interval - datetime.datetime.now().timestamp() % totp.interval + :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): + def now(self) -> str: """ 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): + def verify(self, otp: str, for_time: Optional[datetime.datetime] = None, valid_window: int = 0) -> bool: """ 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() @@ -67,7 +64,7 @@ return utils.strings_equal(str(otp), str(self.at(for_time))) - def provisioning_uri(self, name, issuer_name=None): + def provisioning_uri(self, name: Optional[str] = None, issuer_name: Optional[str] = None) -> str: """ Returns the provisioning URI for the OTP. This can then be encoded in a QR Code and used to provision an OTP app like @@ -76,17 +73,12 @@ 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, + return utils.build_uri(self.secret, name if name else self.name, + issuer=issuer_name if issuer_name else self.issuer, algorithm=self.digest().name, digits=self.digits, period=self.interval) - def timecode(self, for_time): + def timecode(self, for_time: datetime.datetime) -> int: i = time.mktime(for_time.timetuple()) return int(i / self.interval) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyotp-2.3.0/src/pyotp/utils.py new/pyotp-2.4.0/src/pyotp/utils.py --- old/pyotp-2.3.0/src/pyotp/utils.py 2019-07-26 18:58:24.000000000 +0200 +++ new/pyotp-2.4.0/src/pyotp/utils.py 2020-07-29 22:05:34.000000000 +0200 @@ -1,19 +1,18 @@ -from __future__ import absolute_import, division, print_function, unicode_literals +from typing import Dict, Optional, Union 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 +from hmac import compare_digest +from urllib.parse import quote, urlencode -def build_uri(secret, name, initial_count=None, issuer_name=None, - algorithm=None, digits=None, period=None): +def build_uri( + secret: str, + name: str, + initial_count: Optional[int] = None, + issuer: Optional[str] = None, + algorithm: Optional[str] = None, + digits: Optional[int] = None, + period: Optional[int] = None) -> str: """ Returns the provisioning URI for the OTP; works for either TOTP or HOTP. @@ -26,24 +25,16 @@ 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 + :param issuer: 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) @@ -56,17 +47,17 @@ otp_type = 'hotp' if is_initial_count_present else 'totp' base_uri = 'otpauth://{0}/{1}?{2}' - url_args = {'secret': secret} + url_args = {'secret': secret} # type: Dict[str, Union[None, int, str]] label = quote(name) - if issuer_name is not None: - label = quote(issuer_name) + ':' + label - url_args['issuer'] = issuer_name + if issuer is not None: + label = quote(issuer) + ':' + label + url_args['issuer'] = issuer if is_initial_count_present: url_args['counter'] = initial_count if is_algorithm_set: - url_args['algorithm'] = algorithm.upper() + url_args['algorithm'] = algorithm.upper() # type: ignore if is_digits_set: url_args['digits'] = digits if is_period_set: @@ -76,26 +67,7 @@ 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): +def strings_equal(s1: str, s2: str) -> bool: """ Timing-attack resistant string comparison. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyotp-2.3.0/src/pyotp.egg-info/PKG-INFO new/pyotp-2.4.0/src/pyotp.egg-info/PKG-INFO --- old/pyotp-2.3.0/src/pyotp.egg-info/PKG-INFO 2019-07-26 19:00:12.000000000 +0200 +++ new/pyotp-2.4.0/src/pyotp.egg-info/PKG-INFO 2020-07-29 22:06:57.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: pyotp -Version: 2.3.0 +Version: 2.4.0 Summary: Python One Time Password Library Home-page: https://github.com/pyotp/pyotp Author: PyOTP contributors @@ -84,11 +84,15 @@ hotp.verify('316439', 1401) # => True hotp.verify('316439', 1402) # => False - Generating a base32 Secret Key - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - :: + Generating a Secret Key + ~~~~~~~~~~~~~~~~~~~~~~~ + A helper function is provided to generate a 16 character base32 secret, compatible with Google Authenticator and other OTP apps:: + + pyotp.random_base32() - pyotp.random_base32() # returns a 16 character base32 secret. Compatible with Google Authenticator and other OTP apps + Some applications want the secret key to be formatted as a hex-encoded string:: + + pyotp.random_hex() # returns a 32-character hex-encoded secret Google Authenticator Compatible ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -96,17 +100,27 @@ 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("[email protected]", issuer_name="Secure App") + pyotp.totp.TOTP('JBSWY3DPEHPK3PXP').provisioning_uri(name='[email protected]', issuer_name='Secure App') >>> 'otpauth://totp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App' - pyotp.hotp.HOTP('JBSWY3DPEHPK3PXP').provisioning_uri("[email protected]", initial_count=0, issuer_name="Secure App") + pyotp.hotp.HOTP('JBSWY3DPEHPK3PXP').provisioning_uri(name="[email protected]", issuer_name="Secure App", initial_count=0) >>> '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. + Parsing these URLs is also supported:: + + pyotp.parse_uri('otpauth://totp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App') + + >>> <pyotp.totp.TOTP object at 0xFFFFFFFF> + + pyotp.parse_uri('otpauth://hotp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App&counter=0' + + >>> <pyotp.totp.HOTP object at 0xFFFFFFFF> + Working example ~~~~~~~~~~~~~~~ @@ -123,10 +137,10 @@ Links ~~~~~ - * `Project home page (GitHub) <https://github.com/pyotp/pyotp>`_ + * `Project home page (GitHub) <https://github.com/pyauth/pyotp>`_ * `Documentation (Read the Docs) <https://pyotp.readthedocs.io/en/latest/>`_ * `Package distribution (PyPI) <https://pypi.python.org/pypi/pyotp>`_ - * `Change log <https://github.com/pyotp/pyotp/blob/master/Changes.rst>`_ + * `Change log <https://github.com/pyauth/pyotp/blob/master/Changes.rst>`_ * `RFC 4226: HOTP: An HMAC-Based One-Time Password <https://tools.ietf.org/html/rfc4226>`_ * `RFC 6238: TOTP: Time-Based One-Time Password Algorithm <https://tools.ietf.org/html/rfc6238>`_ * `ROTP <https://github.com/mdp/rotp>`_ - Original Ruby OTP library by `Mark Percival <https://github.com/mdp>`_ @@ -139,10 +153,10 @@ * `WebAuthn <https://www.w3.org/TR/webauthn/>`_ * `PyWARP <https://github.com/pyauth/pywarp>`_ - .. 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://github.com/pyauth/pyotp/workflows/Python%20package/badge.svg + :target: https://github.com/pyauth/pyotp/actions + .. image:: https://img.shields.io/codecov/c/github/pyauth/pyotp/master.svg + :target: https://codecov.io/github/pyauth/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 @@ -157,7 +171,6 @@ 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.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyotp-2.3.0/src/pyotp.egg-info/SOURCES.txt new/pyotp-2.4.0/src/pyotp.egg-info/SOURCES.txt --- old/pyotp-2.3.0/src/pyotp.egg-info/SOURCES.txt 2019-07-26 19:00:12.000000000 +0200 +++ new/pyotp-2.4.0/src/pyotp.egg-info/SOURCES.txt 2020-07-29 22:06:57.000000000 +0200 @@ -9,6 +9,7 @@ src/pyotp/compat.py src/pyotp/hotp.py src/pyotp/otp.py +src/pyotp/py.typed src/pyotp/totp.py src/pyotp/utils.py src/pyotp.egg-info/PKG-INFO diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyotp-2.3.0/test.py new/pyotp-2.4.0/test.py --- old/pyotp-2.3.0/test.py 2019-07-26 18:44:42.000000000 +0200 +++ new/pyotp-2.4.0/test.py 2020-07-29 22:05:34.000000000 +0200 @@ -1,18 +1,14 @@ #!/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 +from urllib.parse import urlparse, parse_qsl sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) -import pyotp # noqa +import pyotp + class HOTPExampleValuesFromTheRFC(unittest.TestCase): def test_match_rfc(self): @@ -42,37 +38,55 @@ self.assertFalse(hotp.verify('520489', 10)) def test_provisioning_uri(self): - hotp = pyotp.HOTP('wrn3pqx5uqxqvnqr') + hotp = pyotp.HOTP('wrn3pqx5uqxqvnqr', name='mark@percival') - url = urlparse( - hotp.provisioning_uri('mark@percival')) + url = urlparse(hotp.provisioning_uri()) 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'}) + self.assertEqual( + hotp.provisioning_uri(), + pyotp.parse_uri( + hotp.provisioning_uri() + ).provisioning_uri() + ) - url = urlparse( - hotp.provisioning_uri('mark@percival', initial_count=12)) + hotp = pyotp.HOTP('wrn3pqx5uqxqvnqr', name='mark@percival', initial_count=12) + url = urlparse(hotp.provisioning_uri()) 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( + hotp.provisioning_uri(), + pyotp.parse_uri( + hotp.provisioning_uri() + ).provisioning_uri() + ) + + hotp = pyotp.HOTP('wrn3pqx5uqxqvnqr', name='mark@percival', + issuer='FooCorp!') + url = urlparse(hotp.provisioning_uri()) 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!'}) + self.assertEqual( + hotp.provisioning_uri(), + pyotp.parse_uri( + hotp.provisioning_uri() + ).provisioning_uri() + ) key = 'c7uxuqhgflpw7oruedmglbrk7u6242vb' - hotp = pyotp.HOTP(key, digits=8, digest=hashlib.sha256) - url = urlparse( - hotp.provisioning_uri('baco@peperina', issuer_name='FooCorp')) + hotp = pyotp.HOTP(key, digits=8, digest=hashlib.sha256, + name='baco@peperina', issuer='FooCorp') + url = urlparse(hotp.provisioning_uri()) self.assertEqual(url.scheme, 'otpauth') self.assertEqual(url.netloc, 'hotp') self.assertEqual(url.path, '/FooCorp:baco%40peperina') @@ -80,11 +94,16 @@ {'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( + hotp.provisioning_uri(), + pyotp.parse_uri( + hotp.provisioning_uri() + ).provisioning_uri() + ) + + hotp = pyotp.HOTP(key, digits=8, name='baco@peperina', + issuer='Foo Corp', initial_count=10) + url = urlparse(hotp.provisioning_uri()) self.assertEqual(url.scheme, 'otpauth') self.assertEqual(url.netloc, 'hotp') self.assertEqual(url.path, '/Foo%20Corp:baco%40peperina') @@ -92,6 +111,12 @@ {'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb', 'counter': '10', 'issuer': 'Foo Corp', 'digits': '8'}) + self.assertEqual( + hotp.provisioning_uri(), + pyotp.parse_uri( + hotp.provisioning_uri() + ).provisioning_uri() + ) def test_other_secret(self): hotp = pyotp.HOTP( @@ -183,27 +208,40 @@ self.assertFalse(totp.verify('050471')) def test_provisioning_uri(self): - totp = pyotp.TOTP('wrn3pqx5uqxqvnqr') - url = urlparse( - totp.provisioning_uri('mark@percival')) + totp = pyotp.TOTP('wrn3pqx5uqxqvnqr', name='mark@percival') + url = urlparse(totp.provisioning_uri()) 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( + totp.provisioning_uri(), + pyotp.parse_uri( + totp.provisioning_uri() + ).provisioning_uri() + ) + + totp = pyotp.TOTP('wrn3pqx5uqxqvnqr', name='mark@percival', + issuer='FooCorp!') + url = urlparse(totp.provisioning_uri()) 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!'}) + self.assertEqual( + totp.provisioning_uri(), + pyotp.parse_uri( + totp.provisioning_uri() + ).provisioning_uri() + ) key = 'c7uxuqhgflpw7oruedmglbrk7u6242vb' - totp = pyotp.TOTP(key, digits=8, interval=60, digest=hashlib.sha256) - url = urlparse(totp.provisioning_uri('baco@peperina', issuer_name='FooCorp')) + totp = pyotp.TOTP(key, digits=8, interval=60, digest=hashlib.sha256, + name='baco@peperina', issuer='FooCorp') + url = urlparse(totp.provisioning_uri()) self.assertEqual(url.scheme, 'otpauth') self.assertEqual(url.netloc, 'totp') self.assertEqual(url.path, '/FooCorp:baco%40peperina') @@ -212,9 +250,16 @@ '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( + totp.provisioning_uri(), + pyotp.parse_uri( + totp.provisioning_uri() + ).provisioning_uri() + ) + + totp = pyotp.TOTP(key, digits=8, interval=60, + name='baco@peperina', issuer='FooCorp') + url = urlparse(totp.provisioning_uri()) self.assertEqual(url.scheme, 'otpauth') self.assertEqual(url.netloc, 'totp') self.assertEqual(url.path, '/FooCorp:baco%40peperina') @@ -222,9 +267,15 @@ {'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb', 'issuer': 'FooCorp', 'digits': '8', 'period': '60'}) + self.assertEqual( + totp.provisioning_uri(), + pyotp.parse_uri( + totp.provisioning_uri() + ).provisioning_uri() + ) - totp = pyotp.TOTP(key, digits=8) - url = urlparse(totp.provisioning_uri('baco@peperina', issuer_name='FooCorp')) + totp = pyotp.TOTP(key, digits=8, name='baco@peperina', issuer='FooCorp') + url = urlparse(totp.provisioning_uri()) self.assertEqual(url.scheme, 'otpauth') self.assertEqual(url.netloc, 'totp') self.assertEqual(url.path, '/FooCorp:baco%40peperina') @@ -232,10 +283,22 @@ {'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb', 'issuer': 'FooCorp', 'digits': '8'}) + self.assertEqual( + totp.provisioning_uri(), + pyotp.parse_uri( + totp.provisioning_uri() + ).provisioning_uri() + ) def test_random_key_generation(self): self.assertEqual(len(pyotp.random_base32()), 16) self.assertEqual(len(pyotp.random_base32(length=20)), 20) + self.assertEqual(len(pyotp.random_hex()), 32) + self.assertEqual(len(pyotp.random_hex(length=64)), 64) + with self.assertRaises(Exception): + pyotp.random_base32(length=15) + with self.assertRaises(Exception): + pyotp.random_hex(length=24) class CompareDigestTest(unittest.TestCase): @@ -251,10 +314,6 @@ 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) @@ -280,6 +339,45 @@ self.assertTrue(totp.verify("681610", 200, 1)) self.assertFalse(totp.verify("195979", 200, 1)) +class ParseUriTest(unittest.TestCase): + def test_invalids(self): + with self.assertRaises(ValueError) as cm: + pyotp.parse_uri('http://hello.com') + self.assertEqual('Not an otpauth URI', str(cm.exception)) + + with self.assertRaises(ValueError) as cm: + pyotp.parse_uri('otpauth://totp') + self.assertEqual('No secret found in URI', str(cm.exception)) + + with self.assertRaises(ValueError) as cm: + pyotp.parse_uri('otpauth://derp?secret=foo') + self.assertEqual('Not a supported OTP type', str(cm.exception)) + + with self.assertRaises(ValueError) as cm: + pyotp.parse_uri('otpauth://totp?foo=secret') + self.assertEqual('foo is not a valid parameter', str(cm.exception)) + + with self.assertRaises(ValueError) as cm: + pyotp.parse_uri('otpauth://totp?digits=-1') + self.assertEqual('Digits may only be 6 or 8', str(cm.exception)) + + with self.assertRaises(ValueError) as cm: + pyotp.parse_uri('otpauth://totp/SomeIssuer:?issuer=AnotherIssuer') + self.assertEqual('If issuer is specified in both label and parameters, it should be equal.', str(cm.exception)) + + with self.assertRaises(ValueError) as cm: + pyotp.parse_uri('otpauth://totp?algorithm=aes') + self.assertEqual('Invalid value for algorithm, must be SHA1, SHA256 or SHA512', str(cm.exception)) + + def test_algorithms(self): + otp = pyotp.parse_uri('otpauth://totp?algorithm=SHA1&secret=123456&algorithm=SHA1') + self.assertEqual(hashlib.sha1, otp.digest) + + otp = pyotp.parse_uri('otpauth://totp?algorithm=SHA1&secret=123456&algorithm=SHA256') + self.assertEqual(hashlib.sha256, otp.digest) + + otp = pyotp.parse_uri('otpauth://totp?algorithm=SHA1&secret=123456&algorithm=SHA512') + self.assertEqual(hashlib.sha512, otp.digest) class Timecop(object): """ @@ -305,5 +403,6 @@ timecop = self return FrozenDateTime + if __name__ == '__main__': unittest.main()
