PyOTP 2.2.8__py2.py3-none-any.whl → 2.4.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyotp/__init__.py +94 -15
- pyotp/compat.py +6 -9
- pyotp/hotp.py +20 -16
- pyotp/otp.py +17 -10
- pyotp/py.typed +0 -0
- pyotp/totp.py +16 -24
- pyotp/utils.py +21 -49
- {pyotp-2.2.8.dist-info → pyotp-2.4.0.dist-info}/METADATA +27 -14
- pyotp-2.4.0.dist-info/RECORD +12 -0
- {pyotp-2.2.8.dist-info → pyotp-2.4.0.dist-info}/WHEEL +1 -1
- pyotp-2.2.8.dist-info/RECORD +0 -11
- {pyotp-2.2.8.dist-info → pyotp-2.4.0.dist-info}/LICENSE +0 -0
- {pyotp-2.2.8.dist-info → pyotp-2.4.0.dist-info}/top_level.txt +0 -0
pyotp/__init__.py
CHANGED
|
@@ -1,23 +1,102 @@
|
|
|
1
|
-
from
|
|
2
|
-
print_function, unicode_literals)
|
|
1
|
+
from typing import Any, Dict, Sequence
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
from
|
|
6
|
-
from
|
|
7
|
-
from . import utils # noqa
|
|
3
|
+
import hashlib
|
|
4
|
+
from re import split
|
|
5
|
+
from urllib.parse import unquote, urlparse, parse_qsl
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
from .compat import random
|
|
8
|
+
from .hotp import HOTP as HOTP
|
|
9
|
+
from .otp import OTP as OTP
|
|
10
|
+
from .totp import TOTP as TOTP
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
random = _random.SystemRandom()
|
|
12
|
+
|
|
13
|
+
def random_base32(
|
|
14
|
+
length: int = 16,
|
|
15
|
+
chars: Sequence[str] = list('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')) -> str:
|
|
16
|
+
if length < 16:
|
|
17
|
+
raise Exception("Secrets should be at least 128 bits")
|
|
19
18
|
|
|
20
19
|
return ''.join(
|
|
21
20
|
random.choice(chars)
|
|
22
21
|
for _ in range(length)
|
|
23
22
|
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def random_hex(
|
|
26
|
+
length: int = 32,
|
|
27
|
+
chars: Sequence[str] = list('ABCDEF0123456789')) -> str:
|
|
28
|
+
if length < 32:
|
|
29
|
+
raise Exception("Secrets should be at least 128 bits")
|
|
30
|
+
return random_base32(length=length, chars=chars)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def parse_uri(uri: str) -> OTP:
|
|
34
|
+
"""
|
|
35
|
+
Parses the provisioning URI for the OTP; works for either TOTP or HOTP.
|
|
36
|
+
|
|
37
|
+
See also:
|
|
38
|
+
https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
|
39
|
+
|
|
40
|
+
:param uri: the hotp/totp URI to parse
|
|
41
|
+
:returns: OTP object
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
# Secret (to be filled in later)
|
|
45
|
+
secret = None
|
|
46
|
+
|
|
47
|
+
# Data we'll parse to the correct constructor
|
|
48
|
+
otp_data = {} # type: Dict[str, Any]
|
|
49
|
+
|
|
50
|
+
# Parse with URLlib
|
|
51
|
+
parsed_uri = urlparse(unquote(uri))
|
|
52
|
+
|
|
53
|
+
if parsed_uri.scheme != 'otpauth':
|
|
54
|
+
raise ValueError('Not an otpauth URI')
|
|
55
|
+
|
|
56
|
+
# Parse issuer/accountname info
|
|
57
|
+
accountinfo_parts = split(':|%3A', parsed_uri.path[1:], maxsplit=1)
|
|
58
|
+
if len(accountinfo_parts) == 1:
|
|
59
|
+
otp_data['name'] = accountinfo_parts[0]
|
|
60
|
+
else:
|
|
61
|
+
otp_data['issuer'] = accountinfo_parts[0]
|
|
62
|
+
otp_data['name'] = accountinfo_parts[1]
|
|
63
|
+
|
|
64
|
+
# Parse values
|
|
65
|
+
for key, value in parse_qsl(parsed_uri.query):
|
|
66
|
+
if key == 'secret':
|
|
67
|
+
secret = value
|
|
68
|
+
elif key == 'issuer':
|
|
69
|
+
if 'issuer' in otp_data and otp_data['issuer'] is not None and otp_data['issuer'] != value:
|
|
70
|
+
raise ValueError('If issuer is specified in both label and parameters, it should be equal.')
|
|
71
|
+
otp_data['issuer'] = value
|
|
72
|
+
elif key == 'algorithm':
|
|
73
|
+
if value == 'SHA1':
|
|
74
|
+
otp_data['digest'] = hashlib.sha1
|
|
75
|
+
elif value == 'SHA256':
|
|
76
|
+
otp_data['digest'] = hashlib.sha256
|
|
77
|
+
elif value == 'SHA512':
|
|
78
|
+
otp_data['digest'] = hashlib.sha512
|
|
79
|
+
else:
|
|
80
|
+
raise ValueError('Invalid value for algorithm, must be SHA1, SHA256 or SHA512')
|
|
81
|
+
elif key == 'digits':
|
|
82
|
+
digits = int(value)
|
|
83
|
+
if digits not in [6, 8]:
|
|
84
|
+
raise ValueError('Digits may only be 6 or 8')
|
|
85
|
+
otp_data['digits'] = digits
|
|
86
|
+
elif key == 'period':
|
|
87
|
+
otp_data['interval'] = value
|
|
88
|
+
elif key == 'counter':
|
|
89
|
+
otp_data['initial_count'] = value
|
|
90
|
+
else:
|
|
91
|
+
raise ValueError('{} is not a valid parameter'.format(key))
|
|
92
|
+
|
|
93
|
+
if not secret:
|
|
94
|
+
raise ValueError('No secret found in URI')
|
|
95
|
+
|
|
96
|
+
# Create objects
|
|
97
|
+
if parsed_uri.netloc == 'totp':
|
|
98
|
+
return TOTP(secret, **otp_data)
|
|
99
|
+
elif parsed_uri.netloc == 'hotp':
|
|
100
|
+
return HOTP(secret, **otp_data)
|
|
101
|
+
|
|
102
|
+
raise ValueError('Not a supported OTP type')
|
pyotp/compat.py
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
# Use secrets module if available (Python version >= 3.6) per PEP 506
|
|
2
|
+
try:
|
|
3
|
+
from secrets import SystemRandom # type: ignore
|
|
4
|
+
except ImportError:
|
|
5
|
+
from random import SystemRandom
|
|
2
6
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
USING_PYTHON2 = True if sys.version_info < (3, 0) else False
|
|
6
|
-
|
|
7
|
-
if USING_PYTHON2:
|
|
8
|
-
str = unicode # noqa
|
|
9
|
-
else:
|
|
10
|
-
str = str
|
|
7
|
+
random = SystemRandom()
|
pyotp/hotp.py
CHANGED
|
@@ -1,36 +1,43 @@
|
|
|
1
|
-
from
|
|
1
|
+
from typing import Any, Optional
|
|
2
2
|
|
|
3
3
|
from . import utils
|
|
4
4
|
from .otp import OTP
|
|
5
|
-
|
|
5
|
+
|
|
6
6
|
|
|
7
7
|
class HOTP(OTP):
|
|
8
8
|
"""
|
|
9
9
|
Handler for HMAC-based OTP counters.
|
|
10
10
|
"""
|
|
11
|
-
def
|
|
11
|
+
def __init__(self, *args: Any, initial_count: int = 0, **kwargs: Any) -> None:
|
|
12
|
+
"""
|
|
13
|
+
:param initial_count: starting HMAC counter value, defaults to 0
|
|
14
|
+
"""
|
|
15
|
+
self.initial_count = initial_count
|
|
16
|
+
super(HOTP, self).__init__(*args, **kwargs)
|
|
17
|
+
|
|
18
|
+
def at(self, count: int) -> str:
|
|
12
19
|
"""
|
|
13
20
|
Generates the OTP for the given count.
|
|
14
21
|
|
|
15
22
|
:param count: the OTP HMAC counter
|
|
16
|
-
:type count: int
|
|
17
23
|
:returns: OTP
|
|
18
|
-
:rtype: str
|
|
19
24
|
"""
|
|
20
25
|
return self.generate_otp(count)
|
|
21
26
|
|
|
22
|
-
def verify(self, otp, counter):
|
|
27
|
+
def verify(self, otp: str, counter: int) -> bool:
|
|
23
28
|
"""
|
|
24
29
|
Verifies the OTP passed in against the current counter OTP.
|
|
25
30
|
|
|
26
31
|
:param otp: the OTP to check against
|
|
27
|
-
:
|
|
28
|
-
:param count: the OTP HMAC counter
|
|
29
|
-
:type count: int
|
|
32
|
+
:param counter: the OTP HMAC counter
|
|
30
33
|
"""
|
|
31
34
|
return utils.strings_equal(str(otp), str(self.at(counter)))
|
|
32
35
|
|
|
33
|
-
def provisioning_uri(
|
|
36
|
+
def provisioning_uri(
|
|
37
|
+
self,
|
|
38
|
+
name: Optional[str] = None,
|
|
39
|
+
initial_count: Optional[int] = None,
|
|
40
|
+
issuer_name: Optional[str] = None) -> str:
|
|
34
41
|
"""
|
|
35
42
|
Returns the provisioning URI for the OTP. This can then be
|
|
36
43
|
encoded in a QR Code and used to provision an OTP app like
|
|
@@ -40,19 +47,16 @@ class HOTP(OTP):
|
|
|
40
47
|
https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
|
41
48
|
|
|
42
49
|
:param name: name of the user account
|
|
43
|
-
:type name: str
|
|
44
50
|
:param initial_count: starting HMAC counter value, defaults to 0
|
|
45
|
-
:type initial_count: int
|
|
46
51
|
:param issuer_name: the name of the OTP issuer; this will be the
|
|
47
52
|
organization title of the OTP entry in Authenticator
|
|
48
53
|
:returns: provisioning URI
|
|
49
|
-
:rtype: str
|
|
50
54
|
"""
|
|
51
55
|
return utils.build_uri(
|
|
52
56
|
self.secret,
|
|
53
|
-
name,
|
|
54
|
-
initial_count=initial_count,
|
|
55
|
-
|
|
57
|
+
name=name if name else self.name,
|
|
58
|
+
initial_count=initial_count if initial_count else self.initial_count,
|
|
59
|
+
issuer=issuer_name if issuer_name else self.issuer,
|
|
56
60
|
algorithm=self.digest().name,
|
|
57
61
|
digits=self.digits
|
|
58
62
|
)
|
pyotp/otp.py
CHANGED
|
@@ -1,32 +1,39 @@
|
|
|
1
|
-
from
|
|
1
|
+
from typing import Any, Optional
|
|
2
2
|
|
|
3
3
|
import base64
|
|
4
4
|
import hashlib
|
|
5
5
|
import hmac
|
|
6
|
-
|
|
6
|
+
|
|
7
7
|
|
|
8
8
|
class OTP(object):
|
|
9
9
|
"""
|
|
10
10
|
Base class for OTP handlers.
|
|
11
11
|
"""
|
|
12
|
-
def __init__(
|
|
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
|
"""
|
|
14
21
|
:param s: secret in base32 format
|
|
15
|
-
:type s: str
|
|
16
22
|
:param digits: number of integers in the OTP. Some apps expect this to be 6 digits, others support more.
|
|
17
|
-
:type digits: int
|
|
18
23
|
:param digest: digest function to use in the HMAC (expected to be sha1)
|
|
19
|
-
:
|
|
24
|
+
:param name: account name
|
|
25
|
+
:param issuer: issuer
|
|
20
26
|
"""
|
|
21
27
|
self.digits = digits
|
|
22
28
|
self.digest = digest
|
|
23
29
|
self.secret = s
|
|
30
|
+
self.name = name or 'Secret'
|
|
31
|
+
self.issuer = issuer
|
|
24
32
|
|
|
25
|
-
def generate_otp(self, input):
|
|
33
|
+
def generate_otp(self, input: int) -> str:
|
|
26
34
|
"""
|
|
27
35
|
:param input: the HMAC counter value to use as the OTP input.
|
|
28
36
|
Usually either the counter, or the computed integer based on the Unix timestamp
|
|
29
|
-
:type input: int
|
|
30
37
|
"""
|
|
31
38
|
if input < 0:
|
|
32
39
|
raise ValueError('input must be positive integer')
|
|
@@ -43,14 +50,14 @@ class OTP(object):
|
|
|
43
50
|
|
|
44
51
|
return str_code
|
|
45
52
|
|
|
46
|
-
def byte_secret(self):
|
|
53
|
+
def byte_secret(self) -> bytes:
|
|
47
54
|
missing_padding = len(self.secret) % 8
|
|
48
55
|
if missing_padding != 0:
|
|
49
56
|
self.secret += '=' * (8 - missing_padding)
|
|
50
57
|
return base64.b32decode(self.secret, casefold=True)
|
|
51
58
|
|
|
52
59
|
@staticmethod
|
|
53
|
-
def int_to_bytestring(i, padding=8):
|
|
60
|
+
def int_to_bytestring(i: int, padding: int = 8) -> bytes:
|
|
54
61
|
"""
|
|
55
62
|
Turns an integer to the OATH specified
|
|
56
63
|
bytestring, which is fed to the HMAC
|
pyotp/py.typed
ADDED
|
File without changes
|
pyotp/totp.py
CHANGED
|
@@ -1,60 +1,57 @@
|
|
|
1
|
-
from
|
|
1
|
+
from typing import Any, Union, Optional
|
|
2
2
|
|
|
3
3
|
import datetime
|
|
4
4
|
import time
|
|
5
5
|
|
|
6
6
|
from . import utils
|
|
7
7
|
from .otp import OTP
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
|
|
10
10
|
class TOTP(OTP):
|
|
11
11
|
"""
|
|
12
12
|
Handler for time-based OTP counters.
|
|
13
13
|
"""
|
|
14
|
-
def __init__(self, *args, **kwargs):
|
|
14
|
+
def __init__(self, *args: Any, interval: int = 30, **kwargs: Any) -> None:
|
|
15
15
|
"""
|
|
16
16
|
:param interval: the time interval in seconds
|
|
17
17
|
for OTP. This defaults to 30.
|
|
18
|
-
:type interval: int
|
|
19
18
|
"""
|
|
20
|
-
self.interval =
|
|
19
|
+
self.interval = interval
|
|
21
20
|
super(TOTP, self).__init__(*args, **kwargs)
|
|
22
21
|
|
|
23
|
-
def at(self, for_time, counter_offset=0):
|
|
22
|
+
def at(self, for_time: Union[int, datetime.datetime], counter_offset: int = 0) -> str:
|
|
24
23
|
"""
|
|
25
24
|
Accepts either a Unix timestamp integer or a datetime object.
|
|
26
25
|
|
|
26
|
+
To get the time until the next timecode change (seconds until the current OTP expires), use this instead::
|
|
27
|
+
|
|
28
|
+
totp = pyotp.TOTP(...)
|
|
29
|
+
time_remaining = totp.interval - datetime.datetime.now().timestamp() % totp.interval
|
|
30
|
+
|
|
27
31
|
:param for_time: the time to generate an OTP for
|
|
28
|
-
:type for_time: int or datetime
|
|
29
32
|
:param counter_offset: the amount of ticks to add to the time counter
|
|
30
33
|
:returns: OTP value
|
|
31
|
-
:rtype: str
|
|
32
34
|
"""
|
|
33
35
|
if not isinstance(for_time, datetime.datetime):
|
|
34
36
|
for_time = datetime.datetime.fromtimestamp(int(for_time))
|
|
35
37
|
return self.generate_otp(self.timecode(for_time) + counter_offset)
|
|
36
38
|
|
|
37
|
-
def now(self):
|
|
39
|
+
def now(self) -> str:
|
|
38
40
|
"""
|
|
39
41
|
Generate the current time OTP
|
|
40
42
|
|
|
41
43
|
:returns: OTP value
|
|
42
|
-
:rtype: str
|
|
43
44
|
"""
|
|
44
45
|
return self.generate_otp(self.timecode(datetime.datetime.now()))
|
|
45
46
|
|
|
46
|
-
def verify(self, otp, for_time=None, valid_window=0):
|
|
47
|
+
def verify(self, otp: str, for_time: Optional[datetime.datetime] = None, valid_window: int = 0) -> bool:
|
|
47
48
|
"""
|
|
48
49
|
Verifies the OTP passed in against the current time OTP.
|
|
49
50
|
|
|
50
51
|
:param otp: the OTP to check against
|
|
51
|
-
:type otp: str
|
|
52
52
|
:param for_time: Time to check OTP at (defaults to now)
|
|
53
|
-
:type for_time: int or datetime
|
|
54
53
|
:param valid_window: extends the validity to this many counter ticks before and after the current one
|
|
55
|
-
:type valid_window: int
|
|
56
54
|
:returns: True if verification succeeded, False otherwise
|
|
57
|
-
:rtype: bool
|
|
58
55
|
"""
|
|
59
56
|
if for_time is None:
|
|
60
57
|
for_time = datetime.datetime.now()
|
|
@@ -67,7 +64,7 @@ class TOTP(OTP):
|
|
|
67
64
|
|
|
68
65
|
return utils.strings_equal(str(otp), str(self.at(for_time)))
|
|
69
66
|
|
|
70
|
-
def provisioning_uri(self, name, issuer_name=None):
|
|
67
|
+
def provisioning_uri(self, name: Optional[str] = None, issuer_name: Optional[str] = None) -> str:
|
|
71
68
|
"""
|
|
72
69
|
Returns the provisioning URI for the OTP. This can then be
|
|
73
70
|
encoded in a QR Code and used to provision an OTP app like
|
|
@@ -76,17 +73,12 @@ class TOTP(OTP):
|
|
|
76
73
|
See also:
|
|
77
74
|
https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
|
78
75
|
|
|
79
|
-
:param name: name of the user account
|
|
80
|
-
:type name: str
|
|
81
|
-
:param issuer_name: the name of the OTP issuer; this will be the
|
|
82
|
-
organization title of the OTP entry in Authenticator
|
|
83
|
-
:returns: provisioning URI
|
|
84
|
-
:rtype: str
|
|
85
76
|
"""
|
|
86
|
-
return utils.build_uri(self.secret, name
|
|
77
|
+
return utils.build_uri(self.secret, name if name else self.name,
|
|
78
|
+
issuer=issuer_name if issuer_name else self.issuer,
|
|
87
79
|
algorithm=self.digest().name,
|
|
88
80
|
digits=self.digits, period=self.interval)
|
|
89
81
|
|
|
90
|
-
def timecode(self, for_time):
|
|
82
|
+
def timecode(self, for_time: datetime.datetime) -> int:
|
|
91
83
|
i = time.mktime(for_time.timetuple())
|
|
92
84
|
return int(i / self.interval)
|
pyotp/utils.py
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
|
-
from
|
|
1
|
+
from typing import Dict, Optional, Union
|
|
2
2
|
|
|
3
3
|
import unicodedata
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
algorithm=None, digits=None, period=None):
|
|
4
|
+
from hmac import compare_digest
|
|
5
|
+
from urllib.parse import quote, urlencode
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_uri(
|
|
9
|
+
secret: str,
|
|
10
|
+
name: str,
|
|
11
|
+
initial_count: Optional[int] = None,
|
|
12
|
+
issuer: Optional[str] = None,
|
|
13
|
+
algorithm: Optional[str] = None,
|
|
14
|
+
digits: Optional[int] = None,
|
|
15
|
+
period: Optional[int] = None) -> str:
|
|
17
16
|
"""
|
|
18
17
|
Returns the provisioning URI for the OTP; works for either TOTP or HOTP.
|
|
19
18
|
|
|
@@ -26,24 +25,16 @@ def build_uri(secret, name, initial_count=None, issuer_name=None,
|
|
|
26
25
|
https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
|
27
26
|
|
|
28
27
|
:param secret: the hotp/totp secret used to generate the URI
|
|
29
|
-
:type secret: str
|
|
30
28
|
:param name: name of the account
|
|
31
|
-
:type name: str
|
|
32
29
|
:param initial_count: starting counter value, defaults to None.
|
|
33
30
|
If none, the OTP type will be assumed as TOTP.
|
|
34
|
-
:
|
|
35
|
-
:param issuer_name: the name of the OTP issuer; this will be the
|
|
31
|
+
:param issuer: the name of the OTP issuer; this will be the
|
|
36
32
|
organization title of the OTP entry in Authenticator
|
|
37
|
-
:type issuer_name: str
|
|
38
33
|
:param algorithm: the algorithm used in the OTP generation.
|
|
39
|
-
:type algorithm: str
|
|
40
34
|
:param digits: the length of the OTP generated code.
|
|
41
|
-
:type digits: int
|
|
42
35
|
:param period: the number of seconds the OTP generator is set to
|
|
43
36
|
expire every code.
|
|
44
|
-
:type period: int
|
|
45
37
|
:returns: provisioning uri
|
|
46
|
-
:rtype: str
|
|
47
38
|
"""
|
|
48
39
|
# initial_count may be 0 as a valid param
|
|
49
40
|
is_initial_count_present = (initial_count is not None)
|
|
@@ -56,17 +47,17 @@ def build_uri(secret, name, initial_count=None, issuer_name=None,
|
|
|
56
47
|
otp_type = 'hotp' if is_initial_count_present else 'totp'
|
|
57
48
|
base_uri = 'otpauth://{0}/{1}?{2}'
|
|
58
49
|
|
|
59
|
-
url_args = {'secret': secret}
|
|
50
|
+
url_args = {'secret': secret} # type: Dict[str, Union[None, int, str]]
|
|
60
51
|
|
|
61
52
|
label = quote(name)
|
|
62
|
-
if
|
|
63
|
-
label = quote(
|
|
64
|
-
url_args['issuer'] =
|
|
53
|
+
if issuer is not None:
|
|
54
|
+
label = quote(issuer) + ':' + label
|
|
55
|
+
url_args['issuer'] = issuer
|
|
65
56
|
|
|
66
57
|
if is_initial_count_present:
|
|
67
58
|
url_args['counter'] = initial_count
|
|
68
59
|
if is_algorithm_set:
|
|
69
|
-
url_args['algorithm'] = algorithm.upper()
|
|
60
|
+
url_args['algorithm'] = algorithm.upper() # type: ignore
|
|
70
61
|
if is_digits_set:
|
|
71
62
|
url_args['digits'] = digits
|
|
72
63
|
if is_period_set:
|
|
@@ -76,26 +67,7 @@ def build_uri(secret, name, initial_count=None, issuer_name=None,
|
|
|
76
67
|
return uri
|
|
77
68
|
|
|
78
69
|
|
|
79
|
-
def
|
|
80
|
-
differences = 0
|
|
81
|
-
for c1, c2 in izip_longest(s1, s2):
|
|
82
|
-
if c1 is None or c2 is None:
|
|
83
|
-
differences = 1
|
|
84
|
-
continue
|
|
85
|
-
differences |= ord(c1) ^ ord(c2)
|
|
86
|
-
return differences == 0
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
try:
|
|
90
|
-
# Python 3.3+ and 2.7.7+ include a timing-attack-resistant
|
|
91
|
-
# comparison function, which is probably more reliable than ours.
|
|
92
|
-
# Use it if available.
|
|
93
|
-
from hmac import compare_digest
|
|
94
|
-
except ImportError:
|
|
95
|
-
compare_digest = _compare_digest
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def strings_equal(s1, s2):
|
|
70
|
+
def strings_equal(s1: str, s2: str) -> bool:
|
|
99
71
|
"""
|
|
100
72
|
Timing-attack resistant string comparison.
|
|
101
73
|
|
|
@@ -106,4 +78,4 @@ def strings_equal(s1, s2):
|
|
|
106
78
|
"""
|
|
107
79
|
s1 = unicodedata.normalize('NFKC', s1)
|
|
108
80
|
s2 = unicodedata.normalize('NFKC', s2)
|
|
109
|
-
return compare_digest(s1.encode(), s2.encode())
|
|
81
|
+
return compare_digest(s1.encode("utf-8"), s2.encode("utf-8"))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pyotp
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: Python One Time Password Library
|
|
5
5
|
Home-page: https://github.com/pyotp/pyotp
|
|
6
6
|
Author: PyOTP contributors
|
|
@@ -13,7 +13,6 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
13
13
|
Classifier: Operating System :: MacOS :: MacOS X
|
|
14
14
|
Classifier: Operating System :: POSIX
|
|
15
15
|
Classifier: Programming Language :: Python
|
|
16
|
-
Classifier: Programming Language :: Python :: 2.7
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.4
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.5
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.6
|
|
@@ -98,11 +97,15 @@ Counter-based OTPs
|
|
|
98
97
|
hotp.verify('316439', 1401) # => True
|
|
99
98
|
hotp.verify('316439', 1402) # => False
|
|
100
99
|
|
|
101
|
-
Generating a
|
|
102
|
-
|
|
103
|
-
::
|
|
100
|
+
Generating a Secret Key
|
|
101
|
+
~~~~~~~~~~~~~~~~~~~~~~~
|
|
102
|
+
A helper function is provided to generate a 16 character base32 secret, compatible with Google Authenticator and other OTP apps::
|
|
103
|
+
|
|
104
|
+
pyotp.random_base32()
|
|
104
105
|
|
|
105
|
-
|
|
106
|
+
Some applications want the secret key to be formatted as a hex-encoded string::
|
|
107
|
+
|
|
108
|
+
pyotp.random_hex() # returns a 32-character hex-encoded secret
|
|
106
109
|
|
|
107
110
|
Google Authenticator Compatible
|
|
108
111
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
@@ -110,17 +113,27 @@ Google Authenticator Compatible
|
|
|
110
113
|
PyOTP works with the Google Authenticator iPhone and Android app, as well as other OTP apps like Authy. PyOTP includes the
|
|
111
114
|
ability to generate provisioning URIs for use with the QR Code scanner built into these MFA client apps::
|
|
112
115
|
|
|
113
|
-
pyotp.totp.TOTP('JBSWY3DPEHPK3PXP').provisioning_uri(
|
|
116
|
+
pyotp.totp.TOTP('JBSWY3DPEHPK3PXP').provisioning_uri(name='alice@google.com', issuer_name='Secure App')
|
|
114
117
|
|
|
115
118
|
>>> 'otpauth://totp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App'
|
|
116
119
|
|
|
117
|
-
pyotp.hotp.HOTP('JBSWY3DPEHPK3PXP').provisioning_uri("alice@google.com",
|
|
120
|
+
pyotp.hotp.HOTP('JBSWY3DPEHPK3PXP').provisioning_uri(name="alice@google.com", issuer_name="Secure App", initial_count=0)
|
|
118
121
|
|
|
119
122
|
>>> 'otpauth://hotp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App&counter=0'
|
|
120
123
|
|
|
121
124
|
This URL can then be rendered as a QR Code (for example, using https://github.com/neocotic/qrious) which can then be scanned
|
|
122
125
|
and added to the users list of OTP credentials.
|
|
123
126
|
|
|
127
|
+
Parsing these URLs is also supported::
|
|
128
|
+
|
|
129
|
+
pyotp.parse_uri('otpauth://totp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App')
|
|
130
|
+
|
|
131
|
+
>>> <pyotp.totp.TOTP object at 0xFFFFFFFF>
|
|
132
|
+
|
|
133
|
+
pyotp.parse_uri('otpauth://hotp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App&counter=0'
|
|
134
|
+
|
|
135
|
+
>>> <pyotp.totp.HOTP object at 0xFFFFFFFF>
|
|
136
|
+
|
|
124
137
|
Working example
|
|
125
138
|
~~~~~~~~~~~~~~~
|
|
126
139
|
|
|
@@ -137,10 +150,10 @@ Now run the following and compare the output::
|
|
|
137
150
|
Links
|
|
138
151
|
~~~~~
|
|
139
152
|
|
|
140
|
-
* `Project home page (GitHub) <https://github.com/
|
|
153
|
+
* `Project home page (GitHub) <https://github.com/pyauth/pyotp>`_
|
|
141
154
|
* `Documentation (Read the Docs) <https://pyotp.readthedocs.io/en/latest/>`_
|
|
142
155
|
* `Package distribution (PyPI) <https://pypi.python.org/pypi/pyotp>`_
|
|
143
|
-
* `Change log <https://github.com/
|
|
156
|
+
* `Change log <https://github.com/pyauth/pyotp/blob/master/Changes.rst>`_
|
|
144
157
|
* `RFC 4226: HOTP: An HMAC-Based One-Time Password <https://tools.ietf.org/html/rfc4226>`_
|
|
145
158
|
* `RFC 6238: TOTP: Time-Based One-Time Password Algorithm <https://tools.ietf.org/html/rfc6238>`_
|
|
146
159
|
* `ROTP <https://github.com/mdp/rotp>`_ - Original Ruby OTP library by `Mark Percival <https://github.com/mdp>`_
|
|
@@ -153,10 +166,10 @@ For new applications:
|
|
|
153
166
|
* `WebAuthn <https://www.w3.org/TR/webauthn/>`_
|
|
154
167
|
* `PyWARP <https://github.com/pyauth/pywarp>`_
|
|
155
168
|
|
|
156
|
-
.. image:: https://
|
|
157
|
-
:target: https://
|
|
158
|
-
.. image:: https://img.shields.io/codecov/c/github/
|
|
159
|
-
:target: https://codecov.io/github/
|
|
169
|
+
.. image:: https://github.com/pyauth/pyotp/workflows/Python%20package/badge.svg
|
|
170
|
+
:target: https://github.com/pyauth/pyotp/actions
|
|
171
|
+
.. image:: https://img.shields.io/codecov/c/github/pyauth/pyotp/master.svg
|
|
172
|
+
:target: https://codecov.io/github/pyauth/pyotp?branch=master
|
|
160
173
|
.. image:: https://img.shields.io/pypi/v/pyotp.svg
|
|
161
174
|
:target: https://pypi.python.org/pypi/pyotp
|
|
162
175
|
.. image:: https://img.shields.io/pypi/l/pyotp.svg
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pyotp/__init__.py,sha256=z4ctyTv87_opGZTwvwlM8_EBaDaJsmGPxrKuipjxcg4,3223
|
|
2
|
+
pyotp/compat.py,sha256=i-pxcRWpShxkMTMucqPJdPJibYUaVtEhL9pyweKQ84k,209
|
|
3
|
+
pyotp/hotp.py,sha256=07e4wuWnCc9OJnQ8YX39vtsjSpSXPN_cue7S728Lmv0,2048
|
|
4
|
+
pyotp/otp.py,sha256=AHBnORBETYKnLqYzJUZnram6JddpNFvnHzedRpQbbyk,2479
|
|
5
|
+
pyotp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
pyotp/totp.py,sha256=uawrXOPiimcCys0WdYMD0vJqrIoe4l648Sv4R1mURT4,3140
|
|
7
|
+
pyotp/utils.py,sha256=6HBmC7ILxGOlFB6499Py_fufE4eyQvd4PU1gRngsydU,2836
|
|
8
|
+
pyotp-2.4.0.dist-info/LICENSE,sha256=f1C5lELlDYN3777_-tBjT7mgRr6HCXrzppnWNwsOG2M,1174
|
|
9
|
+
pyotp-2.4.0.dist-info/METADATA,sha256=DRJnqAlfj7tT58wV4Ci8wcU9mwO6XrJILgf1Sg4t-dg,8539
|
|
10
|
+
pyotp-2.4.0.dist-info/WHEEL,sha256=kGT74LWyRUZrL4VgLh6_g12IeVl_9u9ZVhadrgXZUEY,110
|
|
11
|
+
pyotp-2.4.0.dist-info/top_level.txt,sha256=rFDgz6Sm4nbkYk6tlSRiMcJPyYdnY6LcZiAaYTxVc5k,6
|
|
12
|
+
pyotp-2.4.0.dist-info/RECORD,,
|
pyotp-2.2.8.dist-info/RECORD
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
pyotp/__init__.py,sha256=hamm6NeVXq0-y6jlBlVnO7oct5n5EHWltinaBxge8xk,685
|
|
2
|
-
pyotp/compat.py,sha256=QVb4buImn4PyP0qe5uLiXsO2-p4ItTGl01Vbt2dILNE,222
|
|
3
|
-
pyotp/hotp.py,sha256=Jotukhsjpwpbx05iHHPYgH0HzpAFUvRAnWauQ_4N69M,1772
|
|
4
|
-
pyotp/otp.py,sha256=QPyHxZ7fY7_zlrMVVveGQrCfn6gatpaHO3-ok9XwCaU,2344
|
|
5
|
-
pyotp/totp.py,sha256=EHnv9irTDDt2cLUIxy3w_80QaqtduAM08Xq7rU2bIm0,3168
|
|
6
|
-
pyotp/utils.py,sha256=smffRHfGU9fwpzmkLrKz6Z3xt07-PEJ2lf8mJ1Tvn7I,3513
|
|
7
|
-
pyotp-2.2.8.dist-info/LICENSE,sha256=f1C5lELlDYN3777_-tBjT7mgRr6HCXrzppnWNwsOG2M,1174
|
|
8
|
-
pyotp-2.2.8.dist-info/METADATA,sha256=OHVMnmqShG3BbmFJPG9C6bi50zdvLZ27XJHr_NOq148,8004
|
|
9
|
-
pyotp-2.2.8.dist-info/WHEEL,sha256=h_aVn5OB2IERUjMbi2pucmR_zzWJtk303YXvhh60NJ8,110
|
|
10
|
-
pyotp-2.2.8.dist-info/top_level.txt,sha256=rFDgz6Sm4nbkYk6tlSRiMcJPyYdnY6LcZiAaYTxVc5k,6
|
|
11
|
-
pyotp-2.2.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|