pyauth / pyotp

@@ -1,7 +1,7 @@
Loading
1 1
import hashlib
2 2
from re import split
3 3
from typing import Any, Dict, Sequence
4 -
from urllib.parse import unquote, urlparse, parse_qsl
4 +
from urllib.parse import parse_qsl, unquote, urlparse
5 5
6 6
from . import contrib  # noqa:F401
7 7
from .compat import random
@@ -10,20 +10,17 @@
Loading
10 10
from .totp import TOTP as TOTP
11 11
12 12
13 -
def random_base32(length: int = 32, chars: Sequence[str] = list('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')) -> str:
13 +
def random_base32(length: int = 32, chars: Sequence[str] = list("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567")) -> str:
14 14
    # Note: the otpauth scheme DOES NOT use base32 padding for secret lengths not divisible by 8.
15 15
    # Some third-party tools have bugs when dealing with such secrets.
16 16
    # We might consider warning the user when generating a secret of length not divisible by 8.
17 17
    if length < 32:
18 18
        raise ValueError("Secrets should be at least 160 bits")
19 19
20 -
    return ''.join(
21 -
        random.choice(chars)
22 -
        for _ in range(length)
23 -
    )
20 +
    return "".join(random.choice(chars) for _ in range(length))
24 21
25 22
26 -
def random_hex(length: int = 40, chars: Sequence[str] = list('ABCDEF0123456789')) -> str:
23 +
def random_hex(length: int = 40, chars: Sequence[str] = list("ABCDEF0123456789")) -> str:
27 24
    if length < 40:
28 25
        raise ValueError("Secrets should be at least 160 bits")
29 26
    return random_base32(length=length, chars=chars)
@@ -49,53 +46,53 @@
Loading
49 46
    # Parse with URLlib
50 47
    parsed_uri = urlparse(unquote(uri))
51 48
52 -
    if parsed_uri.scheme != 'otpauth':
53 -
        raise ValueError('Not an otpauth URI')
49 +
    if parsed_uri.scheme != "otpauth":
50 +
        raise ValueError("Not an otpauth URI")
54 51
55 52
    # Parse issuer/accountname info
56 -
    accountinfo_parts = split(':|%3A', parsed_uri.path[1:], maxsplit=1)
53 +
    accountinfo_parts = split(":|%3A", parsed_uri.path[1:], maxsplit=1)
57 54
    if len(accountinfo_parts) == 1:
58 -
        otp_data['name'] = accountinfo_parts[0]
55 +
        otp_data["name"] = accountinfo_parts[0]
59 56
    else:
60 -
        otp_data['issuer'] = accountinfo_parts[0]
61 -
        otp_data['name'] = accountinfo_parts[1]
57 +
        otp_data["issuer"] = accountinfo_parts[0]
58 +
        otp_data["name"] = accountinfo_parts[1]
62 59
63 60
    # Parse values
64 61
    for key, value in parse_qsl(parsed_uri.query):
65 -
        if key == 'secret':
62 +
        if key == "secret":
66 63
            secret = value
67 -
        elif key == 'issuer':
68 -
            if 'issuer' in otp_data and otp_data['issuer'] is not None and otp_data['issuer'] != value:
69 -
                raise ValueError('If issuer is specified in both label and parameters, it should be equal.')
70 -
            otp_data['issuer'] = value
71 -
        elif key == 'algorithm':
72 -
            if value == 'SHA1':
73 -
                otp_data['digest'] = hashlib.sha1
74 -
            elif value == 'SHA256':
75 -
                otp_data['digest'] = hashlib.sha256
76 -
            elif value == 'SHA512':
77 -
                otp_data['digest'] = hashlib.sha512
64 +
        elif key == "issuer":
65 +
            if "issuer" in otp_data and otp_data["issuer"] is not None and otp_data["issuer"] != value:
66 +
                raise ValueError("If issuer is specified in both label and parameters, it should be equal.")
67 +
            otp_data["issuer"] = value
68 +
        elif key == "algorithm":
69 +
            if value == "SHA1":
70 +
                otp_data["digest"] = hashlib.sha1
71 +
            elif value == "SHA256":
72 +
                otp_data["digest"] = hashlib.sha256
73 +
            elif value == "SHA512":
74 +
                otp_data["digest"] = hashlib.sha512
78 75
            else:
79 -
                raise ValueError('Invalid value for algorithm, must be SHA1, SHA256 or SHA512')
80 -
        elif key == 'digits':
76 +
                raise ValueError("Invalid value for algorithm, must be SHA1, SHA256 or SHA512")
77 +
        elif key == "digits":
81 78
            digits = int(value)
82 79
            if digits not in [6, 7, 8]:
83 -
                raise ValueError('Digits may only be 6, 7, or 8')
84 -
            otp_data['digits'] = digits
85 -
        elif key == 'period':
86 -
            otp_data['interval'] = int(value)
87 -
        elif key == 'counter':
88 -
            otp_data['initial_count'] = int(value)
89 -
        elif key != 'image':
90 -
            raise ValueError('{} is not a valid parameter'.format(key))
80 +
                raise ValueError("Digits may only be 6, 7, or 8")
81 +
            otp_data["digits"] = digits
82 +
        elif key == "period":
83 +
            otp_data["interval"] = int(value)
84 +
        elif key == "counter":
85 +
            otp_data["initial_count"] = int(value)
86 +
        elif key != "image":
87 +
            raise ValueError("{} is not a valid parameter".format(key))
91 88
92 89
    if not secret:
93 -
        raise ValueError('No secret found in URI')
90 +
        raise ValueError("No secret found in URI")
94 91
95 92
    # Create objects
96 -
    if parsed_uri.netloc == 'totp':
93 +
    if parsed_uri.netloc == "totp":
97 94
        return TOTP(secret, **otp_data)
98 -
    elif parsed_uri.netloc == 'hotp':
95 +
    elif parsed_uri.netloc == "hotp":
99 96
        return HOTP(secret, **otp_data)
100 97
101 -
    raise ValueError('Not a supported OTP type')
98 +
    raise ValueError("Not a supported OTP type")

@@ -8,12 +8,19 @@
Loading
8 8
    """
9 9
    Base class for OTP handlers.
10 10
    """
11 -
    def __init__(self, s: str, digits: int = 6, digest: Any = hashlib.sha1, name: Optional[str] = None,
12 -
                 issuer: Optional[str] = None) -> None:
11 +
12 +
    def __init__(
13 +
        self,
14 +
        s: str,
15 +
        digits: int = 6,
16 +
        digest: Any = hashlib.sha1,
17 +
        name: Optional[str] = None,
18 +
        issuer: Optional[str] = None,
19 +
    ) -> None:
13 20
        self.digits = digits
14 21
        self.digest = digest
15 22
        self.secret = s
16 -
        self.name = name or 'Secret'
23 +
        self.name = name or "Secret"
17 24
        self.issuer = issuer
18 25
19 26
    def generate_otp(self, input: int) -> str:
@@ -22,17 +29,19 @@
Loading
22 29
            Usually either the counter, or the computed integer based on the Unix timestamp
23 30
        """
24 31
        if input < 0:
25 -
            raise ValueError('input must be positive integer')
32 +
            raise ValueError("input must be positive integer")
26 33
        hasher = hmac.new(self.byte_secret(), self.int_to_bytestring(input), self.digest)
27 34
        hmac_hash = bytearray(hasher.digest())
28 -
        offset = hmac_hash[-1] & 0xf
29 -
        code = ((hmac_hash[offset] & 0x7f) << 24 |
30 -
                (hmac_hash[offset + 1] & 0xff) << 16 |
31 -
                (hmac_hash[offset + 2] & 0xff) << 8 |
32 -
                (hmac_hash[offset + 3] & 0xff))
33 -
        str_code = str(code % 10 ** self.digits)
35 +
        offset = hmac_hash[-1] & 0xF
36 +
        code = (
37 +
            (hmac_hash[offset] & 0x7F) << 24
38 +
            | (hmac_hash[offset + 1] & 0xFF) << 16
39 +
            | (hmac_hash[offset + 2] & 0xFF) << 8
40 +
            | (hmac_hash[offset + 3] & 0xFF)
41 +
        )
42 +
        str_code = str(code % 10**self.digits)
34 43
        while len(str_code) < self.digits:
35 -
            str_code = '0' + str_code
44 +
            str_code = "0" + str_code
36 45
37 46
        return str_code
38 47
@@ -40,7 +49,7 @@
Loading
40 49
        secret = self.secret
41 50
        missing_padding = len(secret) % 8
42 51
        if missing_padding != 0:
43 -
            secret += '=' * (8 - missing_padding)
52 +
            secret += "=" * (8 - missing_padding)
44 53
        return base64.b32decode(secret, casefold=True)
45 54
46 55
    @staticmethod
@@ -57,4 +66,4 @@
Loading
57 66
        # It's necessary to convert the final result from bytearray to bytes
58 67
        # because the hmac functions in python 2.6 and 3.3 don't work with
59 68
        # bytearray
60 -
        return bytes(bytearray(reversed(result)).rjust(padding, b'\0'))
69 +
        return bytes(bytearray(reversed(result)).rjust(padding, b"\0"))

@@ -4,9 +4,16 @@
Loading
4 4
from urllib.parse import quote, urlencode, urlparse
5 5
6 6
7 -
def build_uri(secret: str, name: str, initial_count: Optional[int] = None, issuer: Optional[str] = None,
8 -
              algorithm: Optional[str] = None, digits: Optional[int] = None, period: Optional[int] = None,
9 -
              image: Optional[str] = None) -> str:
7 +
def build_uri(
8 +
    secret: str,
9 +
    name: str,
10 +
    initial_count: Optional[int] = None,
11 +
    issuer: Optional[str] = None,
12 +
    algorithm: Optional[str] = None,
13 +
    digits: Optional[int] = None,
14 +
    period: Optional[int] = None,
15 +
    image: Optional[str] = None,
16 +
) -> str:
10 17
    """
11 18
    Returns the provisioning URI for the OTP; works for either TOTP or HOTP.
12 19
@@ -32,36 +39,36 @@
Loading
32 39
    :returns: provisioning uri
33 40
    """
34 41
    # initial_count may be 0 as a valid param
35 -
    is_initial_count_present = (initial_count is not None)
42 +
    is_initial_count_present = initial_count is not None
36 43
37 44
    # Handling values different from defaults
38 -
    is_algorithm_set = (algorithm is not None and algorithm != 'sha1')
39 -
    is_digits_set = (digits is not None and digits != 6)
40 -
    is_period_set = (period is not None and period != 30)
45 +
    is_algorithm_set = algorithm is not None and algorithm != "sha1"
46 +
    is_digits_set = digits is not None and digits != 6
47 +
    is_period_set = period is not None and period != 30
41 48
42 -
    otp_type = 'hotp' if is_initial_count_present else 'totp'
43 -
    base_uri = 'otpauth://{0}/{1}?{2}'
49 +
    otp_type = "hotp" if is_initial_count_present else "totp"
50 +
    base_uri = "otpauth://{0}/{1}?{2}"
44 51
45 -
    url_args = {'secret': secret}  # type: Dict[str, Union[None, int, str]]
52 +
    url_args = {"secret": secret}  # type: Dict[str, Union[None, int, str]]
46 53
47 54
    label = quote(name)
48 55
    if issuer is not None:
49 -
        label = quote(issuer) + ':' + label
50 -
        url_args['issuer'] = issuer
56 +
        label = quote(issuer) + ":" + label
57 +
        url_args["issuer"] = issuer
51 58
52 59
    if is_initial_count_present:
53 -
        url_args['counter'] = initial_count
60 +
        url_args["counter"] = initial_count
54 61
    if is_algorithm_set:
55 -
        url_args['algorithm'] = algorithm.upper()  # type: ignore
62 +
        url_args["algorithm"] = algorithm.upper()  # type: ignore
56 63
    if is_digits_set:
57 -
        url_args['digits'] = digits
64 +
        url_args["digits"] = digits
58 65
    if is_period_set:
59 -
        url_args['period'] = period
66 +
        url_args["period"] = period
60 67
    if image:
61 68
        image_uri = urlparse(image)
62 -
        if image_uri.scheme != 'https' or not image_uri.netloc or not image_uri.path:
63 -
            raise ValueError('{} is not a valid url'.format(image_uri))
64 -
        url_args['image'] = image
69 +
        if image_uri.scheme != "https" or not image_uri.netloc or not image_uri.path:
70 +
            raise ValueError("{} is not a valid url".format(image_uri))
71 +
        url_args["image"] = image
65 72
66 73
    uri = base_uri.format(otp_type, label, urlencode(url_args).replace("+", "%20"))
67 74
    return uri
@@ -76,6 +83,6 @@
Loading
76 83
    still reveal to a timing attack whether the strings are the same
77 84
    length.
78 85
    """
79 -
    s1 = unicodedata.normalize('NFKC', s1)
80 -
    s2 = unicodedata.normalize('NFKC', s2)
86 +
    s1 = unicodedata.normalize("NFKC", s1)
87 +
    s2 = unicodedata.normalize("NFKC", s2)
81 88
    return compare_digest(s1.encode("utf-8"), s2.encode("utf-8"))

@@ -9,8 +9,16 @@
Loading
9 9
    """
10 10
    Handler for HMAC-based OTP counters.
11 11
    """
12 -
    def __init__(self, s: str, digits: int = 6, digest: Any = hashlib.sha1, name: Optional[str] = None,
13 -
                 issuer: Optional[str] = None, initial_count: int = 0) -> None:
12 +
13 +
    def __init__(
14 +
        self,
15 +
        s: str,
16 +
        digits: int = 6,
17 +
        digest: Any = hashlib.sha1,
18 +
        name: Optional[str] = None,
19 +
        issuer: Optional[str] = None,
20 +
        initial_count: int = 0,
21 +
    ) -> None:
14 22
        """
15 23
        :param s: secret in base32 format
16 24
        :param initial_count: starting HMAC counter value, defaults to 0
@@ -41,11 +49,12 @@
Loading
41 49
        return utils.strings_equal(str(otp), str(self.at(counter)))
42 50
43 51
    def provisioning_uri(
44 -
            self,
45 -
            name: Optional[str] = None,
46 -
            initial_count: Optional[int] = None,
47 -
            issuer_name: Optional[str] = None,
48 -
            image: Optional[str] = None) -> str:
52 +
        self,
53 +
        name: Optional[str] = None,
54 +
        initial_count: Optional[int] = None,
55 +
        issuer_name: Optional[str] = None,
56 +
        image: Optional[str] = None,
57 +
    ) -> str:
49 58
        """
50 59
        Returns the provisioning URI for the OTP.  This can then be
51 60
        encoded in a QR Code and used to provision an OTP app like

@@ -2,7 +2,7 @@
Loading
2 2
import datetime
3 3
import hashlib
4 4
import time
5 -
from typing import Any, Union, Optional
5 +
from typing import Any, Optional, Union
6 6
7 7
from . import utils
8 8
from .otp import OTP
@@ -12,8 +12,16 @@
Loading
12 12
    """
13 13
    Handler for time-based OTP counters.
14 14
    """
15 -
    def __init__(self, s: str, digits: int = 6, digest: Any = hashlib.sha1, name: Optional[str] = None,
16 -
                 issuer: Optional[str] = None, interval: int = 30) -> None:
15 +
16 +
    def __init__(
17 +
        self,
18 +
        s: str,
19 +
        digits: int = 6,
20 +
        digest: Any = hashlib.sha1,
21 +
        name: Optional[str] = None,
22 +
        issuer: Optional[str] = None,
23 +
        interval: int = 30,
24 +
    ) -> None:
17 25
        """
18 26
        :param s: secret in base32 format
19 27
        :param interval: the time interval in seconds for OTP. This defaults to 30.
@@ -70,8 +78,9 @@
Loading
70 78
71 79
        return utils.strings_equal(str(otp), str(self.at(for_time)))
72 80
73 -
    def provisioning_uri(self, name: Optional[str] = None, issuer_name: Optional[str] = None,
74 -
                         image: Optional[str] = None) -> str:
81 +
    def provisioning_uri(
82 +
        self, name: Optional[str] = None, issuer_name: Optional[str] = None, image: Optional[str] = None
83 +
    ) -> str:
75 84
76 85
        """
77 86
        Returns the provisioning URI for the OTP.  This can then be
@@ -82,10 +91,15 @@
Loading
82 91
            https://github.com/google/google-authenticator/wiki/Key-Uri-Format
83 92
84 93
        """
85 -
        return utils.build_uri(self.secret, name if name else self.name,
86 -
                               issuer=issuer_name if issuer_name else self.issuer,
87 -
                               algorithm=self.digest().name,
88 -
                               digits=self.digits, period=self.interval, image=image)
94 +
        return utils.build_uri(
95 +
            self.secret,
96 +
            name if name else self.name,
97 +
            issuer=issuer_name if issuer_name else self.issuer,
98 +
            algorithm=self.digest().name,
99 +
            digits=self.digits,
100 +
            period=self.interval,
101 +
            image=image,
102 +
        )
89 103
90 104
    def timecode(self, for_time: datetime.datetime) -> int:
91 105
        """

@@ -1,5 +1,5 @@
Loading
1 -
from typing import Optional
2 1
import hashlib
2 +
from typing import Optional
3 3
4 4
from ..totp import TOTP
5 5
@@ -12,8 +12,7 @@
Loading
12 12
    Steam's custom TOTP. Subclass of `pyotp.totp.TOTP`.
13 13
    """
14 14
15 -
    def __init__(self, s: str, name: Optional[str] = None,
16 -
                 issuer: Optional[str] = None, interval: int = 30) -> None:
15 +
    def __init__(self, s: str, name: Optional[str] = None, issuer: Optional[str] = None, interval: int = 30) -> None:
17 16
        """
18 17
        :param s: secret in base32 format
19 18
        :param interval: the time interval in seconds for OTP. This defaults to 30.
Files Coverage
src/pyotp 98.03%
Project Totals (8 files) 98.03%
Sunburst
The inner-most circle is the entire project, moving away from the center are folders then, finally, a single file. The size and color of each slice is representing the number of statements and the coverage, respectively.
Icicle
The top section represents the entire project. Proceeding with folders and finally individual files. The size and color of each slice is representing the number of statements and the coverage, respectively.
Grid
Each block represents a single file in the project. The size and color of each block is represented by the number of statements and the coverage, respectively.
Loading