From b9b11d36d2dbdbbbeec8ded47a0cc3c5bfc08e5a Mon Sep 17 00:00:00 2001 From: Andrey Kislyuk Date: Sun, 11 Sep 2022 11:52:28 -0700 Subject: [PATCH] Use isort and black --- .../workflows/{pythonpackage.yml => ci.yml} | 2 + docs/conf.py | 5 +- pyproject.toml | 5 + setup.cfg | 2 +- setup.py | 4 +- src/pyotp/__init__.py | 75 ++- src/pyotp/contrib/steam.py | 5 +- src/pyotp/hotp.py | 23 +- src/pyotp/otp.py | 35 +- src/pyotp/totp.py | 32 +- src/pyotp/utils.py | 49 +- test.py | 462 ++++++++---------- 12 files changed, 347 insertions(+), 352 deletions(-) rename .github/workflows/{pythonpackage.yml => ci.yml} (88%) create mode 100644 pyproject.toml diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/ci.yml similarity index 88% rename from .github/workflows/pythonpackage.yml rename to .github/workflows/ci.yml index 9e4d6ac..8a67646 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/ci.yml @@ -20,3 +20,5 @@ jobs: - name: Test run: make test - uses: codecov/codecov-action@v2 + - uses: isort/isort-action@v1.0.0 + - uses: psf/black@stable diff --git a/docs/conf.py b/docs/conf.py index 4ff44fb..aca5187 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -8,7 +8,7 @@ author = "PyOTP contributors" version = "" release = "" -language = None +language = "en" master_doc = "index" extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"] source_suffix = [".rst", ".md"] @@ -20,6 +20,7 @@ fh.write("Documentation for this project has moved to https://pyauth.github.io/pyotp") else: import guzzle_sphinx_theme + html_theme_path = guzzle_sphinx_theme.html_theme_path() html_theme = "guzzle_sphinx_theme" html_theme_options = { @@ -31,6 +32,6 @@ "logo-text.html", # "globaltoc.html", "localtoc.html", - "searchbox.html" + "searchbox.html", ] } diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..51e341e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,5 @@ +[tool.black] +line-length = 120 +[tool.isort] +profile = "black" +line_length = 120 diff --git a/setup.cfg b/setup.cfg index 6fd2051..a5726f4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,3 @@ [flake8] max-line-length=120 -ignore: E401, W504 +extend-ignore=E203 diff --git a/setup.py b/setup.py index 7d70bcd..73ab364 100755 --- a/setup.py +++ b/setup.py @@ -31,6 +31,6 @@ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", - "Topic :: Software Development :: Libraries :: Python Modules" - ] + "Topic :: Software Development :: Libraries :: Python Modules", + ], ) diff --git a/src/pyotp/__init__.py b/src/pyotp/__init__.py index 1e1c205..ba24c5b 100644 --- a/src/pyotp/__init__.py +++ b/src/pyotp/__init__.py @@ -1,7 +1,7 @@ import hashlib from re import split from typing import Any, Dict, Sequence -from urllib.parse import unquote, urlparse, parse_qsl +from urllib.parse import parse_qsl, unquote, urlparse from . import contrib # noqa:F401 from .compat import random @@ -10,20 +10,17 @@ from .totp import TOTP as TOTP -def random_base32(length: int = 32, chars: Sequence[str] = list('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')) -> str: +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) - for _ in range(length) - ) + return "".join(random.choice(chars) for _ in range(length)) -def random_hex(length: int = 40, chars: Sequence[str] = list('ABCDEF0123456789')) -> str: +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) @@ -49,53 +46,53 @@ def parse_uri(uri: str) -> OTP: # Parse with URLlib parsed_uri = urlparse(unquote(uri)) - if parsed_uri.scheme != 'otpauth': - raise ValueError('Not an otpauth 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) + accountinfo_parts = split(":|%3A", parsed_uri.path[1:], maxsplit=1) if len(accountinfo_parts) == 1: - otp_data['name'] = accountinfo_parts[0] + otp_data["name"] = accountinfo_parts[0] else: - otp_data['issuer'] = accountinfo_parts[0] - otp_data['name'] = accountinfo_parts[1] + 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': + 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 + 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': + raise ValueError("Invalid value for algorithm, must be SHA1, SHA256 or SHA512") + elif key == "digits": digits = int(value) 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'] = int(value) - elif key == 'counter': - otp_data['initial_count'] = int(value) - elif key != 'image': - raise ValueError('{} is not a valid parameter'.format(key)) + raise ValueError("Digits may only be 6, 7, or 8") + otp_data["digits"] = digits + elif key == "period": + otp_data["interval"] = int(value) + elif key == "counter": + otp_data["initial_count"] = int(value) + elif key != "image": + raise ValueError("{} is not a valid parameter".format(key)) if not secret: - raise ValueError('No secret found in URI') + raise ValueError("No secret found in URI") # Create objects - if parsed_uri.netloc == 'totp': + if parsed_uri.netloc == "totp": return TOTP(secret, **otp_data) - elif parsed_uri.netloc == 'hotp': + elif parsed_uri.netloc == "hotp": return HOTP(secret, **otp_data) - raise ValueError('Not a supported OTP type') + raise ValueError("Not a supported OTP type") diff --git a/src/pyotp/contrib/steam.py b/src/pyotp/contrib/steam.py index e229b0f..ee4d287 100644 --- a/src/pyotp/contrib/steam.py +++ b/src/pyotp/contrib/steam.py @@ -1,5 +1,5 @@ -from typing import Optional import hashlib +from typing import Optional from ..totp import TOTP @@ -12,8 +12,7 @@ class Steam(TOTP): Steam's custom TOTP. Subclass of `pyotp.totp.TOTP`. """ - def __init__(self, s: str, name: Optional[str] = None, - issuer: Optional[str] = None, interval: int = 30) -> None: + def __init__(self, s: str, name: Optional[str] = None, issuer: Optional[str] = None, interval: int = 30) -> None: """ :param s: secret in base32 format :param interval: the time interval in seconds for OTP. This defaults to 30. diff --git a/src/pyotp/hotp.py b/src/pyotp/hotp.py index b569c1e..f4cc5eb 100644 --- a/src/pyotp/hotp.py +++ b/src/pyotp/hotp.py @@ -9,8 +9,16 @@ class HOTP(OTP): """ Handler for HMAC-based OTP counters. """ - 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: + + 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 @@ -41,11 +49,12 @@ def verify(self, otp: str, counter: int) -> bool: return utils.strings_equal(str(otp), str(self.at(counter))) def provisioning_uri( - self, - name: Optional[str] = None, - initial_count: Optional[int] = None, - issuer_name: Optional[str] = None, - image: Optional[str] = None) -> str: + self, + name: Optional[str] = None, + initial_count: Optional[int] = 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 diff --git a/src/pyotp/otp.py b/src/pyotp/otp.py index 9a33d3d..1f0b654 100644 --- a/src/pyotp/otp.py +++ b/src/pyotp/otp.py @@ -8,12 +8,19 @@ 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: + + 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 - self.name = name or 'Secret' + self.name = name or "Secret" self.issuer = issuer def generate_otp(self, input: int) -> str: @@ -22,17 +29,19 @@ def generate_otp(self, input: int) -> str: Usually either the counter, or the computed integer based on the Unix timestamp """ if input < 0: - raise ValueError('input must be positive integer') + 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) + 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 + str_code = "0" + str_code return str_code @@ -40,7 +49,7 @@ def byte_secret(self) -> bytes: secret = self.secret missing_padding = len(secret) % 8 if missing_padding != 0: - secret += '=' * (8 - missing_padding) + secret += "=" * (8 - missing_padding) return base64.b32decode(secret, casefold=True) @staticmethod @@ -57,4 +66,4 @@ def int_to_bytestring(i: int, padding: int = 8) -> bytes: # 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')) + return bytes(bytearray(reversed(result)).rjust(padding, b"\0")) diff --git a/src/pyotp/totp.py b/src/pyotp/totp.py index 75fdf36..fada05c 100644 --- a/src/pyotp/totp.py +++ b/src/pyotp/totp.py @@ -2,7 +2,7 @@ import datetime import hashlib import time -from typing import Any, Union, Optional +from typing import Any, Optional, Union from . import utils from .otp import OTP @@ -12,8 +12,16 @@ class TOTP(OTP): """ Handler for time-based OTP counters. """ - def __init__(self, s: str, digits: int = 6, digest: Any = hashlib.sha1, name: Optional[str] = None, - issuer: Optional[str] = None, interval: int = 30) -> 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 s: secret in base32 format :param interval: the time interval in seconds for OTP. This defaults to 30. @@ -70,8 +78,9 @@ def verify(self, otp: str, for_time: Optional[datetime.datetime] = None, valid_w return utils.strings_equal(str(otp), str(self.at(for_time))) - def provisioning_uri(self, name: Optional[str] = None, issuer_name: Optional[str] = None, - image: 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 @@ -82,10 +91,15 @@ def provisioning_uri(self, name: Optional[str] = None, issuer_name: Optional[str https://github.com/google/google-authenticator/wiki/Key-Uri-Format """ - 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, image=image) + 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, + image=image, + ) def timecode(self, for_time: datetime.datetime) -> int: """ diff --git a/src/pyotp/utils.py b/src/pyotp/utils.py index ad2fe30..0ec2593 100644 --- a/src/pyotp/utils.py +++ b/src/pyotp/utils.py @@ -4,9 +4,16 @@ 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, - image: Optional[str] = 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. @@ -32,36 +39,36 @@ def build_uri(secret: str, name: str, initial_count: Optional[int] = None, issue :returns: provisioning uri """ # initial_count may be 0 as a valid param - is_initial_count_present = (initial_count is not None) + 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) + 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}' + otp_type = "hotp" if is_initial_count_present else "totp" + base_uri = "otpauth://{0}/{1}?{2}" - url_args = {'secret': secret} # type: Dict[str, Union[None, int, str]] + url_args = {"secret": secret} # type: Dict[str, Union[None, int, str]] label = quote(name) if issuer is not None: - label = quote(issuer) + ':' + label - url_args['issuer'] = issuer + label = quote(issuer) + ":" + label + url_args["issuer"] = issuer if is_initial_count_present: - url_args['counter'] = initial_count + url_args["counter"] = initial_count if is_algorithm_set: - url_args['algorithm'] = algorithm.upper() # type: ignore + url_args["algorithm"] = algorithm.upper() # type: ignore if is_digits_set: - url_args['digits'] = digits + url_args["digits"] = digits if is_period_set: - url_args['period'] = period + 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 + 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 @@ -76,6 +83,6 @@ def strings_equal(s1: str, s2: str) -> bool: still reveal to a timing attack whether the strings are the same length. """ - s1 = unicodedata.normalize('NFKC', s1) - s2 = unicodedata.normalize('NFKC', s2) + s1 = unicodedata.normalize("NFKC", s1) + s2 = unicodedata.normalize("NFKC", s2) return compare_digest(s1.encode("utf-8"), s2.encode("utf-8")) diff --git a/test.py b/test.py index befc5b0..a1e40d0 100755 --- a/test.py +++ b/test.py @@ -1,12 +1,15 @@ #!/usr/bin/env python -# coding: utf-8 -import base64, datetime, hashlib, os, sys, unittest +import base64 +import datetime +import hashlib +import os +import sys +import unittest +from urllib.parse import parse_qsl, urlparse from warnings import warn -from urllib.parse import urlparse, parse_qsl - -sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src")) import pyotp # noqa @@ -14,109 +17,85 @@ 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') + 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') + 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)) + 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', name='mark@percival') + hotp = pyotp.HOTP("wrn3pqx5uqxqvnqr", name="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() - ) + 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()) - hotp = pyotp.HOTP('wrn3pqx5uqxqvnqr', name='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'}) - self.assertEqual( - hotp.provisioning_uri(), - pyotp.parse_uri( - hotp.provisioning_uri() - ).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"}) + self.assertEqual(hotp.provisioning_uri(), pyotp.parse_uri(hotp.provisioning_uri()).provisioning_uri()) - hotp = pyotp.HOTP('wrn3pqx5uqxqvnqr', name='mark@percival', - issuer='FooCorp!') + 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(url.scheme, "otpauth") + self.assertEqual(url.netloc, "hotp") + self.assertEqual(url.path, "/FooCorp%21:mark%40percival") self.assertEqual( - hotp.provisioning_uri(), - pyotp.parse_uri( - hotp.provisioning_uri() - ).provisioning_uri() + 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, - name='baco@peperina', issuer='FooCorp') + key = "c7uxuqhgflpw7oruedmglbrk7u6242vb" + 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') - self.assertEqual(dict(parse_qsl(url.query)), - {'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb', - 'counter': '0', 'issuer': 'FooCorp', - 'digits': '8', 'algorithm': 'SHA256'}) + self.assertEqual(url.scheme, "otpauth") + self.assertEqual(url.netloc, "hotp") + self.assertEqual(url.path, "/FooCorp:baco%40peperina") self.assertEqual( - hotp.provisioning_uri(), - pyotp.parse_uri( - hotp.provisioning_uri() - ).provisioning_uri() + dict(parse_qsl(url.query)), + { + "secret": "c7uxuqhgflpw7oruedmglbrk7u6242vb", + "counter": "0", + "issuer": "FooCorp", + "digits": "8", + "algorithm": "SHA256", + }, ) + 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) + 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') - self.assertEqual(dict(parse_qsl(url.query)), - {'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb', - 'counter': '10', 'issuer': 'Foo Corp', - 'digits': '8'}) + self.assertEqual(url.scheme, "otpauth") + self.assertEqual(url.netloc, "hotp") + self.assertEqual(url.path, "/Foo%20Corp:baco%40peperina") self.assertEqual( - hotp.provisioning_uri(), - pyotp.parse_uri( - hotp.provisioning_uri() - ).provisioning_uri() + dict(parse_qsl(url.query)), + {"secret": "c7uxuqhgflpw7oruedmglbrk7u6242vb", "counter": "10", "issuer": "Foo Corp", "digits": "8"}, ) + self.assertEqual(hotp.provisioning_uri(), pyotp.parse_uri(hotp.provisioning_uri()).provisioning_uri()) code = pyotp.totp.TOTP("S46SQCPPTCNPROMHWYBDCTBZXV") self.assertEqual(code.provisioning_uri(), "otpauth://totp/Secret?secret=S46SQCPPTCNPROMHWYBDCTBZXV") @@ -124,44 +103,39 @@ def test_provisioning_uri(self): self.assertEqual(code.provisioning_uri(), "otpauth://totp/Secret?secret=S46SQCPPTCNPROMHWYBDCTBZXV") 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') + 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.sha1, b"12345678901234567890"): ( + (59, "94287082"), + (1111111109, "07081804"), + (1111111111, "14050471"), + (1234567890, "89005924"), + (2000000000, "69279037"), + (20000000000, "65353130"), ), - - (hashlib.sha256, b'12345678901234567890123456789012'): ( + (hashlib.sha256, b"12345678901234567890123456789012"): ( (59, 46119246), - (1111111109, '68084774'), - (1111111111, '67062674'), - (1234567890, '91819424'), - (2000000000, '90698825'), - (20000000000, '77737706'), + (1111111109, "68084774"), + (1111111111, "67062674"), + (1234567890, "91819424"), + (2000000000, "90698825"), + (20000000000, "77737706"), ), - - (hashlib.sha512, - b'1234567890123456789012345678901234567890123456789012345678901234'): - ( + (hashlib.sha512, b"1234567890123456789012345678901234567890123456789012345678901234"): ( (59, 90693936), - (1111111109, '25091201'), - (1111111111, '99943326'), - (1234567890, '93441116'), - (2000000000, '38618901'), - (20000000000, '47863826'), + (1111111109, "25091201"), + (1111111111, "99943326"), + (1234567890, "93441116"), + (2000000000, "38618901"), + (20000000000, "47863826"), ), } @@ -170,8 +144,10 @@ def test_match_rfc(self): 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)") + 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)" @@ -179,121 +155,95 @@ def test_match_rfc(self): 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') + 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') + totp = pyotp.TOTP("wrn3pqx5uqxqvnqr") with Timecop(1297553958): - self.assertEqual(totp.now(), '102705') + self.assertEqual(totp.now(), "102705") def test_validate_totp(self): - totp = pyotp.TOTP('wrn3pqx5uqxqvnqr') + totp = pyotp.TOTP("wrn3pqx5uqxqvnqr") with Timecop(1297553958): - self.assertTrue(totp.verify('102705')) - self.assertTrue(totp.verify('102705')) + self.assertTrue(totp.verify("102705")) + self.assertTrue(totp.verify("102705")) with Timecop(1297553958 + 30): - self.assertFalse(totp.verify('102705')) + self.assertFalse(totp.verify("102705")) def test_input_before_epoch(self): - totp = pyotp.TOTP('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ') + 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') + 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') + totp = pyotp.TOTP("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ") with Timecop(1111111111): - self.assertTrue(totp.verify('050471')) + self.assertTrue(totp.verify("050471")) with Timecop(1297553958 + 30): - self.assertFalse(totp.verify('050471')) + self.assertFalse(totp.verify("050471")) def test_provisioning_uri(self): - totp = pyotp.TOTP('wrn3pqx5uqxqvnqr', name='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'}) - self.assertEqual( - totp.provisioning_uri(), - pyotp.parse_uri( - totp.provisioning_uri() - ).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"}) + self.assertEqual(totp.provisioning_uri(), pyotp.parse_uri(totp.provisioning_uri()).provisioning_uri()) - totp = pyotp.TOTP('wrn3pqx5uqxqvnqr', name='mark@percival', - issuer='FooCorp!') + 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, - name='baco@peperina', issuer='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!"}) + 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, 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') - self.assertEqual(dict(parse_qsl(url.query)), - {'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb', - 'issuer': 'FooCorp', - 'digits': '8', 'period': '60', - 'algorithm': 'SHA256'}) + self.assertEqual(url.scheme, "otpauth") + self.assertEqual(url.netloc, "totp") + self.assertEqual(url.path, "/FooCorp:baco%40peperina") self.assertEqual( - totp.provisioning_uri(), - pyotp.parse_uri( - totp.provisioning_uri() - ).provisioning_uri() + dict(parse_qsl(url.query)), + { + "secret": "c7uxuqhgflpw7oruedmglbrk7u6242vb", + "issuer": "FooCorp", + "digits": "8", + "period": "60", + "algorithm": "SHA256", + }, ) + 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') + 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') - self.assertEqual(dict(parse_qsl(url.query)), - {'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb', - 'issuer': 'FooCorp', - 'digits': '8', 'period': '60'}) + self.assertEqual(url.scheme, "otpauth") + self.assertEqual(url.netloc, "totp") + self.assertEqual(url.path, "/FooCorp:baco%40peperina") self.assertEqual( - totp.provisioning_uri(), - pyotp.parse_uri( - totp.provisioning_uri() - ).provisioning_uri() + dict(parse_qsl(url.query)), + {"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, name='baco@peperina', issuer='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') - self.assertEqual(dict(parse_qsl(url.query)), - {'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb', - 'issuer': 'FooCorp', - 'digits': '8'}) + self.assertEqual(url.scheme, "otpauth") + self.assertEqual(url.netloc, "totp") + self.assertEqual(url.path, "/FooCorp:baco%40peperina") self.assertEqual( - totp.provisioning_uri(), - pyotp.parse_uri( - totp.provisioning_uri() - ).provisioning_uri() + dict(parse_qsl(url.query)), + {"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()), 32) @@ -315,7 +265,6 @@ def test_match_examples(self): self.assertEqual(steam.at(60), "M4HQB") self.assertEqual(steam.at(90), "DTVB3") - steam = pyotp.contrib.Steam("FMXNK4QEGKVPULRTADY6JIDK5VHUBGZW") self.assertEqual(steam.at(0), "C5V56") @@ -326,25 +275,25 @@ def test_match_examples(self): def test_verify(self): steam = pyotp.contrib.Steam("BASE32SECRET3232") with Timecop(1662883100): - self.assertTrue(steam.verify('N3G63')) + self.assertTrue(steam.verify("N3G63")) with Timecop(1662883100 + 30): - self.assertFalse(steam.verify('N3G63')) + self.assertFalse(steam.verify("N3G63")) with Timecop(946681223): - self.assertTrue(steam.verify('7VP3X')) + self.assertTrue(steam.verify("7VP3X")) with Timecop(946681223 + 30): - self.assertFalse(steam.verify('7VP3X')) + self.assertFalse(steam.verify("7VP3X")) steam = pyotp.contrib.Steam("FMXNK4QEGKVPULRTADY6JIDK5VHUBGZW") with Timecop(1662884261): - self.assertTrue(steam.verify('V6WKJ')) + self.assertTrue(steam.verify("V6WKJ")) with Timecop(1662884261 + 30): - self.assertFalse(steam.verify('V6WKJ')) + self.assertFalse(steam.verify("V6WKJ")) with Timecop(946681223): - self.assertTrue(steam.verify('4MK54')) + self.assertTrue(steam.verify("4MK54")) with Timecop(946681223 + 30): - self.assertFalse(steam.verify('4MK54')) + self.assertFalse(steam.verify("4MK54")) class CompareDigestTest(unittest.TestCase): @@ -389,83 +338,86 @@ def test_valid_window(self): 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)) + 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)) + 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)) + 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)) + 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, 7, or 8', str(cm.exception)) + pyotp.parse_uri("otpauth://totp?digits=-1") + 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') - self.assertEqual('If issuer is specified in both label and parameters, it should be equal.', str(cm.exception)) + 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)) + 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=GEZDGNBV&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') + 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=GEZDGNBV&algorithm=SHA1&period=60') + 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') + 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') + 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') + 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') + 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') + 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') + 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') + self.assertEqual(otp.at(0), "918961") + self.assertEqual(otp.at(9000), "934470") - otp = pyotp.parse_uri('otpauth://totp?algorithm=SHA1&secret=GEZDGNBV&algorithm=SHA512') + 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.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' + 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.provisioning_uri(name="n", issuer_name="i", image="nourl") - otp = pyotp.parse_uri(otp.provisioning_uri(name='n', issuer_name='i', image='https://test.net/test.png')) + otp = pyotp.parse_uri(otp.provisioning_uri(name="n", issuer_name="i", image="https://test.net/test.png")) self.assertEqual(hashlib.sha512, otp.digest) @@ -494,5 +446,5 @@ def now(cls, **kwargs): return FrozenDateTime -if __name__ == '__main__': +if __name__ == "__main__": unittest.main()