PyOTP 2.4.1__tar.gz → 2.5.0__tar.gz
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-2.4.1/src/pyotp.egg-info → pyotp-2.5.0}/PKG-INFO +2 -2
- {pyotp-2.4.1 → pyotp-2.5.0}/README.rst +1 -1
- {pyotp-2.4.1 → pyotp-2.5.0}/setup.py +1 -1
- {pyotp-2.4.1 → pyotp-2.5.0}/src/pyotp/__init__.py +8 -13
- {pyotp-2.4.1 → pyotp-2.5.0}/src/pyotp/hotp.py +13 -4
- {pyotp-2.4.1 → pyotp-2.5.0}/src/pyotp/otp.py +3 -17
- {pyotp-2.4.1 → pyotp-2.5.0}/src/pyotp/totp.py +15 -8
- {pyotp-2.4.1 → pyotp-2.5.0}/src/pyotp/utils.py +11 -11
- {pyotp-2.4.1 → pyotp-2.5.0/src/pyotp.egg-info}/PKG-INFO +2 -2
- {pyotp-2.4.1 → pyotp-2.5.0}/test.py +11 -7
- {pyotp-2.4.1 → pyotp-2.5.0}/LICENSE +0 -0
- {pyotp-2.4.1 → pyotp-2.5.0}/MANIFEST.in +0 -0
- {pyotp-2.4.1 → pyotp-2.5.0}/requirements.txt +0 -0
- {pyotp-2.4.1 → pyotp-2.5.0}/setup.cfg +0 -0
- {pyotp-2.4.1 → pyotp-2.5.0}/src/pyotp/compat.py +0 -0
- {pyotp-2.4.1 → pyotp-2.5.0}/src/pyotp/py.typed +0 -0
- {pyotp-2.4.1 → pyotp-2.5.0}/src/pyotp.egg-info/SOURCES.txt +0 -0
- {pyotp-2.4.1 → pyotp-2.5.0}/src/pyotp.egg-info/dependency_links.txt +0 -0
- {pyotp-2.4.1 → pyotp-2.5.0}/src/pyotp.egg-info/not-zip-safe +0 -0
- {pyotp-2.4.1 → pyotp-2.5.0}/src/pyotp.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 1.1
|
|
2
2
|
Name: pyotp
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.0
|
|
4
4
|
Summary: Python One Time Password Library
|
|
5
5
|
Home-page: https://github.com/pyotp/pyotp
|
|
6
6
|
Author: PyOTP contributors
|
|
@@ -138,7 +138,7 @@ Description: PyOTP - The Python One-Time Password Library
|
|
|
138
138
|
~~~~~
|
|
139
139
|
|
|
140
140
|
* `Project home page (GitHub) <https://github.com/pyauth/pyotp>`_
|
|
141
|
-
* `Documentation
|
|
141
|
+
* `Documentation <https://pyauth.github.io/pyotp/>`_
|
|
142
142
|
* `Package distribution (PyPI) <https://pypi.python.org/pypi/pyotp>`_
|
|
143
143
|
* `Change log <https://github.com/pyauth/pyotp/blob/master/Changes.rst>`_
|
|
144
144
|
* `RFC 4226: HOTP: An HMAC-Based One-Time Password <https://tools.ietf.org/html/rfc4226>`_
|
|
@@ -130,7 +130,7 @@ Links
|
|
|
130
130
|
~~~~~
|
|
131
131
|
|
|
132
132
|
* `Project home page (GitHub) <https://github.com/pyauth/pyotp>`_
|
|
133
|
-
* `Documentation
|
|
133
|
+
* `Documentation <https://pyauth.github.io/pyotp/>`_
|
|
134
134
|
* `Package distribution (PyPI) <https://pypi.python.org/pypi/pyotp>`_
|
|
135
135
|
* `Change log <https://github.com/pyauth/pyotp/blob/master/Changes.rst>`_
|
|
136
136
|
* `RFC 4226: HOTP: An HMAC-Based One-Time Password <https://tools.ietf.org/html/rfc4226>`_
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
from typing import Any, Dict, Sequence
|
|
2
|
-
|
|
3
1
|
import hashlib
|
|
4
2
|
from re import split
|
|
3
|
+
from typing import Any, Dict, Sequence
|
|
5
4
|
from urllib.parse import unquote, urlparse, parse_qsl
|
|
6
5
|
|
|
7
6
|
from .compat import random
|
|
@@ -10,11 +9,9 @@ from .otp import OTP as OTP
|
|
|
10
9
|
from .totp import TOTP as TOTP
|
|
11
10
|
|
|
12
11
|
|
|
13
|
-
def random_base32(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if length < 16:
|
|
17
|
-
raise Exception("Secrets should be at least 128 bits")
|
|
12
|
+
def random_base32(length: int = 26, chars: Sequence[str] = list('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')) -> str:
|
|
13
|
+
if length < 26:
|
|
14
|
+
raise ValueError("Secrets should be at least 128 bits")
|
|
18
15
|
|
|
19
16
|
return ''.join(
|
|
20
17
|
random.choice(chars)
|
|
@@ -22,11 +19,9 @@ def random_base32(
|
|
|
22
19
|
)
|
|
23
20
|
|
|
24
21
|
|
|
25
|
-
def random_hex(
|
|
26
|
-
length: int = 32,
|
|
27
|
-
chars: Sequence[str] = list('ABCDEF0123456789')) -> str:
|
|
22
|
+
def random_hex(length: int = 32, chars: Sequence[str] = list('ABCDEF0123456789')) -> str:
|
|
28
23
|
if length < 32:
|
|
29
|
-
raise
|
|
24
|
+
raise ValueError("Secrets should be at least 128 bits")
|
|
30
25
|
return random_base32(length=length, chars=chars)
|
|
31
26
|
|
|
32
27
|
|
|
@@ -80,8 +75,8 @@ def parse_uri(uri: str) -> OTP:
|
|
|
80
75
|
raise ValueError('Invalid value for algorithm, must be SHA1, SHA256 or SHA512')
|
|
81
76
|
elif key == 'digits':
|
|
82
77
|
digits = int(value)
|
|
83
|
-
if digits not in [6, 8]:
|
|
84
|
-
raise ValueError('Digits may only be 6 or 8')
|
|
78
|
+
if digits not in [6, 7, 8]:
|
|
79
|
+
raise ValueError('Digits may only be 6, 7, or 8')
|
|
85
80
|
otp_data['digits'] = digits
|
|
86
81
|
elif key == 'period':
|
|
87
82
|
otp_data['interval'] = int(value)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import hashlib
|
|
1
2
|
from typing import Any, Optional
|
|
2
3
|
|
|
3
4
|
from . import utils
|
|
@@ -8,12 +9,18 @@ class HOTP(OTP):
|
|
|
8
9
|
"""
|
|
9
10
|
Handler for HMAC-based OTP counters.
|
|
10
11
|
"""
|
|
11
|
-
def __init__(self,
|
|
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
14
|
"""
|
|
15
|
+
:param s: secret in base32 format
|
|
13
16
|
:param initial_count: starting HMAC counter value, defaults to 0
|
|
17
|
+
:param digits: number of integers in the OTP. Some apps expect this to be 6 digits, others support more.
|
|
18
|
+
:param digest: digest function to use in the HMAC (expected to be sha1)
|
|
19
|
+
:param name: account name
|
|
20
|
+
:param issuer: issuer
|
|
14
21
|
"""
|
|
15
22
|
self.initial_count = initial_count
|
|
16
|
-
super(
|
|
23
|
+
super().__init__(s=s, digits=digits, digest=digest, name=name, issuer=issuer)
|
|
17
24
|
|
|
18
25
|
def at(self, count: int) -> str:
|
|
19
26
|
"""
|
|
@@ -37,7 +44,8 @@ class HOTP(OTP):
|
|
|
37
44
|
self,
|
|
38
45
|
name: Optional[str] = None,
|
|
39
46
|
initial_count: Optional[int] = None,
|
|
40
|
-
issuer_name: Optional[str] = None
|
|
47
|
+
issuer_name: Optional[str] = None,
|
|
48
|
+
image: Optional[str] = None) -> str:
|
|
41
49
|
"""
|
|
42
50
|
Returns the provisioning URI for the OTP. This can then be
|
|
43
51
|
encoded in a QR Code and used to provision an OTP app like
|
|
@@ -58,5 +66,6 @@ class HOTP(OTP):
|
|
|
58
66
|
initial_count=initial_count if initial_count else self.initial_count,
|
|
59
67
|
issuer=issuer_name if issuer_name else self.issuer,
|
|
60
68
|
algorithm=self.digest().name,
|
|
61
|
-
digits=self.digits
|
|
69
|
+
digits=self.digits,
|
|
70
|
+
image=image,
|
|
62
71
|
)
|
|
@@ -1,29 +1,15 @@
|
|
|
1
|
-
from typing import Any, Optional
|
|
2
|
-
|
|
3
1
|
import base64
|
|
4
2
|
import hashlib
|
|
5
3
|
import hmac
|
|
4
|
+
from typing import Any, Optional
|
|
6
5
|
|
|
7
6
|
|
|
8
7
|
class OTP(object):
|
|
9
8
|
"""
|
|
10
9
|
Base class for OTP handlers.
|
|
11
10
|
"""
|
|
12
|
-
def __init__(
|
|
13
|
-
|
|
14
|
-
s: str,
|
|
15
|
-
digits: int = 6,
|
|
16
|
-
digest: Any = hashlib.sha1,
|
|
17
|
-
name: Optional[str] = None,
|
|
18
|
-
issuer: Optional[str] = None
|
|
19
|
-
) -> None:
|
|
20
|
-
"""
|
|
21
|
-
:param s: secret in base32 format
|
|
22
|
-
:param digits: number of integers in the OTP. Some apps expect this to be 6 digits, others support more.
|
|
23
|
-
:param digest: digest function to use in the HMAC (expected to be sha1)
|
|
24
|
-
:param name: account name
|
|
25
|
-
:param issuer: issuer
|
|
26
|
-
"""
|
|
11
|
+
def __init__(self, s: str, digits: int = 6, digest: Any = hashlib.sha1, name: Optional[str] = None,
|
|
12
|
+
issuer: Optional[str] = None) -> None:
|
|
27
13
|
self.digits = digits
|
|
28
14
|
self.digest = digest
|
|
29
15
|
self.secret = s
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
from typing import Any, Union, Optional
|
|
2
|
-
|
|
3
1
|
import calendar
|
|
4
2
|
import datetime
|
|
3
|
+
import hashlib
|
|
5
4
|
import time
|
|
5
|
+
from typing import Any, Union, Optional
|
|
6
6
|
|
|
7
7
|
from . import utils
|
|
8
8
|
from .otp import OTP
|
|
@@ -12,13 +12,18 @@ class TOTP(OTP):
|
|
|
12
12
|
"""
|
|
13
13
|
Handler for time-based OTP counters.
|
|
14
14
|
"""
|
|
15
|
-
def __init__(self,
|
|
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:
|
|
16
17
|
"""
|
|
17
|
-
:param
|
|
18
|
-
|
|
18
|
+
:param s: secret in base32 format
|
|
19
|
+
:param interval: the time interval in seconds for OTP. This defaults to 30.
|
|
20
|
+
:param digits: number of integers in the OTP. Some apps expect this to be 6 digits, others support more.
|
|
21
|
+
:param digest: digest function to use in the HMAC (expected to be sha1)
|
|
22
|
+
:param name: account name
|
|
23
|
+
:param issuer: issuer
|
|
19
24
|
"""
|
|
20
25
|
self.interval = interval
|
|
21
|
-
super(
|
|
26
|
+
super().__init__(s=s, digits=digits, digest=digest, name=name, issuer=issuer)
|
|
22
27
|
|
|
23
28
|
def at(self, for_time: Union[int, datetime.datetime], counter_offset: int = 0) -> str:
|
|
24
29
|
"""
|
|
@@ -65,7 +70,9 @@ class TOTP(OTP):
|
|
|
65
70
|
|
|
66
71
|
return utils.strings_equal(str(otp), str(self.at(for_time)))
|
|
67
72
|
|
|
68
|
-
def provisioning_uri(self, name: Optional[str] = None, issuer_name: Optional[str] = None
|
|
73
|
+
def provisioning_uri(self, name: Optional[str] = None, issuer_name: Optional[str] = None,
|
|
74
|
+
image: Optional[str] = None) -> str:
|
|
75
|
+
|
|
69
76
|
"""
|
|
70
77
|
Returns the provisioning URI for the OTP. This can then be
|
|
71
78
|
encoded in a QR Code and used to provision an OTP app like
|
|
@@ -78,7 +85,7 @@ class TOTP(OTP):
|
|
|
78
85
|
return utils.build_uri(self.secret, name if name else self.name,
|
|
79
86
|
issuer=issuer_name if issuer_name else self.issuer,
|
|
80
87
|
algorithm=self.digest().name,
|
|
81
|
-
digits=self.digits, period=self.interval)
|
|
88
|
+
digits=self.digits, period=self.interval, image=image)
|
|
82
89
|
|
|
83
90
|
def timecode(self, for_time: datetime.datetime) -> int:
|
|
84
91
|
"""
|
|
@@ -1,18 +1,12 @@
|
|
|
1
|
-
from typing import Dict, Optional, Union
|
|
2
|
-
|
|
3
1
|
import unicodedata
|
|
4
2
|
from hmac import compare_digest
|
|
5
|
-
from
|
|
3
|
+
from typing import Dict, Optional, Union
|
|
4
|
+
from urllib.parse import quote, urlencode, urlparse
|
|
6
5
|
|
|
7
6
|
|
|
8
|
-
def build_uri(
|
|
9
|
-
|
|
10
|
-
|
|
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:
|
|
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:
|
|
16
10
|
"""
|
|
17
11
|
Returns the provisioning URI for the OTP; works for either TOTP or HOTP.
|
|
18
12
|
|
|
@@ -34,6 +28,7 @@ def build_uri(
|
|
|
34
28
|
:param digits: the length of the OTP generated code.
|
|
35
29
|
:param period: the number of seconds the OTP generator is set to
|
|
36
30
|
expire every code.
|
|
31
|
+
:param image: optional logo image url
|
|
37
32
|
:returns: provisioning uri
|
|
38
33
|
"""
|
|
39
34
|
# initial_count may be 0 as a valid param
|
|
@@ -62,6 +57,11 @@ def build_uri(
|
|
|
62
57
|
url_args['digits'] = digits
|
|
63
58
|
if is_period_set:
|
|
64
59
|
url_args['period'] = period
|
|
60
|
+
if image:
|
|
61
|
+
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
|
|
65
65
|
|
|
66
66
|
uri = base_uri.format(otp_type, label, urlencode(url_args).replace("+", "%20"))
|
|
67
67
|
return uri
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 1.1
|
|
2
2
|
Name: pyotp
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.0
|
|
4
4
|
Summary: Python One Time Password Library
|
|
5
5
|
Home-page: https://github.com/pyotp/pyotp
|
|
6
6
|
Author: PyOTP contributors
|
|
@@ -138,7 +138,7 @@ Description: PyOTP - The Python One-Time Password Library
|
|
|
138
138
|
~~~~~
|
|
139
139
|
|
|
140
140
|
* `Project home page (GitHub) <https://github.com/pyauth/pyotp>`_
|
|
141
|
-
* `Documentation
|
|
141
|
+
* `Documentation <https://pyauth.github.io/pyotp/>`_
|
|
142
142
|
* `Package distribution (PyPI) <https://pypi.python.org/pypi/pyotp>`_
|
|
143
143
|
* `Change log <https://github.com/pyauth/pyotp/blob/master/Changes.rst>`_
|
|
144
144
|
* `RFC 4226: HOTP: An HMAC-Based One-Time Password <https://tools.ietf.org/html/rfc4226>`_
|
|
@@ -291,14 +291,14 @@ class TOTPExampleValuesFromTheRFC(unittest.TestCase):
|
|
|
291
291
|
)
|
|
292
292
|
|
|
293
293
|
def test_random_key_generation(self):
|
|
294
|
-
self.assertEqual(len(pyotp.random_base32()),
|
|
295
|
-
self.assertEqual(len(pyotp.random_base32(length=
|
|
294
|
+
self.assertEqual(len(pyotp.random_base32()), 26)
|
|
295
|
+
self.assertEqual(len(pyotp.random_base32(length=32)), 32)
|
|
296
296
|
self.assertEqual(len(pyotp.random_hex()), 32)
|
|
297
297
|
self.assertEqual(len(pyotp.random_hex(length=64)), 64)
|
|
298
|
-
with self.assertRaises(
|
|
299
|
-
pyotp.random_base32(length=
|
|
300
|
-
with self.assertRaises(
|
|
301
|
-
pyotp.random_hex(length=
|
|
298
|
+
with self.assertRaises(ValueError):
|
|
299
|
+
pyotp.random_base32(length=25)
|
|
300
|
+
with self.assertRaises(ValueError):
|
|
301
|
+
pyotp.random_hex(length=31)
|
|
302
302
|
|
|
303
303
|
|
|
304
304
|
class CompareDigestTest(unittest.TestCase):
|
|
@@ -360,7 +360,7 @@ class ParseUriTest(unittest.TestCase):
|
|
|
360
360
|
|
|
361
361
|
with self.assertRaises(ValueError) as cm:
|
|
362
362
|
pyotp.parse_uri('otpauth://totp?digits=-1')
|
|
363
|
-
self.assertEqual('Digits may only be 6 or 8', str(cm.exception))
|
|
363
|
+
self.assertEqual('Digits may only be 6, 7, or 8', str(cm.exception))
|
|
364
364
|
|
|
365
365
|
with self.assertRaises(ValueError) as cm:
|
|
366
366
|
pyotp.parse_uri('otpauth://totp/SomeIssuer:?issuer=AnotherIssuer')
|
|
@@ -412,6 +412,10 @@ class ParseUriTest(unittest.TestCase):
|
|
|
412
412
|
self.assertEqual(otp.at(0), '816660')
|
|
413
413
|
self.assertEqual(otp.at(9000), '524153')
|
|
414
414
|
|
|
415
|
+
self.assertEqual(otp.provisioning_uri(name='n', issuer_name='i', image='https://test.net/test.png'),
|
|
416
|
+
'otpauth://totp/i:n?secret=GEZDGNBV&issuer=i&algorithm=SHA512&image=https%3A%2F%2Ftest.net%2Ftest.png')
|
|
417
|
+
with self.assertRaises(ValueError):
|
|
418
|
+
otp.provisioning_uri(name='n', issuer_name='i', image='nourl')
|
|
415
419
|
|
|
416
420
|
class Timecop(object):
|
|
417
421
|
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|