PyOTP 2.0.1__tar.gz → 2.1.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.0.1/src/pyotp.egg-info → pyotp-2.1.0}/PKG-INFO +1 -1
- pyotp-2.1.0/requirements.txt +1 -0
- {pyotp-2.0.1 → pyotp-2.1.0}/setup.py +1 -1
- {pyotp-2.0.1 → pyotp-2.1.0}/src/pyotp/__init__.py +0 -3
- {pyotp-2.0.1 → pyotp-2.1.0}/src/pyotp/hotp.py +1 -1
- {pyotp-2.0.1 → pyotp-2.1.0}/src/pyotp/otp.py +4 -1
- {pyotp-2.0.1 → pyotp-2.1.0}/src/pyotp/totp.py +12 -4
- {pyotp-2.0.1 → pyotp-2.1.0}/src/pyotp/utils.py +4 -1
- {pyotp-2.0.1 → pyotp-2.1.0/src/pyotp.egg-info}/PKG-INFO +1 -1
- {pyotp-2.0.1 → pyotp-2.1.0}/src/pyotp.egg-info/SOURCES.txt +1 -0
- pyotp-2.1.0/src/pyotp.egg-info/requires.txt +1 -0
- {pyotp-2.0.1 → pyotp-2.1.0}/test.py +35 -14
- pyotp-2.0.1/requirements.txt +0 -0
- {pyotp-2.0.1 → pyotp-2.1.0}/LICENSE +0 -0
- {pyotp-2.0.1 → pyotp-2.1.0}/MANIFEST.in +0 -0
- {pyotp-2.0.1 → pyotp-2.1.0}/README.rst +0 -0
- {pyotp-2.0.1 → pyotp-2.1.0}/setup.cfg +0 -0
- {pyotp-2.0.1 → pyotp-2.1.0}/src/pyotp.egg-info/dependency_links.txt +0 -0
- {pyotp-2.0.1 → pyotp-2.1.0}/src/pyotp.egg-info/not-zip-safe +0 -0
- {pyotp-2.0.1 → pyotp-2.1.0}/src/pyotp.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
future==0.15.2
|
|
@@ -3,7 +3,7 @@ from __future__ import print_function, unicode_literals, division, absolute_impo
|
|
|
3
3
|
import base64
|
|
4
4
|
import hashlib
|
|
5
5
|
import hmac
|
|
6
|
-
|
|
6
|
+
from future.builtins import str
|
|
7
7
|
|
|
8
8
|
class OTP(object):
|
|
9
9
|
def __init__(self, s, digits=6, digest=hashlib.sha1):
|
|
@@ -46,6 +46,9 @@ class OTP(object):
|
|
|
46
46
|
return str_code
|
|
47
47
|
|
|
48
48
|
def byte_secret(self):
|
|
49
|
+
missing_padding = len(self.secret) % 8
|
|
50
|
+
if missing_padding != 0:
|
|
51
|
+
self.secret += '=' * (8 - missing_padding)
|
|
49
52
|
return base64.b32decode(self.secret, casefold=True)
|
|
50
53
|
|
|
51
54
|
@staticmethod
|
|
@@ -5,7 +5,7 @@ import time
|
|
|
5
5
|
|
|
6
6
|
from pyotp import utils
|
|
7
7
|
from pyotp.otp import OTP
|
|
8
|
-
|
|
8
|
+
from future.builtins import str
|
|
9
9
|
|
|
10
10
|
class TOTP(OTP):
|
|
11
11
|
def __init__(self, *args, **kwargs):
|
|
@@ -16,15 +16,16 @@ class TOTP(OTP):
|
|
|
16
16
|
self.interval = kwargs.pop('interval', 30)
|
|
17
17
|
super(TOTP, self).__init__(*args, **kwargs)
|
|
18
18
|
|
|
19
|
-
def at(self, for_time):
|
|
19
|
+
def at(self, for_time, counter_offset=0):
|
|
20
20
|
"""
|
|
21
21
|
Accepts either a Unix timestamp integer or a Time object.
|
|
22
22
|
Time objects will be adjusted to UTC automatically
|
|
23
23
|
@param [Time/Integer] time the time to generate an OTP for
|
|
24
|
+
@param [Integer] counter_offset an amount of ticks to add to the time counter
|
|
24
25
|
"""
|
|
25
26
|
if not isinstance(for_time, datetime.datetime):
|
|
26
27
|
for_time = datetime.datetime.fromtimestamp(int(for_time))
|
|
27
|
-
return self.generate_otp(self.timecode(for_time))
|
|
28
|
+
return self.generate_otp(self.timecode(for_time) + counter_offset)
|
|
28
29
|
|
|
29
30
|
def now(self):
|
|
30
31
|
"""
|
|
@@ -33,13 +34,20 @@ class TOTP(OTP):
|
|
|
33
34
|
"""
|
|
34
35
|
return self.generate_otp(self.timecode(datetime.datetime.now()))
|
|
35
36
|
|
|
36
|
-
def verify(self, otp, for_time=None):
|
|
37
|
+
def verify(self, otp, for_time=None, valid_window=0):
|
|
37
38
|
"""
|
|
38
39
|
Verifies the OTP passed in against the current time OTP
|
|
39
40
|
@param [String/Integer] otp the OTP to check against
|
|
41
|
+
@param [Integer] valid_window extends the validity to this many counter ticks before and after the current one
|
|
40
42
|
"""
|
|
41
43
|
if for_time is None:
|
|
42
44
|
for_time = datetime.datetime.now()
|
|
45
|
+
|
|
46
|
+
if valid_window:
|
|
47
|
+
for i in range(-valid_window, valid_window + 1):
|
|
48
|
+
if utils.strings_equal(str(otp), str(self.at(for_time, i))):
|
|
49
|
+
return True
|
|
50
|
+
return False
|
|
43
51
|
|
|
44
52
|
return utils.strings_equal(str(otp), str(self.at(for_time)))
|
|
45
53
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from __future__ import print_function, unicode_literals, division, absolute_import
|
|
2
2
|
|
|
3
|
+
import unicodedata
|
|
4
|
+
|
|
3
5
|
try:
|
|
4
6
|
from urllib.parse import quote
|
|
5
7
|
except ImportError:
|
|
6
8
|
from urllib import quote
|
|
7
9
|
|
|
8
|
-
|
|
9
10
|
def build_uri(secret, name, initial_count=None, issuer_name=None):
|
|
10
11
|
"""
|
|
11
12
|
Returns the provisioning URI for the OTP; works for either TOTP or HOTP.
|
|
@@ -60,6 +61,8 @@ def strings_equal(s1, s2):
|
|
|
60
61
|
still reveal to a timing attack whether the strings are the same
|
|
61
62
|
length.
|
|
62
63
|
"""
|
|
64
|
+
s1 = unicodedata.normalize('NFKC', s1)
|
|
65
|
+
s2 = unicodedata.normalize('NFKC', s2)
|
|
63
66
|
try:
|
|
64
67
|
# Python 3.3+ and 2.7.7+ include a timing-attack-resistant
|
|
65
68
|
# comparison function, which is probably more reliable than ours.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
future==0.15.2
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
|
+
# coding: utf-8
|
|
2
3
|
|
|
3
4
|
from __future__ import print_function, unicode_literals, division, absolute_import
|
|
4
5
|
|
|
@@ -12,9 +13,8 @@ import unittest
|
|
|
12
13
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
|
13
14
|
import pyotp
|
|
14
15
|
|
|
15
|
-
|
|
16
16
|
class HOTPExampleValuesFromTheRFC(unittest.TestCase):
|
|
17
|
-
def
|
|
17
|
+
def test_match_rfc(self):
|
|
18
18
|
# 12345678901234567890 in Bas32
|
|
19
19
|
# GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ
|
|
20
20
|
hotp = pyotp.HOTP('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ')
|
|
@@ -29,13 +29,13 @@ class HOTPExampleValuesFromTheRFC(unittest.TestCase):
|
|
|
29
29
|
self.assertEqual(hotp.at(8), '399871')
|
|
30
30
|
self.assertEqual(hotp.at(9), '520489')
|
|
31
31
|
|
|
32
|
-
def
|
|
32
|
+
def test_verify_otp_reuse(self):
|
|
33
33
|
hotp = pyotp.HOTP('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ')
|
|
34
34
|
self.assertTrue(hotp.verify('520489', 9))
|
|
35
35
|
self.assertFalse(hotp.verify('520489', 10))
|
|
36
36
|
self.assertFalse(hotp.verify('520489', 10))
|
|
37
37
|
|
|
38
|
-
def
|
|
38
|
+
def test_provisioning_uri(self):
|
|
39
39
|
hotp = pyotp.HOTP('wrn3pqx5uqxqvnqr')
|
|
40
40
|
|
|
41
41
|
self.assertEqual(
|
|
@@ -50,6 +50,13 @@ class HOTPExampleValuesFromTheRFC(unittest.TestCase):
|
|
|
50
50
|
hotp.provisioning_uri('mark@percival', issuer_name='FooCorp!'),
|
|
51
51
|
'otpauth://hotp/FooCorp%21:mark@percival?secret=wrn3pqx5uqxqvnqr&counter=0&issuer=FooCorp%21')
|
|
52
52
|
|
|
53
|
+
def test_other_secret(self):
|
|
54
|
+
hotp = pyotp.HOTP('N3OVNIBRERIO5OHGVCMDGS4V4RJ3AUZOUN34J6FRM4P6JIFCG3ZA')
|
|
55
|
+
self.assertEqual(hotp.at(0), '737863')
|
|
56
|
+
self.assertEqual(hotp.at(1), '390601')
|
|
57
|
+
self.assertEqual(hotp.at(2), '363354')
|
|
58
|
+
self.assertEqual(hotp.at(3), '936780')
|
|
59
|
+
self.assertEqual(hotp.at(4), '654019')
|
|
53
60
|
|
|
54
61
|
class TOTPExampleValuesFromTheRFC(unittest.TestCase):
|
|
55
62
|
RFC_VALUES = {
|
|
@@ -81,7 +88,7 @@ class TOTPExampleValuesFromTheRFC(unittest.TestCase):
|
|
|
81
88
|
),
|
|
82
89
|
}
|
|
83
90
|
|
|
84
|
-
def
|
|
91
|
+
def test_match_rfc(self):
|
|
85
92
|
for digest, secret in self.RFC_VALUES:
|
|
86
93
|
totp = pyotp.TOTP(base64.b32encode(secret), 8, digest)
|
|
87
94
|
for utime, code in self.RFC_VALUES[(digest, secret)]:
|
|
@@ -90,18 +97,18 @@ class TOTPExampleValuesFromTheRFC(unittest.TestCase):
|
|
|
90
97
|
msg %= (value, code, digest().name, utime)
|
|
91
98
|
self.assertEqual(value, str(code), msg)
|
|
92
99
|
|
|
93
|
-
def
|
|
100
|
+
def test_match_rfc_digit_length(self):
|
|
94
101
|
totp = pyotp.TOTP('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ')
|
|
95
102
|
self.assertEqual(totp.at(1111111111), '050471')
|
|
96
103
|
self.assertEqual(totp.at(1234567890), '005924')
|
|
97
104
|
self.assertEqual(totp.at(2000000000), '279037')
|
|
98
105
|
|
|
99
|
-
def
|
|
106
|
+
def test_match_google_authenticator_output(self):
|
|
100
107
|
totp = pyotp.TOTP('wrn3pqx5uqxqvnqr')
|
|
101
108
|
with Timecop(1297553958):
|
|
102
109
|
self.assertEqual(totp.now(), '102705')
|
|
103
110
|
|
|
104
|
-
def
|
|
111
|
+
def test_validate_totp(self):
|
|
105
112
|
totp = pyotp.TOTP('wrn3pqx5uqxqvnqr')
|
|
106
113
|
with Timecop(1297553958):
|
|
107
114
|
self.assertTrue(totp.verify('102705'))
|
|
@@ -109,14 +116,14 @@ class TOTPExampleValuesFromTheRFC(unittest.TestCase):
|
|
|
109
116
|
with Timecop(1297553958 + 30):
|
|
110
117
|
self.assertFalse(totp.verify('102705'))
|
|
111
118
|
|
|
112
|
-
def
|
|
119
|
+
def test_validate_totp_with_digit_length(self):
|
|
113
120
|
totp = pyotp.TOTP('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ')
|
|
114
121
|
with Timecop(1111111111):
|
|
115
122
|
self.assertTrue(totp.verify('050471'))
|
|
116
123
|
with Timecop(1297553958 + 30):
|
|
117
124
|
self.assertFalse(totp.verify('050471'))
|
|
118
125
|
|
|
119
|
-
def
|
|
126
|
+
def test_provisioning_uri(self):
|
|
120
127
|
totp = pyotp.TOTP('wrn3pqx5uqxqvnqr')
|
|
121
128
|
self.assertEqual(
|
|
122
129
|
totp.provisioning_uri('mark@percival'),
|
|
@@ -126,13 +133,12 @@ class TOTPExampleValuesFromTheRFC(unittest.TestCase):
|
|
|
126
133
|
totp.provisioning_uri('mark@percival', issuer_name='FooCorp!'),
|
|
127
134
|
'otpauth://totp/FooCorp%21:mark@percival?secret=wrn3pqx5uqxqvnqr&issuer=FooCorp%21')
|
|
128
135
|
|
|
129
|
-
def
|
|
136
|
+
def test_random_key_generation(self):
|
|
130
137
|
self.assertEqual(len(pyotp.random_base32()), 16)
|
|
131
138
|
self.assertEqual(len(pyotp.random_base32(length=20)), 20)
|
|
132
139
|
|
|
133
|
-
|
|
134
140
|
class StringComparisonTest(unittest.TestCase):
|
|
135
|
-
def
|
|
141
|
+
def test_comparisons(self):
|
|
136
142
|
self.assertTrue(pyotp.utils.strings_equal("", ""))
|
|
137
143
|
self.assertTrue(pyotp.utils.strings_equal("a", "a"))
|
|
138
144
|
self.assertTrue(pyotp.utils.strings_equal("a" * 1000, "a" * 1000))
|
|
@@ -141,6 +147,22 @@ class StringComparisonTest(unittest.TestCase):
|
|
|
141
147
|
self.assertFalse(pyotp.utils.strings_equal("a", ""))
|
|
142
148
|
self.assertFalse(pyotp.utils.strings_equal("a" * 999 + "b", "a" * 1000))
|
|
143
149
|
|
|
150
|
+
def test_fullwidth_input(self):
|
|
151
|
+
self.assertTrue(pyotp.utils.strings_equal("xs12345", "xs12345"))
|
|
152
|
+
|
|
153
|
+
class CounterOffsetTest(unittest.TestCase):
|
|
154
|
+
def test_counter_offset(self):
|
|
155
|
+
totp = pyotp.TOTP("ABCDEFGH")
|
|
156
|
+
self.assertEqual(totp.at(200), "028307")
|
|
157
|
+
self.assertTrue(totp.at(200, 1), "681610")
|
|
158
|
+
|
|
159
|
+
class ValidWindowTest(unittest.TestCase):
|
|
160
|
+
def test_valid_window(self):
|
|
161
|
+
totp = pyotp.TOTP("ABCDEFGH")
|
|
162
|
+
self.assertTrue(totp.verify("451564", 200, 1))
|
|
163
|
+
self.assertTrue(totp.verify("028307", 200, 1))
|
|
164
|
+
self.assertTrue(totp.verify("681610", 200, 1))
|
|
165
|
+
self.assertFalse(totp.verify("195979", 200, 1))
|
|
144
166
|
|
|
145
167
|
class Timecop(object):
|
|
146
168
|
"""
|
|
@@ -166,6 +188,5 @@ class Timecop(object):
|
|
|
166
188
|
timecop = self
|
|
167
189
|
return FrozenDateTime
|
|
168
190
|
|
|
169
|
-
|
|
170
191
|
if __name__ == '__main__':
|
|
171
192
|
unittest.main()
|
pyotp-2.0.1/requirements.txt
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|