Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-pyotp for openSUSE:Factory checked in at 2021-03-25 14:52:48 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-pyotp (Old) and /work/SRC/openSUSE:Factory/.python-pyotp.new.2401 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pyotp" Thu Mar 25 14:52:48 2021 rev:4 rq:881277 version:2.6.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-pyotp/python-pyotp.changes 2020-08-17 14:39:50.622568939 +0200 +++ /work/SRC/openSUSE:Factory/.python-pyotp.new.2401/python-pyotp.changes 2021-03-25 14:52:49.552516010 +0100 @@ -1,0 +2,11 @@ +Wed Mar 24 10:47:36 UTC 2021 - Martin Hauke <mar...@gmx.de> + +- update to 2.6.0 + * Raise default and minimum base32 secret length to 32, and hex + secret length to 40 (160 bits as recommended by the RFC). + * Fix issue where provisioning_uri would return invalid results + after calling verify(). +- update to 2.5.0 + * parse_uri accepts and ignores optional image parameter. + +------------------------------------------------------------------- Old: ---- pyotp-2.4.0.tar.gz New: ---- pyotp-2.6.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-pyotp.spec ++++++ --- /var/tmp/diff_new_pack.86bdO9/_old 2021-03-25 14:52:50.148516616 +0100 +++ /var/tmp/diff_new_pack.86bdO9/_new 2021-03-25 14:52:50.152516621 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-pyotp # -# Copyright (c) 2020 SUSE LLC +# Copyright (c) 2021 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -19,7 +19,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} %define skip_python2 1 Name: python-pyotp -Version: 2.4.0 +Version: 2.6.0 Release: 0 Summary: Python One Time Password Library License: MIT ++++++ pyotp-2.4.0.tar.gz -> pyotp-2.6.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyotp-2.4.0/LICENSE new/pyotp-2.6.0/LICENSE --- old/pyotp-2.4.0/LICENSE 2017-11-06 17:59:14.000000000 +0100 +++ new/pyotp-2.6.0/LICENSE 2021-02-04 20:19:37.000000000 +0100 @@ -1,4 +1,4 @@ -Copyright (C) 2011-2017 Mark Percival <m...@mdp.im>, +Copyright (C) 2011-2021 Mark Percival <m...@mdp.im>, Nathan Reynolds <em...@nreynolds.co.uk>, Andrey Kislyuk <kisl...@gmail.com>, and PyOTP contributors diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyotp-2.4.0/PKG-INFO new/pyotp-2.6.0/PKG-INFO --- old/pyotp-2.4.0/PKG-INFO 2020-07-29 22:06:57.000000000 +0200 +++ new/pyotp-2.6.0/PKG-INFO 2021-02-04 20:47:47.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: pyotp -Version: 2.4.0 +Version: 2.6.0 Summary: Python One Time Password Library Home-page: https://github.com/pyotp/pyotp Author: PyOTP contributors @@ -138,7 +138,7 @@ ~~~~~ * `Project home page (GitHub) <https://github.com/pyauth/pyotp>`_ - * `Documentation (Read the Docs) <https://pyotp.readthedocs.io/en/latest/>`_ + * `Documentation <https://pyauth.github.io/pyotp/>`_ * `Package distribution (PyPI) <https://pypi.python.org/pypi/pyotp>`_ * `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>`_ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyotp-2.4.0/README.rst new/pyotp-2.6.0/README.rst --- old/pyotp-2.4.0/README.rst 2020-07-29 22:05:34.000000000 +0200 +++ new/pyotp-2.6.0/README.rst 2020-10-17 06:27:44.000000000 +0200 @@ -130,7 +130,7 @@ ~~~~~ * `Project home page (GitHub) <https://github.com/pyauth/pyotp>`_ -* `Documentation (Read the Docs) <https://pyotp.readthedocs.io/en/latest/>`_ +* `Documentation <https://pyauth.github.io/pyotp/>`_ * `Package distribution (PyPI) <https://pypi.python.org/pypi/pyotp>`_ * `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>`_ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyotp-2.4.0/setup.py new/pyotp-2.6.0/setup.py --- old/pyotp-2.4.0/setup.py 2020-07-29 22:05:54.000000000 +0200 +++ new/pyotp-2.6.0/setup.py 2021-02-04 20:45:36.000000000 +0100 @@ -7,7 +7,7 @@ setup( name="pyotp", - version="2.4.0", + version="2.6.0", url="https://github.com/pyotp/pyotp", license="MIT License", author="PyOTP contributors", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyotp-2.4.0/src/pyotp/__init__.py new/pyotp-2.6.0/src/pyotp/__init__.py --- old/pyotp-2.4.0/src/pyotp/__init__.py 2020-07-29 22:05:34.000000000 +0200 +++ new/pyotp-2.6.0/src/pyotp/__init__.py 2021-02-04 20:42:19.000000000 +0100 @@ -1,7 +1,6 @@ -from typing import Any, Dict, Sequence - import hashlib from re import split +from typing import Any, Dict, Sequence from urllib.parse import unquote, urlparse, parse_qsl from .compat import random @@ -10,11 +9,12 @@ 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") +def random_base32(length: int = 32, chars: Sequence[str] = list('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')) -> str: + # Note: the otpauth scheme DOES NOT use base32 padding for secret lengths not divisible by 8. + # Some third-party tools have bugs when dealing with such secrets. + # We might consider warning the user when generating a secret of length not divisible by 8. + if length < 32: + raise ValueError("Secrets should be at least 160 bits") return ''.join( random.choice(chars) @@ -22,11 +22,9 @@ ) -def random_hex( - length: int = 32, - chars: Sequence[str] = list('ABCDEF0123456789')) -> str: - if length < 32: - raise Exception("Secrets should be at least 128 bits") +def random_hex(length: int = 40, chars: Sequence[str] = list('ABCDEF0123456789')) -> str: + if length < 40: + raise ValueError("Secrets should be at least 160 bits") return random_base32(length=length, chars=chars) @@ -80,14 +78,14 @@ 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') + if digits not in [6, 7, 8]: + raise ValueError('Digits may only be 6, 7, or 8') otp_data['digits'] = digits elif key == 'period': - otp_data['interval'] = value + otp_data['interval'] = int(value) elif key == 'counter': - otp_data['initial_count'] = value - else: + otp_data['initial_count'] = int(value) + elif key != 'image': raise ValueError('{} is not a valid parameter'.format(key)) if not secret: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyotp-2.4.0/src/pyotp/hotp.py new/pyotp-2.6.0/src/pyotp/hotp.py --- old/pyotp-2.4.0/src/pyotp/hotp.py 2020-07-29 22:05:34.000000000 +0200 +++ new/pyotp-2.6.0/src/pyotp/hotp.py 2021-01-30 00:12:46.000000000 +0100 @@ -1,3 +1,4 @@ +import hashlib from typing import Any, Optional from . import utils @@ -8,12 +9,18 @@ """ Handler for HMAC-based OTP counters. """ - def __init__(self, *args: Any, initial_count: int = 0, **kwargs: Any) -> None: + def __init__(self, s: str, digits: int = 6, digest: Any = hashlib.sha1, name: Optional[str] = None, + issuer: Optional[str] = None, initial_count: int = 0) -> None: """ + :param s: secret in base32 format :param initial_count: starting HMAC counter value, defaults to 0 + :param digits: number of integers in the OTP. Some apps expect this to be 6 digits, others support more. + :param digest: digest function to use in the HMAC (expected to be sha1) + :param name: account name + :param issuer: issuer """ self.initial_count = initial_count - super(HOTP, self).__init__(*args, **kwargs) + super().__init__(s=s, digits=digits, digest=digest, name=name, issuer=issuer) def at(self, count: int) -> str: """ @@ -22,7 +29,7 @@ :param count: the OTP HMAC counter :returns: OTP """ - return self.generate_otp(count) + return self.generate_otp(self.initial_count + count) def verify(self, otp: str, counter: int) -> bool: """ @@ -37,7 +44,8 @@ self, name: Optional[str] = None, initial_count: Optional[int] = None, - issuer_name: Optional[str] = None) -> str: + issuer_name: Optional[str] = None, + image: 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 @@ -58,5 +66,6 @@ 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 + digits=self.digits, + image=image, ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyotp-2.4.0/src/pyotp/otp.py new/pyotp-2.6.0/src/pyotp/otp.py --- old/pyotp-2.4.0/src/pyotp/otp.py 2020-07-29 22:05:34.000000000 +0200 +++ new/pyotp-2.6.0/src/pyotp/otp.py 2021-02-04 20:19:45.000000000 +0100 @@ -1,29 +1,15 @@ -from typing import Any, Optional - import base64 import hashlib import hmac +from typing import Any, Optional class OTP(object): """ Base class for OTP handlers. """ - 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 - :param digits: number of integers in the OTP. Some apps expect this to be 6 digits, others support more. - :param digest: digest function to use in the HMAC (expected to be sha1) - :param name: account name - :param issuer: issuer - """ + def __init__(self, s: str, digits: int = 6, digest: Any = hashlib.sha1, name: Optional[str] = None, + issuer: Optional[str] = None) -> None: self.digits = digits self.digest = digest self.secret = s @@ -51,10 +37,11 @@ return str_code def byte_secret(self) -> bytes: - missing_padding = len(self.secret) % 8 + secret = self.secret + missing_padding = len(secret) % 8 if missing_padding != 0: - self.secret += '=' * (8 - missing_padding) - return base64.b32decode(self.secret, casefold=True) + secret += '=' * (8 - missing_padding) + return base64.b32decode(secret, casefold=True) @staticmethod def int_to_bytestring(i: int, padding: int = 8) -> bytes: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyotp-2.4.0/src/pyotp/totp.py new/pyotp-2.6.0/src/pyotp/totp.py --- old/pyotp-2.4.0/src/pyotp/totp.py 2020-07-29 22:05:34.000000000 +0200 +++ new/pyotp-2.6.0/src/pyotp/totp.py 2021-01-30 00:12:46.000000000 +0100 @@ -1,7 +1,8 @@ -from typing import Any, Union, Optional - +import calendar import datetime +import hashlib import time +from typing import Any, Union, Optional from . import utils from .otp import OTP @@ -11,13 +12,18 @@ """ Handler for time-based OTP counters. """ - def __init__(self, *args: Any, interval: int = 30, **kwargs: Any) -> None: + def __init__(self, s: str, digits: int = 6, digest: Any = hashlib.sha1, name: Optional[str] = None, + issuer: Optional[str] = None, interval: int = 30) -> None: """ - :param interval: the time interval in seconds - for OTP. This defaults to 30. + :param s: secret in base32 format + :param interval: the time interval in seconds for OTP. This defaults to 30. + :param digits: number of integers in the OTP. Some apps expect this to be 6 digits, others support more. + :param digest: digest function to use in the HMAC (expected to be sha1) + :param name: account name + :param issuer: issuer """ self.interval = interval - super(TOTP, self).__init__(*args, **kwargs) + super().__init__(s=s, digits=digits, digest=digest, name=name, issuer=issuer) def at(self, for_time: Union[int, datetime.datetime], counter_offset: int = 0) -> str: """ @@ -64,7 +70,9 @@ return utils.strings_equal(str(otp), str(self.at(for_time))) - def provisioning_uri(self, name: Optional[str] = None, issuer_name: Optional[str] = None) -> str: + def provisioning_uri(self, name: Optional[str] = None, issuer_name: Optional[str] = None, + image: 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 @@ -77,8 +85,16 @@ 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) + digits=self.digits, period=self.interval, image=image) def timecode(self, for_time: datetime.datetime) -> int: - i = time.mktime(for_time.timetuple()) - return int(i / self.interval) + """ + Accepts either a timezone naive (`for_time.tzinfo is None`) or + a timezone aware datetime as argument and returns the + corresponding counter value (timecode). + + """ + if for_time.tzinfo: + return int(calendar.timegm(for_time.utctimetuple()) / self.interval) + else: + return int(time.mktime(for_time.timetuple()) / self.interval) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyotp-2.4.0/src/pyotp/utils.py new/pyotp-2.6.0/src/pyotp/utils.py --- old/pyotp-2.4.0/src/pyotp/utils.py 2020-07-29 22:05:34.000000000 +0200 +++ new/pyotp-2.6.0/src/pyotp/utils.py 2021-01-30 00:12:46.000000000 +0100 @@ -1,18 +1,12 @@ -from typing import Dict, Optional, Union - import unicodedata from hmac import compare_digest -from urllib.parse import quote, urlencode +from typing import Dict, Optional, Union +from urllib.parse import quote, urlencode, urlparse -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: +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, + image: Optional[str] = None) -> str: """ Returns the provisioning URI for the OTP; works for either TOTP or HOTP. @@ -34,6 +28,7 @@ :param digits: the length of the OTP generated code. :param period: the number of seconds the OTP generator is set to expire every code. + :param image: optional logo image url :returns: provisioning uri """ # initial_count may be 0 as a valid param @@ -62,6 +57,11 @@ url_args['digits'] = digits if is_period_set: url_args['period'] = period + if image: + image_uri = urlparse(image) + if image_uri.scheme != 'https' or not image_uri.netloc or not image_uri.path: + raise ValueError('{} is not a valid url'.format(image_uri)) + url_args['image'] = image uri = base_uri.format(otp_type, label, urlencode(url_args).replace("+", "%20")) return uri diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyotp-2.4.0/src/pyotp.egg-info/PKG-INFO new/pyotp-2.6.0/src/pyotp.egg-info/PKG-INFO --- old/pyotp-2.4.0/src/pyotp.egg-info/PKG-INFO 2020-07-29 22:06:57.000000000 +0200 +++ new/pyotp-2.6.0/src/pyotp.egg-info/PKG-INFO 2021-02-04 20:47:46.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: pyotp -Version: 2.4.0 +Version: 2.6.0 Summary: Python One Time Password Library Home-page: https://github.com/pyotp/pyotp Author: PyOTP contributors @@ -138,7 +138,7 @@ ~~~~~ * `Project home page (GitHub) <https://github.com/pyauth/pyotp>`_ - * `Documentation (Read the Docs) <https://pyotp.readthedocs.io/en/latest/>`_ + * `Documentation <https://pyauth.github.io/pyotp/>`_ * `Package distribution (PyPI) <https://pypi.python.org/pypi/pyotp>`_ * `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>`_ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyotp-2.4.0/test.py new/pyotp-2.6.0/test.py --- old/pyotp-2.4.0/test.py 2020-07-29 22:05:34.000000000 +0200 +++ new/pyotp-2.6.0/test.py 2021-02-04 20:33:11.000000000 +0100 @@ -7,7 +7,7 @@ from urllib.parse import urlparse, parse_qsl sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) -import pyotp +import pyotp # noqa class HOTPExampleValuesFromTheRFC(unittest.TestCase): @@ -118,6 +118,11 @@ ).provisioning_uri() ) + code = pyotp.totp.TOTP("S46SQCPPTCNPROMHWYBDCTBZXV") + self.assertEqual(code.provisioning_uri(), "otpauth://totp/Secret?secret=S46SQCPPTCNPROMHWYBDCTBZXV") + code.verify("123456") + self.assertEqual(code.provisioning_uri(), "otpauth://totp/Secret?secret=S46SQCPPTCNPROMHWYBDCTBZXV") + def test_other_secret(self): hotp = pyotp.HOTP( 'N3OVNIBRERIO5OHGVCMDGS4V4RJ3AUZOUN34J6FRM4P6JIFCG3ZA') @@ -291,14 +296,14 @@ ) 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) + self.assertEqual(len(pyotp.random_base32()), 32) + self.assertEqual(len(pyotp.random_base32(length=34)), 34) + self.assertEqual(len(pyotp.random_hex()), 40) + self.assertEqual(len(pyotp.random_hex(length=42)), 42) + with self.assertRaises(ValueError): + pyotp.random_base32(length=31) + with self.assertRaises(ValueError): + pyotp.random_hex(length=39) class CompareDigestTest(unittest.TestCase): @@ -339,6 +344,7 @@ 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: @@ -359,7 +365,7 @@ 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)) + self.assertEqual('Digits may only be 6, 7, or 8', str(cm.exception)) with self.assertRaises(ValueError) as cm: pyotp.parse_uri('otpauth://totp/SomeIssuer:?issuer=AnotherIssuer') @@ -369,16 +375,59 @@ pyotp.parse_uri('otpauth://totp?algorithm=aes') self.assertEqual('Invalid value for algorithm, must be SHA1, SHA256 or SHA512', str(cm.exception)) + @unittest.skipIf(sys.version_info < (3, 6), "Skipping test that requires deterministic dict key enumeration") def test_algorithms(self): - otp = pyotp.parse_uri('otpauth://totp?algorithm=SHA1&secret=123456&algorithm=SHA1') + otp = pyotp.parse_uri('otpauth://totp?algorithm=SHA1&secret=GEZDGNBV&algorithm=SHA1') self.assertEqual(hashlib.sha1, otp.digest) + self.assertEqual(otp.at(0), '734055') + self.assertEqual(otp.at(30), '662488') + self.assertEqual(otp.at(60), '289363') + self.assertEqual(otp.provisioning_uri(), 'otpauth://totp/Secret?secret=GEZDGNBV') + self.assertEqual(otp.provisioning_uri(name='n', issuer_name='i'), 'otpauth://totp/i:n?secret=GEZDGNBV&issuer=i') - otp = pyotp.parse_uri('otpauth://totp?algorithm=SHA1&secret=123456&algorithm=SHA256') + otp = pyotp.parse_uri('otpauth://totp?algorithm=SHA1&secret=GEZDGNBV&algorithm=SHA1&period=60') + self.assertEqual(hashlib.sha1, otp.digest) + self.assertEqual(otp.at(30), '734055') + self.assertEqual(otp.at(60), '662488') + self.assertEqual(otp.provisioning_uri(name='n', issuer_name='i'), + 'otpauth://totp/i:n?secret=GEZDGNBV&issuer=i&period=60') + + otp = pyotp.parse_uri('otpauth://hotp?algorithm=SHA1&secret=GEZDGNBV&algorithm=SHA1') + self.assertEqual(hashlib.sha1, otp.digest) + self.assertEqual(otp.at(0), '734055') + self.assertEqual(otp.at(1), '662488') + self.assertEqual(otp.at(2), '289363') + self.assertEqual(otp.provisioning_uri(name='n', issuer_name='i'), + 'otpauth://hotp/i:n?secret=GEZDGNBV&issuer=i&counter=0') + + otp = pyotp.parse_uri('otpauth://hotp?algorithm=SHA1&secret=GEZDGNBV&algorithm=SHA1&counter=1') + self.assertEqual(hashlib.sha1, otp.digest) + self.assertEqual(otp.at(0), '662488') + self.assertEqual(otp.at(1), '289363') + self.assertEqual(otp.provisioning_uri(name='n', issuer_name='i'), + 'otpauth://hotp/i:n?secret=GEZDGNBV&issuer=i&counter=1') + + otp = pyotp.parse_uri('otpauth://totp?algorithm=SHA1&secret=GEZDGNBV&algorithm=SHA256') self.assertEqual(hashlib.sha256, otp.digest) + self.assertEqual(otp.at(0), '918961') + self.assertEqual(otp.at(9000), '934470') + + otp = pyotp.parse_uri('otpauth://totp?algorithm=SHA1&secret=GEZDGNBV&algorithm=SHA512') + self.assertEqual(hashlib.sha512, otp.digest) + self.assertEqual(otp.at(0), '816660') + self.assertEqual(otp.at(9000), '524153') + + self.assertEqual( + otp.provisioning_uri(name='n', issuer_name='i', image='https://test.net/test.png'), + 'otpauth://totp/i:n?secret=GEZDGNBV&issuer=i&algorithm=SHA512&image=https%3A%2F%2Ftest.net%2Ftest.png' + ) + with self.assertRaises(ValueError): + otp.provisioning_uri(name='n', issuer_name='i', image='nourl') - otp = pyotp.parse_uri('otpauth://totp?algorithm=SHA1&secret=123456&algorithm=SHA512') + otp = pyotp.parse_uri(otp.provisioning_uri(name='n', issuer_name='i', image='https://test.net/test.png')) self.assertEqual(hashlib.sha512, otp.digest) + class Timecop(object): """ Half-assed clone of timecop.rb, just enough to pass our tests.