Skip to content

Commit

Permalink
Use isort and black
Browse files Browse the repository at this point in the history
  • Loading branch information
kislyuk committed Sep 11, 2022
1 parent d8acd41 commit b9b11d3
Show file tree
Hide file tree
Showing 12 changed files with 347 additions and 352 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 3 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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 = {
Expand All @@ -31,6 +32,6 @@
"logo-text.html",
# "globaltoc.html",
"localtoc.html",
"searchbox.html"
"searchbox.html",
]
}
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[tool.black]
line-length = 120
[tool.isort]
profile = "black"
line_length = 120
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[flake8]
max-line-length=120
ignore: E401, W504
extend-ignore=E203
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
)
75 changes: 36 additions & 39 deletions src/pyotp/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand All @@ -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")
5 changes: 2 additions & 3 deletions src/pyotp/contrib/steam.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from typing import Optional
import hashlib
from typing import Optional

from ..totp import TOTP

Expand All @@ -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.
Expand Down
23 changes: 16 additions & 7 deletions src/pyotp/hotp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
35 changes: 22 additions & 13 deletions src/pyotp/otp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -22,25 +29,27 @@ 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

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
Expand All @@ -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"))
32 changes: 23 additions & 9 deletions src/pyotp/totp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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:
"""
Expand Down
Loading

0 comments on commit b9b11d3

Please sign in to comment.