PyOTP 2.1.1__tar.gz → 2.2.1__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.1.1 → pyotp-2.2.1}/LICENSE +3 -4
- {pyotp-2.1.1/src/pyotp.egg-info → pyotp-2.2.1}/PKG-INFO +5 -7
- {pyotp-2.1.1 → pyotp-2.2.1}/README.rst +4 -6
- pyotp-2.2.1/requirements.txt +0 -0
- {pyotp-2.1.1 → pyotp-2.2.1}/setup.cfg +4 -0
- {pyotp-2.1.1 → pyotp-2.2.1}/setup.py +4 -3
- pyotp-2.2.1/src/pyotp/__init__.py +17 -0
- pyotp-2.2.1/src/pyotp/compat.py +10 -0
- {pyotp-2.1.1 → pyotp-2.2.1}/src/pyotp/hotp.py +6 -4
- {pyotp-2.1.1 → pyotp-2.2.1}/src/pyotp/otp.py +7 -11
- {pyotp-2.1.1 → pyotp-2.2.1}/src/pyotp/totp.py +12 -9
- pyotp-2.2.1/src/pyotp/utils.py +101 -0
- {pyotp-2.1.1 → pyotp-2.2.1/src/pyotp.egg-info}/PKG-INFO +5 -7
- {pyotp-2.1.1 → pyotp-2.2.1}/src/pyotp.egg-info/SOURCES.txt +1 -1
- pyotp-2.2.1/test.py +296 -0
- pyotp-2.1.1/requirements.txt +0 -1
- pyotp-2.1.1/src/pyotp/__init__.py +0 -15
- pyotp-2.1.1/src/pyotp/utils.py +0 -82
- pyotp-2.1.1/src/pyotp.egg-info/requires.txt +0 -1
- pyotp-2.1.1/test.py +0 -192
- {pyotp-2.1.1 → pyotp-2.2.1}/MANIFEST.in +0 -0
- {pyotp-2.1.1 → pyotp-2.2.1}/src/pyotp.egg-info/dependency_links.txt +0 -0
- {pyotp-2.1.1 → pyotp-2.2.1}/src/pyotp.egg-info/not-zip-safe +0 -0
- {pyotp-2.1.1 → pyotp-2.2.1}/src/pyotp.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
Copyright (C) 2011
|
|
2
|
-
|
|
3
|
-
Python port copyright 2011 by Nathan Reynolds <email@nreynolds.co.uk>
|
|
1
|
+
Copyright (C) 2011-2016 Mark Percival <m@mdp.im>,
|
|
2
|
+
Nathan Reynolds <email@nreynolds.co.uk>, and PyOTP contributors
|
|
4
3
|
|
|
5
4
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
5
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -18,4 +17,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
18
17
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
18
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
19
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
-
THE SOFTWARE.
|
|
20
|
+
THE SOFTWARE.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 1.1
|
|
2
2
|
Name: pyotp
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.1
|
|
4
4
|
Summary: Python One Time Password Library
|
|
5
5
|
Home-page: https://github.com/pyotp/pyotp
|
|
6
6
|
Author: PyOTP contributors
|
|
@@ -84,13 +84,13 @@ Description: PyOTP - The Python One-Time Password Library
|
|
|
84
84
|
|
|
85
85
|
Scan the following barcode with your phone, using Google Authenticator
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
.. image:: http://chart.apis.google.com/chart?cht=qr&chs=250x250&chl=otpauth%3A%2F%2Ftotp%2Falice%40google.com%3Fsecret%3DJBSWY3DPEHPK3PXP
|
|
88
88
|
|
|
89
89
|
Now run the following and compare the output::
|
|
90
90
|
|
|
91
91
|
import pyotp
|
|
92
92
|
totp = pyotp.TOTP("JBSWY3DPEHPK3PXP")
|
|
93
|
-
print
|
|
93
|
+
print("Current OTP:", totp.now())
|
|
94
94
|
|
|
95
95
|
Links
|
|
96
96
|
~~~~~
|
|
@@ -106,12 +106,10 @@ Description: PyOTP - The Python One-Time Password Library
|
|
|
106
106
|
|
|
107
107
|
.. image:: https://img.shields.io/travis/pyotp/pyotp.svg
|
|
108
108
|
:target: https://travis-ci.org/pyotp/pyotp
|
|
109
|
-
.. image:: https://img.shields.io/
|
|
110
|
-
:target: https://
|
|
109
|
+
.. image:: https://img.shields.io/codecov/c/github/pyotp/pyotp/master.svg
|
|
110
|
+
:target: https://codecov.io/github/pyotp/pyotp?branch=master
|
|
111
111
|
.. image:: https://img.shields.io/pypi/v/pyotp.svg
|
|
112
112
|
:target: https://pypi.python.org/pypi/pyotp
|
|
113
|
-
.. image:: https://img.shields.io/pypi/dm/pyotp.svg
|
|
114
|
-
:target: https://pypi.python.org/pypi/pyotp
|
|
115
113
|
.. image:: https://img.shields.io/pypi/l/pyotp.svg
|
|
116
114
|
:target: https://pypi.python.org/pypi/pyotp
|
|
117
115
|
.. image:: https://readthedocs.org/projects/pyotp/badge/?version=latest
|
|
@@ -76,13 +76,13 @@ Working example
|
|
|
76
76
|
|
|
77
77
|
Scan the following barcode with your phone, using Google Authenticator
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
.. image:: http://chart.apis.google.com/chart?cht=qr&chs=250x250&chl=otpauth%3A%2F%2Ftotp%2Falice%40google.com%3Fsecret%3DJBSWY3DPEHPK3PXP
|
|
80
80
|
|
|
81
81
|
Now run the following and compare the output::
|
|
82
82
|
|
|
83
83
|
import pyotp
|
|
84
84
|
totp = pyotp.TOTP("JBSWY3DPEHPK3PXP")
|
|
85
|
-
print
|
|
85
|
+
print("Current OTP:", totp.now())
|
|
86
86
|
|
|
87
87
|
Links
|
|
88
88
|
~~~~~
|
|
@@ -98,12 +98,10 @@ Links
|
|
|
98
98
|
|
|
99
99
|
.. image:: https://img.shields.io/travis/pyotp/pyotp.svg
|
|
100
100
|
:target: https://travis-ci.org/pyotp/pyotp
|
|
101
|
-
.. image:: https://img.shields.io/
|
|
102
|
-
:target: https://
|
|
101
|
+
.. image:: https://img.shields.io/codecov/c/github/pyotp/pyotp/master.svg
|
|
102
|
+
:target: https://codecov.io/github/pyotp/pyotp?branch=master
|
|
103
103
|
.. image:: https://img.shields.io/pypi/v/pyotp.svg
|
|
104
104
|
:target: https://pypi.python.org/pypi/pyotp
|
|
105
|
-
.. image:: https://img.shields.io/pypi/dm/pyotp.svg
|
|
106
|
-
:target: https://pypi.python.org/pypi/pyotp
|
|
107
105
|
.. image:: https://img.shields.io/pypi/l/pyotp.svg
|
|
108
106
|
:target: https://pypi.python.org/pypi/pyotp
|
|
109
107
|
.. image:: https://readthedocs.org/projects/pyotp/badge/?version=latest
|
|
File without changes
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
|
|
3
|
-
import os
|
|
4
|
-
from setuptools import setup
|
|
3
|
+
import os
|
|
4
|
+
from setuptools import setup
|
|
5
5
|
|
|
6
6
|
install_requires = [line.rstrip() for line in open(os.path.join(os.path.dirname(__file__), "requirements.txt"))]
|
|
7
7
|
|
|
8
8
|
setup(
|
|
9
9
|
name='pyotp',
|
|
10
|
-
version='2.
|
|
10
|
+
version='2.2.1',
|
|
11
11
|
url='https://github.com/pyotp/pyotp',
|
|
12
12
|
license='BSD License',
|
|
13
13
|
author='PyOTP contributors',
|
|
@@ -19,6 +19,7 @@ setup(
|
|
|
19
19
|
package_dir={'': 'src'},
|
|
20
20
|
platforms=['MacOS X', 'Posix'],
|
|
21
21
|
zip_safe=False,
|
|
22
|
+
test_suite="test",
|
|
22
23
|
classifiers=[
|
|
23
24
|
'Intended Audience :: Developers',
|
|
24
25
|
'License :: OSI Approved :: BSD License',
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import (absolute_import, division,
|
|
2
|
+
print_function, unicode_literals)
|
|
3
|
+
|
|
4
|
+
import random as _random
|
|
5
|
+
|
|
6
|
+
from pyotp.hotp import HOTP # noqa
|
|
7
|
+
from pyotp.otp import OTP # noqa
|
|
8
|
+
from pyotp.totp import TOTP # noqa
|
|
9
|
+
from . import utils # noqa
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def random_base32(length=16, random=_random.SystemRandom(),
|
|
13
|
+
chars=list('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')):
|
|
14
|
+
return ''.join(
|
|
15
|
+
random.choice(chars)
|
|
16
|
+
for _ in range(length)
|
|
17
|
+
)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
from __future__ import
|
|
1
|
+
from __future__ import absolute_import, division, print_function, unicode_literals
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
from
|
|
5
|
-
from
|
|
3
|
+
from . import utils
|
|
4
|
+
from .otp import OTP
|
|
5
|
+
from .compat import str
|
|
6
6
|
|
|
7
7
|
class HOTP(OTP):
|
|
8
8
|
def at(self, count):
|
|
@@ -37,4 +37,6 @@ class HOTP(OTP):
|
|
|
37
37
|
name,
|
|
38
38
|
initial_count=initial_count,
|
|
39
39
|
issuer_name=issuer_name,
|
|
40
|
+
algorithm=self.digest().name,
|
|
41
|
+
digits=self.digits
|
|
40
42
|
)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
from __future__ import
|
|
1
|
+
from __future__ import absolute_import, division, print_function, unicode_literals
|
|
2
2
|
|
|
3
3
|
import base64
|
|
4
4
|
import hashlib
|
|
5
5
|
import hmac
|
|
6
|
-
from
|
|
6
|
+
from .compat import str
|
|
7
7
|
|
|
8
8
|
class OTP(object):
|
|
9
9
|
def __init__(self, s, digits=6, digest=hashlib.sha1):
|
|
@@ -27,13 +27,8 @@ class OTP(object):
|
|
|
27
27
|
Usually either the counter, or the computed integer
|
|
28
28
|
based on the Unix timestamp
|
|
29
29
|
"""
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
self.int_to_bytestring(input),
|
|
33
|
-
self.digest,
|
|
34
|
-
).digest()
|
|
35
|
-
|
|
36
|
-
hmac_hash = bytearray(hmac_hash)
|
|
30
|
+
hasher = hmac.new(self.byte_secret(), self.int_to_bytestring(input), self.digest)
|
|
31
|
+
hmac_hash = bytearray(hasher.digest())
|
|
37
32
|
offset = hmac_hash[-1] & 0xf
|
|
38
33
|
code = ((hmac_hash[offset] & 0x7f) << 24 |
|
|
39
34
|
(hmac_hash[offset + 1] & 0xff) << 16 |
|
|
@@ -62,6 +57,7 @@ class OTP(object):
|
|
|
62
57
|
while i != 0:
|
|
63
58
|
result.append(i & 0xFF)
|
|
64
59
|
i >>= 8
|
|
65
|
-
# It's necessary to convert the final result from bytearray to bytes
|
|
66
|
-
# the hmac functions in python 2.6 and 3.3 don't work with
|
|
60
|
+
# It's necessary to convert the final result from bytearray to bytes
|
|
61
|
+
# because the hmac functions in python 2.6 and 3.3 don't work with
|
|
62
|
+
# bytearray
|
|
67
63
|
return bytes(bytearray(reversed(result)).rjust(padding, b'\0'))
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
from __future__ import
|
|
1
|
+
from __future__ import absolute_import, division, print_function, unicode_literals
|
|
2
2
|
|
|
3
3
|
import datetime
|
|
4
4
|
import time
|
|
5
5
|
|
|
6
|
-
from
|
|
7
|
-
from
|
|
8
|
-
from
|
|
6
|
+
from . import utils
|
|
7
|
+
from .otp import OTP
|
|
8
|
+
from .compat import str
|
|
9
9
|
|
|
10
10
|
class TOTP(OTP):
|
|
11
11
|
def __init__(self, *args, **kwargs):
|
|
@@ -19,9 +19,9 @@ class TOTP(OTP):
|
|
|
19
19
|
def at(self, for_time, counter_offset=0):
|
|
20
20
|
"""
|
|
21
21
|
Accepts either a Unix timestamp integer or a Time object.
|
|
22
|
-
Time objects will be adjusted to UTC automatically
|
|
23
22
|
@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
|
|
23
|
+
@param [Integer] counter_offset an amount of ticks to add to the time
|
|
24
|
+
counter
|
|
25
25
|
"""
|
|
26
26
|
if not isinstance(for_time, datetime.datetime):
|
|
27
27
|
for_time = datetime.datetime.fromtimestamp(int(for_time))
|
|
@@ -38,11 +38,12 @@ class TOTP(OTP):
|
|
|
38
38
|
"""
|
|
39
39
|
Verifies the OTP passed in against the current time OTP
|
|
40
40
|
@param [String/Integer] otp the OTP to check against
|
|
41
|
-
@param [Integer] valid_window extends the validity to this many counter
|
|
41
|
+
@param [Integer] valid_window extends the validity to this many counter
|
|
42
|
+
ticks before and after the current one
|
|
42
43
|
"""
|
|
43
44
|
if for_time is None:
|
|
44
45
|
for_time = datetime.datetime.now()
|
|
45
|
-
|
|
46
|
+
|
|
46
47
|
if valid_window:
|
|
47
48
|
for i in range(-valid_window, valid_window + 1):
|
|
48
49
|
if utils.strings_equal(str(otp), str(self.at(for_time, i))):
|
|
@@ -59,7 +60,9 @@ class TOTP(OTP):
|
|
|
59
60
|
@param [String] name of the account
|
|
60
61
|
@return [String] provisioning uri
|
|
61
62
|
"""
|
|
62
|
-
return utils.build_uri(self.secret, name, issuer_name=issuer_name
|
|
63
|
+
return utils.build_uri(self.secret, name, issuer_name=issuer_name,
|
|
64
|
+
algorithm=self.digest().name,
|
|
65
|
+
digits=self.digits, period=self.interval)
|
|
63
66
|
|
|
64
67
|
def timecode(self, for_time):
|
|
65
68
|
i = time.mktime(for_time.timetuple())
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from __future__ import absolute_import, division, print_function, unicode_literals
|
|
2
|
+
|
|
3
|
+
import unicodedata
|
|
4
|
+
try:
|
|
5
|
+
from itertools import izip_longest
|
|
6
|
+
except ImportError:
|
|
7
|
+
from itertools import zip_longest as izip_longest
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from urllib.parse import quote, urlencode
|
|
11
|
+
except ImportError:
|
|
12
|
+
from urllib import quote, urlencode
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_uri(secret, name, initial_count=None, issuer_name=None,
|
|
16
|
+
algorithm=None, digits=None, period=None):
|
|
17
|
+
"""
|
|
18
|
+
Returns the provisioning URI for the OTP; works for either TOTP or HOTP.
|
|
19
|
+
|
|
20
|
+
This can then be encoded in a QR Code and used to provision the Google
|
|
21
|
+
Authenticator app.
|
|
22
|
+
|
|
23
|
+
For module-internal use.
|
|
24
|
+
|
|
25
|
+
See also:
|
|
26
|
+
https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
|
27
|
+
|
|
28
|
+
@param [String] the hotp/totp secret used to generate the URI
|
|
29
|
+
@param [String] name of the account
|
|
30
|
+
@param [Integer] initial_count starting counter value, defaults to None.
|
|
31
|
+
If none, the OTP type will be assumed as TOTP.
|
|
32
|
+
@param [String] the name of the OTP issuer; this will be the
|
|
33
|
+
organization title of the OTP entry in Authenticator
|
|
34
|
+
@param [String] the algorithm used in the OTP generation.
|
|
35
|
+
@param [Integer] the length of the OTP generated code.
|
|
36
|
+
@param [Integer] the number of seconds the OTP generator is set to
|
|
37
|
+
expire every code.
|
|
38
|
+
@return [String] provisioning uri
|
|
39
|
+
"""
|
|
40
|
+
# initial_count may be 0 as a valid param
|
|
41
|
+
is_initial_count_present = (initial_count is not None)
|
|
42
|
+
|
|
43
|
+
# Handling values different from defaults
|
|
44
|
+
is_algorithm_set = (algorithm is not None and algorithm != 'sha1')
|
|
45
|
+
is_digits_set = (digits is not None and digits != 6)
|
|
46
|
+
is_period_set = (period is not None and period != 30)
|
|
47
|
+
|
|
48
|
+
otp_type = 'hotp' if is_initial_count_present else 'totp'
|
|
49
|
+
base_uri = 'otpauth://{}/{}?{}'
|
|
50
|
+
|
|
51
|
+
url_args = {'secret': secret}
|
|
52
|
+
|
|
53
|
+
label = quote(name)
|
|
54
|
+
if issuer_name is not None:
|
|
55
|
+
label = quote(issuer_name) + ':' + label
|
|
56
|
+
url_args['issuer'] = issuer_name
|
|
57
|
+
|
|
58
|
+
if is_initial_count_present:
|
|
59
|
+
url_args['counter'] = initial_count
|
|
60
|
+
if is_algorithm_set:
|
|
61
|
+
url_args['algorithm'] = algorithm.upper()
|
|
62
|
+
if is_digits_set:
|
|
63
|
+
url_args['digits'] = digits
|
|
64
|
+
if is_period_set:
|
|
65
|
+
url_args['period'] = period
|
|
66
|
+
|
|
67
|
+
uri = base_uri.format(otp_type, label, urlencode(url_args))
|
|
68
|
+
return uri
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _compare_digest(s1, s2):
|
|
72
|
+
differences = 0
|
|
73
|
+
for c1, c2 in izip_longest(s1, s2):
|
|
74
|
+
if c1 is None or c2 is None:
|
|
75
|
+
differences = 1
|
|
76
|
+
continue
|
|
77
|
+
differences |= ord(c1) ^ ord(c2)
|
|
78
|
+
return differences == 0
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
# Python 3.3+ and 2.7.7+ include a timing-attack-resistant
|
|
82
|
+
# comparison function, which is probably more reliable than ours.
|
|
83
|
+
# Use it if available.
|
|
84
|
+
from hmac import compare_digest
|
|
85
|
+
|
|
86
|
+
except ImportError:
|
|
87
|
+
compare_digest = _compare_digest
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def strings_equal(s1, s2):
|
|
91
|
+
"""
|
|
92
|
+
Timing-attack resistant string comparison.
|
|
93
|
+
|
|
94
|
+
Normal comparison using == will short-circuit on the first mismatching
|
|
95
|
+
character. This avoids that by scanning the whole string, though we
|
|
96
|
+
still reveal to a timing attack whether the strings are the same
|
|
97
|
+
length.
|
|
98
|
+
"""
|
|
99
|
+
s1 = unicodedata.normalize('NFKC', s1)
|
|
100
|
+
s2 = unicodedata.normalize('NFKC', s2)
|
|
101
|
+
return compare_digest(s1, s2)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 1.1
|
|
2
2
|
Name: pyotp
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.1
|
|
4
4
|
Summary: Python One Time Password Library
|
|
5
5
|
Home-page: https://github.com/pyotp/pyotp
|
|
6
6
|
Author: PyOTP contributors
|
|
@@ -84,13 +84,13 @@ Description: PyOTP - The Python One-Time Password Library
|
|
|
84
84
|
|
|
85
85
|
Scan the following barcode with your phone, using Google Authenticator
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
.. image:: http://chart.apis.google.com/chart?cht=qr&chs=250x250&chl=otpauth%3A%2F%2Ftotp%2Falice%40google.com%3Fsecret%3DJBSWY3DPEHPK3PXP
|
|
88
88
|
|
|
89
89
|
Now run the following and compare the output::
|
|
90
90
|
|
|
91
91
|
import pyotp
|
|
92
92
|
totp = pyotp.TOTP("JBSWY3DPEHPK3PXP")
|
|
93
|
-
print
|
|
93
|
+
print("Current OTP:", totp.now())
|
|
94
94
|
|
|
95
95
|
Links
|
|
96
96
|
~~~~~
|
|
@@ -106,12 +106,10 @@ Description: PyOTP - The Python One-Time Password Library
|
|
|
106
106
|
|
|
107
107
|
.. image:: https://img.shields.io/travis/pyotp/pyotp.svg
|
|
108
108
|
:target: https://travis-ci.org/pyotp/pyotp
|
|
109
|
-
.. image:: https://img.shields.io/
|
|
110
|
-
:target: https://
|
|
109
|
+
.. image:: https://img.shields.io/codecov/c/github/pyotp/pyotp/master.svg
|
|
110
|
+
:target: https://codecov.io/github/pyotp/pyotp?branch=master
|
|
111
111
|
.. image:: https://img.shields.io/pypi/v/pyotp.svg
|
|
112
112
|
:target: https://pypi.python.org/pypi/pyotp
|
|
113
|
-
.. image:: https://img.shields.io/pypi/dm/pyotp.svg
|
|
114
|
-
:target: https://pypi.python.org/pypi/pyotp
|
|
115
113
|
.. image:: https://img.shields.io/pypi/l/pyotp.svg
|
|
116
114
|
:target: https://pypi.python.org/pypi/pyotp
|
|
117
115
|
.. image:: https://readthedocs.org/projects/pyotp/badge/?version=latest
|
|
@@ -6,6 +6,7 @@ setup.cfg
|
|
|
6
6
|
setup.py
|
|
7
7
|
test.py
|
|
8
8
|
src/pyotp/__init__.py
|
|
9
|
+
src/pyotp/compat.py
|
|
9
10
|
src/pyotp/hotp.py
|
|
10
11
|
src/pyotp/otp.py
|
|
11
12
|
src/pyotp/totp.py
|
|
@@ -14,5 +15,4 @@ src/pyotp.egg-info/PKG-INFO
|
|
|
14
15
|
src/pyotp.egg-info/SOURCES.txt
|
|
15
16
|
src/pyotp.egg-info/dependency_links.txt
|
|
16
17
|
src/pyotp.egg-info/not-zip-safe
|
|
17
|
-
src/pyotp.egg-info/requires.txt
|
|
18
18
|
src/pyotp.egg-info/top_level.txt
|
pyotp-2.2.1/test.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# coding: utf-8
|
|
3
|
+
|
|
4
|
+
from __future__ import absolute_import, division, print_function, unicode_literals
|
|
5
|
+
|
|
6
|
+
import base64
|
|
7
|
+
import datetime
|
|
8
|
+
import hashlib
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import unittest
|
|
12
|
+
try:
|
|
13
|
+
from urllib.parse import urlparse, parse_qsl
|
|
14
|
+
except ImportError:
|
|
15
|
+
from urlparse import urlparse, parse_qsl
|
|
16
|
+
|
|
17
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
|
18
|
+
import pyotp # noqa
|
|
19
|
+
|
|
20
|
+
class HOTPExampleValuesFromTheRFC(unittest.TestCase):
|
|
21
|
+
def test_match_rfc(self):
|
|
22
|
+
# 12345678901234567890 in Bas32
|
|
23
|
+
# GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ
|
|
24
|
+
hotp = pyotp.HOTP('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ')
|
|
25
|
+
self.assertEqual(hotp.at(0), '755224')
|
|
26
|
+
self.assertEqual(hotp.at(1), '287082')
|
|
27
|
+
self.assertEqual(hotp.at(2), '359152')
|
|
28
|
+
self.assertEqual(hotp.at(3), '969429')
|
|
29
|
+
self.assertEqual(hotp.at(4), '338314')
|
|
30
|
+
self.assertEqual(hotp.at(5), '254676')
|
|
31
|
+
self.assertEqual(hotp.at(6), '287922')
|
|
32
|
+
self.assertEqual(hotp.at(7), '162583')
|
|
33
|
+
self.assertEqual(hotp.at(8), '399871')
|
|
34
|
+
self.assertEqual(hotp.at(9), '520489')
|
|
35
|
+
|
|
36
|
+
def test_verify_otp_reuse(self):
|
|
37
|
+
hotp = pyotp.HOTP('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ')
|
|
38
|
+
self.assertTrue(hotp.verify('520489', 9))
|
|
39
|
+
self.assertFalse(hotp.verify('520489', 10))
|
|
40
|
+
self.assertFalse(hotp.verify('520489', 10))
|
|
41
|
+
|
|
42
|
+
def test_provisioning_uri(self):
|
|
43
|
+
hotp = pyotp.HOTP('wrn3pqx5uqxqvnqr')
|
|
44
|
+
|
|
45
|
+
url = urlparse(
|
|
46
|
+
hotp.provisioning_uri('mark@percival'))
|
|
47
|
+
self.assertEqual(url.scheme, 'otpauth')
|
|
48
|
+
self.assertEqual(url.netloc, 'hotp')
|
|
49
|
+
self.assertEqual(url.path, '/mark%40percival')
|
|
50
|
+
self.assertEqual(dict(parse_qsl(url.query)),
|
|
51
|
+
{'secret': 'wrn3pqx5uqxqvnqr', 'counter': '0'})
|
|
52
|
+
|
|
53
|
+
url = urlparse(
|
|
54
|
+
hotp.provisioning_uri('mark@percival', initial_count=12))
|
|
55
|
+
self.assertEqual(url.scheme, 'otpauth')
|
|
56
|
+
self.assertEqual(url.netloc, 'hotp')
|
|
57
|
+
self.assertEqual(url.path, '/mark%40percival')
|
|
58
|
+
self.assertEqual(dict(parse_qsl(url.query)),
|
|
59
|
+
{'secret': 'wrn3pqx5uqxqvnqr', 'counter': '12'})
|
|
60
|
+
|
|
61
|
+
url = urlparse(
|
|
62
|
+
hotp.provisioning_uri('mark@percival', issuer_name='FooCorp!'))
|
|
63
|
+
self.assertEqual(url.scheme, 'otpauth')
|
|
64
|
+
self.assertEqual(url.netloc, 'hotp')
|
|
65
|
+
self.assertEqual(url.path, '/FooCorp%21:mark%40percival')
|
|
66
|
+
self.assertEqual(dict(parse_qsl(url.query)),
|
|
67
|
+
{'secret': 'wrn3pqx5uqxqvnqr', 'counter': '0',
|
|
68
|
+
'issuer': 'FooCorp!'})
|
|
69
|
+
|
|
70
|
+
key = 'c7uxuqhgflpw7oruedmglbrk7u6242vb'
|
|
71
|
+
hotp = pyotp.HOTP(key, digits=8, digest=hashlib.sha256)
|
|
72
|
+
url = urlparse(
|
|
73
|
+
hotp.provisioning_uri('baco@peperina', issuer_name='FooCorp'))
|
|
74
|
+
self.assertEqual(url.scheme, 'otpauth')
|
|
75
|
+
self.assertEqual(url.netloc, 'hotp')
|
|
76
|
+
self.assertEqual(url.path, '/FooCorp:baco%40peperina')
|
|
77
|
+
self.assertEqual(dict(parse_qsl(url.query)),
|
|
78
|
+
{'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb',
|
|
79
|
+
'counter': '0', 'issuer': 'FooCorp',
|
|
80
|
+
'digits': '8', 'algorithm': 'SHA256'})
|
|
81
|
+
|
|
82
|
+
hotp = pyotp.HOTP(key, digits=8)
|
|
83
|
+
url = urlparse(
|
|
84
|
+
hotp.provisioning_uri('baco@peperina', issuer_name='FooCorp',
|
|
85
|
+
initial_count=10))
|
|
86
|
+
self.assertEqual(url.scheme, 'otpauth')
|
|
87
|
+
self.assertEqual(url.netloc, 'hotp')
|
|
88
|
+
self.assertEqual(url.path, '/FooCorp:baco%40peperina')
|
|
89
|
+
self.assertEqual(dict(parse_qsl(url.query)),
|
|
90
|
+
{'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb',
|
|
91
|
+
'counter': '10', 'issuer': 'FooCorp',
|
|
92
|
+
'digits': '8'})
|
|
93
|
+
|
|
94
|
+
def test_other_secret(self):
|
|
95
|
+
hotp = pyotp.HOTP(
|
|
96
|
+
'N3OVNIBRERIO5OHGVCMDGS4V4RJ3AUZOUN34J6FRM4P6JIFCG3ZA')
|
|
97
|
+
self.assertEqual(hotp.at(0), '737863')
|
|
98
|
+
self.assertEqual(hotp.at(1), '390601')
|
|
99
|
+
self.assertEqual(hotp.at(2), '363354')
|
|
100
|
+
self.assertEqual(hotp.at(3), '936780')
|
|
101
|
+
self.assertEqual(hotp.at(4), '654019')
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class TOTPExampleValuesFromTheRFC(unittest.TestCase):
|
|
105
|
+
RFC_VALUES = {
|
|
106
|
+
(hashlib.sha1, b'12345678901234567890'): (
|
|
107
|
+
(59, '94287082'),
|
|
108
|
+
(1111111109, '07081804'),
|
|
109
|
+
(1111111111, '14050471'),
|
|
110
|
+
(1234567890, '89005924'),
|
|
111
|
+
(2000000000, '69279037'),
|
|
112
|
+
(20000000000, '65353130'),
|
|
113
|
+
),
|
|
114
|
+
|
|
115
|
+
(hashlib.sha256, b'12345678901234567890123456789012'): (
|
|
116
|
+
(59, 46119246),
|
|
117
|
+
(1111111109, '68084774'),
|
|
118
|
+
(1111111111, '67062674'),
|
|
119
|
+
(1234567890, '91819424'),
|
|
120
|
+
(2000000000, '90698825'),
|
|
121
|
+
(20000000000, '77737706'),
|
|
122
|
+
),
|
|
123
|
+
|
|
124
|
+
(hashlib.sha512,
|
|
125
|
+
b'1234567890123456789012345678901234567890123456789012345678901234'):
|
|
126
|
+
(
|
|
127
|
+
(59, 90693936),
|
|
128
|
+
(1111111109, '25091201'),
|
|
129
|
+
(1111111111, '99943326'),
|
|
130
|
+
(1234567890, '93441116'),
|
|
131
|
+
(2000000000, '38618901'),
|
|
132
|
+
(20000000000, '47863826'),
|
|
133
|
+
),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
def test_match_rfc(self):
|
|
137
|
+
for digest, secret in self.RFC_VALUES:
|
|
138
|
+
totp = pyotp.TOTP(base64.b32encode(secret), 8, digest)
|
|
139
|
+
for utime, code in self.RFC_VALUES[(digest, secret)]:
|
|
140
|
+
# 32-bit platforms use native functions to handle timestamps, so they fail this test
|
|
141
|
+
# (and will fail after 19 January 2038)
|
|
142
|
+
if utime > sys.maxsize:
|
|
143
|
+
continue
|
|
144
|
+
value = totp.at(utime)
|
|
145
|
+
msg = "%s != %s (%s, time=%d)"
|
|
146
|
+
msg %= (value, code, digest().name, utime)
|
|
147
|
+
self.assertEqual(value, str(code), msg)
|
|
148
|
+
|
|
149
|
+
def test_match_rfc_digit_length(self):
|
|
150
|
+
totp = pyotp.TOTP('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ')
|
|
151
|
+
self.assertEqual(totp.at(1111111111), '050471')
|
|
152
|
+
self.assertEqual(totp.at(1234567890), '005924')
|
|
153
|
+
self.assertEqual(totp.at(2000000000), '279037')
|
|
154
|
+
|
|
155
|
+
def test_match_google_authenticator_output(self):
|
|
156
|
+
totp = pyotp.TOTP('wrn3pqx5uqxqvnqr')
|
|
157
|
+
with Timecop(1297553958):
|
|
158
|
+
self.assertEqual(totp.now(), '102705')
|
|
159
|
+
|
|
160
|
+
def test_validate_totp(self):
|
|
161
|
+
totp = pyotp.TOTP('wrn3pqx5uqxqvnqr')
|
|
162
|
+
with Timecop(1297553958):
|
|
163
|
+
self.assertTrue(totp.verify('102705'))
|
|
164
|
+
self.assertTrue(totp.verify('102705'))
|
|
165
|
+
with Timecop(1297553958 + 30):
|
|
166
|
+
self.assertFalse(totp.verify('102705'))
|
|
167
|
+
|
|
168
|
+
def test_validate_totp_with_digit_length(self):
|
|
169
|
+
totp = pyotp.TOTP('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ')
|
|
170
|
+
with Timecop(1111111111):
|
|
171
|
+
self.assertTrue(totp.verify('050471'))
|
|
172
|
+
with Timecop(1297553958 + 30):
|
|
173
|
+
self.assertFalse(totp.verify('050471'))
|
|
174
|
+
|
|
175
|
+
def test_provisioning_uri(self):
|
|
176
|
+
totp = pyotp.TOTP('wrn3pqx5uqxqvnqr')
|
|
177
|
+
url = urlparse(
|
|
178
|
+
totp.provisioning_uri('mark@percival'))
|
|
179
|
+
self.assertEqual(url.scheme, 'otpauth')
|
|
180
|
+
self.assertEqual(url.netloc, 'totp')
|
|
181
|
+
self.assertEqual(url.path, '/mark%40percival')
|
|
182
|
+
self.assertEqual(dict(parse_qsl(url.query)),
|
|
183
|
+
{'secret': 'wrn3pqx5uqxqvnqr'})
|
|
184
|
+
|
|
185
|
+
url = urlparse(
|
|
186
|
+
totp.provisioning_uri('mark@percival', issuer_name='FooCorp!'))
|
|
187
|
+
self.assertEqual(url.scheme, 'otpauth')
|
|
188
|
+
self.assertEqual(url.netloc, 'totp')
|
|
189
|
+
self.assertEqual(url.path, '/FooCorp%21:mark%40percival')
|
|
190
|
+
self.assertEqual(dict(parse_qsl(url.query)),
|
|
191
|
+
{'secret': 'wrn3pqx5uqxqvnqr',
|
|
192
|
+
'issuer': 'FooCorp!'})
|
|
193
|
+
|
|
194
|
+
key = 'c7uxuqhgflpw7oruedmglbrk7u6242vb'
|
|
195
|
+
totp = pyotp.TOTP(key, digits=8, interval=60, digest=hashlib.sha256)
|
|
196
|
+
url = urlparse(totp.provisioning_uri('baco@peperina', issuer_name='FooCorp'))
|
|
197
|
+
self.assertEqual(url.scheme, 'otpauth')
|
|
198
|
+
self.assertEqual(url.netloc, 'totp')
|
|
199
|
+
self.assertEqual(url.path, '/FooCorp:baco%40peperina')
|
|
200
|
+
self.assertEqual(dict(parse_qsl(url.query)),
|
|
201
|
+
{'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb',
|
|
202
|
+
'issuer': 'FooCorp',
|
|
203
|
+
'digits': '8', 'period': '60',
|
|
204
|
+
'algorithm': 'SHA256'})
|
|
205
|
+
|
|
206
|
+
totp = pyotp.TOTP(key, digits=8, interval=60)
|
|
207
|
+
url = urlparse(totp.provisioning_uri('baco@peperina', issuer_name='FooCorp'))
|
|
208
|
+
self.assertEqual(url.scheme, 'otpauth')
|
|
209
|
+
self.assertEqual(url.netloc, 'totp')
|
|
210
|
+
self.assertEqual(url.path, '/FooCorp:baco%40peperina')
|
|
211
|
+
self.assertEqual(dict(parse_qsl(url.query)),
|
|
212
|
+
{'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb',
|
|
213
|
+
'issuer': 'FooCorp',
|
|
214
|
+
'digits': '8', 'period': '60'})
|
|
215
|
+
|
|
216
|
+
totp = pyotp.TOTP(key, digits=8)
|
|
217
|
+
url = urlparse(totp.provisioning_uri('baco@peperina', issuer_name='FooCorp'))
|
|
218
|
+
self.assertEqual(url.scheme, 'otpauth')
|
|
219
|
+
self.assertEqual(url.netloc, 'totp')
|
|
220
|
+
self.assertEqual(url.path, '/FooCorp:baco%40peperina')
|
|
221
|
+
self.assertEqual(dict(parse_qsl(url.query)),
|
|
222
|
+
{'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb',
|
|
223
|
+
'issuer': 'FooCorp',
|
|
224
|
+
'digits': '8'})
|
|
225
|
+
|
|
226
|
+
def test_random_key_generation(self):
|
|
227
|
+
self.assertEqual(len(pyotp.random_base32()), 16)
|
|
228
|
+
self.assertEqual(len(pyotp.random_base32(length=20)), 20)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class CompareDigestTest(unittest.TestCase):
|
|
232
|
+
method = staticmethod(pyotp.utils.compare_digest)
|
|
233
|
+
|
|
234
|
+
def test_comparisons(self):
|
|
235
|
+
self.assertTrue(self.method("", ""))
|
|
236
|
+
self.assertTrue(self.method("a", "a"))
|
|
237
|
+
self.assertTrue(self.method("a" * 1000, "a" * 1000))
|
|
238
|
+
|
|
239
|
+
self.assertFalse(self.method("", "a"))
|
|
240
|
+
self.assertFalse(self.method("a", ""))
|
|
241
|
+
self.assertFalse(self.method("a" * 999 + "b", "a" * 1000))
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class FallBackCompareDigestTest(CompareDigestTest):
|
|
245
|
+
method = staticmethod(pyotp.utils._compare_digest)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class StringComparisonTest(CompareDigestTest):
|
|
249
|
+
method = staticmethod(pyotp.utils.strings_equal)
|
|
250
|
+
|
|
251
|
+
def test_fullwidth_input(self):
|
|
252
|
+
self.assertTrue(self.method("xs12345", "xs12345"))
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class CounterOffsetTest(unittest.TestCase):
|
|
256
|
+
def test_counter_offset(self):
|
|
257
|
+
totp = pyotp.TOTP("ABCDEFGH")
|
|
258
|
+
self.assertEqual(totp.at(200), "028307")
|
|
259
|
+
self.assertTrue(totp.at(200, 1), "681610")
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class ValidWindowTest(unittest.TestCase):
|
|
263
|
+
def test_valid_window(self):
|
|
264
|
+
totp = pyotp.TOTP("ABCDEFGH")
|
|
265
|
+
self.assertTrue(totp.verify("451564", 200, 1))
|
|
266
|
+
self.assertTrue(totp.verify("028307", 200, 1))
|
|
267
|
+
self.assertTrue(totp.verify("681610", 200, 1))
|
|
268
|
+
self.assertFalse(totp.verify("195979", 200, 1))
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class Timecop(object):
|
|
272
|
+
"""
|
|
273
|
+
Half-assed clone of timecop.rb, just enough to pass our tests.
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
def __init__(self, freeze_timestamp):
|
|
277
|
+
self.freeze_timestamp = freeze_timestamp
|
|
278
|
+
|
|
279
|
+
def __enter__(self):
|
|
280
|
+
self.real_datetime = datetime.datetime
|
|
281
|
+
datetime.datetime = self.frozen_datetime()
|
|
282
|
+
|
|
283
|
+
def __exit__(self, type, value, traceback):
|
|
284
|
+
datetime.datetime = self.real_datetime
|
|
285
|
+
|
|
286
|
+
def frozen_datetime(self):
|
|
287
|
+
class FrozenDateTime(datetime.datetime):
|
|
288
|
+
@classmethod
|
|
289
|
+
def now(cls, **kwargs):
|
|
290
|
+
return cls.fromtimestamp(timecop.freeze_timestamp)
|
|
291
|
+
|
|
292
|
+
timecop = self
|
|
293
|
+
return FrozenDateTime
|
|
294
|
+
|
|
295
|
+
if __name__ == '__main__':
|
|
296
|
+
unittest.main()
|
pyotp-2.1.1/requirements.txt
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
future==0.15.2
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
from __future__ import print_function, unicode_literals, division, absolute_import
|
|
2
|
-
|
|
3
|
-
import random as _random
|
|
4
|
-
|
|
5
|
-
from pyotp.hotp import HOTP
|
|
6
|
-
from pyotp.otp import OTP
|
|
7
|
-
from pyotp.totp import TOTP
|
|
8
|
-
from . import utils
|
|
9
|
-
|
|
10
|
-
def random_base32(length=16, random=_random.SystemRandom(),
|
|
11
|
-
chars=list('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')):
|
|
12
|
-
return ''.join(
|
|
13
|
-
random.choice(chars)
|
|
14
|
-
for _ in range(length)
|
|
15
|
-
)
|
pyotp-2.1.1/src/pyotp/utils.py
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
from __future__ import print_function, unicode_literals, division, absolute_import
|
|
2
|
-
|
|
3
|
-
import unicodedata
|
|
4
|
-
|
|
5
|
-
try:
|
|
6
|
-
from urllib.parse import quote
|
|
7
|
-
except ImportError:
|
|
8
|
-
from urllib import quote
|
|
9
|
-
|
|
10
|
-
def build_uri(secret, name, initial_count=None, issuer_name=None):
|
|
11
|
-
"""
|
|
12
|
-
Returns the provisioning URI for the OTP; works for either TOTP or HOTP.
|
|
13
|
-
|
|
14
|
-
This can then be encoded in a QR Code and used to provision the Google
|
|
15
|
-
Authenticator app.
|
|
16
|
-
|
|
17
|
-
For module-internal use.
|
|
18
|
-
|
|
19
|
-
See also:
|
|
20
|
-
http://code.google.com/p/google-authenticator/wiki/KeyUriFormat
|
|
21
|
-
|
|
22
|
-
@param [String] the hotp/totp secret used to generate the URI
|
|
23
|
-
@param [String] name of the account
|
|
24
|
-
@param [Integer] initial_count starting counter value, defaults to None.
|
|
25
|
-
If none, the OTP type will be assumed as TOTP.
|
|
26
|
-
@param [String] the name of the OTP issuer; this will be the
|
|
27
|
-
organization title of the OTP entry in Authenticator
|
|
28
|
-
@return [String] provisioning uri
|
|
29
|
-
"""
|
|
30
|
-
# initial_count may be 0 as a valid param
|
|
31
|
-
is_initial_count_present = (initial_count is not None)
|
|
32
|
-
|
|
33
|
-
otp_type = 'hotp' if is_initial_count_present else 'totp'
|
|
34
|
-
base = 'otpauth://%s/' % otp_type
|
|
35
|
-
|
|
36
|
-
if issuer_name:
|
|
37
|
-
issuer_name = quote(issuer_name)
|
|
38
|
-
base += '%s:' % issuer_name
|
|
39
|
-
|
|
40
|
-
uri = '%(base)s%(name)s?secret=%(secret)s' % {
|
|
41
|
-
'name': quote(name, safe='@'),
|
|
42
|
-
'secret': secret,
|
|
43
|
-
'base': base,
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if is_initial_count_present:
|
|
47
|
-
uri += '&counter=%s' % initial_count
|
|
48
|
-
|
|
49
|
-
if issuer_name:
|
|
50
|
-
uri += '&issuer=%s' % issuer_name
|
|
51
|
-
|
|
52
|
-
return uri
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def strings_equal(s1, s2):
|
|
56
|
-
"""
|
|
57
|
-
Timing-attack resistant string comparison.
|
|
58
|
-
|
|
59
|
-
Normal comparison using == will short-circuit on the first mismatching
|
|
60
|
-
character. This avoids that by scanning the whole string, though we
|
|
61
|
-
still reveal to a timing attack whether the strings are the same
|
|
62
|
-
length.
|
|
63
|
-
"""
|
|
64
|
-
s1 = unicodedata.normalize('NFKC', s1)
|
|
65
|
-
s2 = unicodedata.normalize('NFKC', s2)
|
|
66
|
-
try:
|
|
67
|
-
# Python 3.3+ and 2.7.7+ include a timing-attack-resistant
|
|
68
|
-
# comparison function, which is probably more reliable than ours.
|
|
69
|
-
# Use it if available.
|
|
70
|
-
from hmac import compare_digest
|
|
71
|
-
|
|
72
|
-
return compare_digest(s1, s2)
|
|
73
|
-
except ImportError:
|
|
74
|
-
pass
|
|
75
|
-
|
|
76
|
-
if len(s1) != len(s2):
|
|
77
|
-
return False
|
|
78
|
-
|
|
79
|
-
differences = 0
|
|
80
|
-
for c1, c2 in zip(s1, s2):
|
|
81
|
-
differences |= ord(c1) ^ ord(c2)
|
|
82
|
-
return differences == 0
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
future==0.15.2
|
pyotp-2.1.1/test.py
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python
|
|
2
|
-
# coding: utf-8
|
|
3
|
-
|
|
4
|
-
from __future__ import print_function, unicode_literals, division, absolute_import
|
|
5
|
-
|
|
6
|
-
import base64
|
|
7
|
-
import datetime
|
|
8
|
-
import hashlib
|
|
9
|
-
import os
|
|
10
|
-
import sys
|
|
11
|
-
import unittest
|
|
12
|
-
|
|
13
|
-
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
|
14
|
-
import pyotp
|
|
15
|
-
|
|
16
|
-
class HOTPExampleValuesFromTheRFC(unittest.TestCase):
|
|
17
|
-
def test_match_rfc(self):
|
|
18
|
-
# 12345678901234567890 in Bas32
|
|
19
|
-
# GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ
|
|
20
|
-
hotp = pyotp.HOTP('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ')
|
|
21
|
-
self.assertEqual(hotp.at(0), '755224')
|
|
22
|
-
self.assertEqual(hotp.at(1), '287082')
|
|
23
|
-
self.assertEqual(hotp.at(2), '359152')
|
|
24
|
-
self.assertEqual(hotp.at(3), '969429')
|
|
25
|
-
self.assertEqual(hotp.at(4), '338314')
|
|
26
|
-
self.assertEqual(hotp.at(5), '254676')
|
|
27
|
-
self.assertEqual(hotp.at(6), '287922')
|
|
28
|
-
self.assertEqual(hotp.at(7), '162583')
|
|
29
|
-
self.assertEqual(hotp.at(8), '399871')
|
|
30
|
-
self.assertEqual(hotp.at(9), '520489')
|
|
31
|
-
|
|
32
|
-
def test_verify_otp_reuse(self):
|
|
33
|
-
hotp = pyotp.HOTP('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ')
|
|
34
|
-
self.assertTrue(hotp.verify('520489', 9))
|
|
35
|
-
self.assertFalse(hotp.verify('520489', 10))
|
|
36
|
-
self.assertFalse(hotp.verify('520489', 10))
|
|
37
|
-
|
|
38
|
-
def test_provisioning_uri(self):
|
|
39
|
-
hotp = pyotp.HOTP('wrn3pqx5uqxqvnqr')
|
|
40
|
-
|
|
41
|
-
self.assertEqual(
|
|
42
|
-
hotp.provisioning_uri('mark@percival'),
|
|
43
|
-
'otpauth://hotp/mark@percival?secret=wrn3pqx5uqxqvnqr&counter=0')
|
|
44
|
-
|
|
45
|
-
self.assertEqual(
|
|
46
|
-
hotp.provisioning_uri('mark@percival', initial_count=12),
|
|
47
|
-
'otpauth://hotp/mark@percival?secret=wrn3pqx5uqxqvnqr&counter=12')
|
|
48
|
-
|
|
49
|
-
self.assertEqual(
|
|
50
|
-
hotp.provisioning_uri('mark@percival', issuer_name='FooCorp!'),
|
|
51
|
-
'otpauth://hotp/FooCorp%21:mark@percival?secret=wrn3pqx5uqxqvnqr&counter=0&issuer=FooCorp%21')
|
|
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')
|
|
60
|
-
|
|
61
|
-
class TOTPExampleValuesFromTheRFC(unittest.TestCase):
|
|
62
|
-
RFC_VALUES = {
|
|
63
|
-
(hashlib.sha1, b'12345678901234567890'): (
|
|
64
|
-
(59, '94287082'),
|
|
65
|
-
(1111111109, '07081804'),
|
|
66
|
-
(1111111111, '14050471'),
|
|
67
|
-
(1234567890, '89005924'),
|
|
68
|
-
(2000000000, '69279037'),
|
|
69
|
-
(20000000000, '65353130'),
|
|
70
|
-
),
|
|
71
|
-
|
|
72
|
-
(hashlib.sha256, b'12345678901234567890123456789012'): (
|
|
73
|
-
(59, 46119246),
|
|
74
|
-
(1111111109, '68084774'),
|
|
75
|
-
(1111111111, '67062674'),
|
|
76
|
-
(1234567890, '91819424'),
|
|
77
|
-
(2000000000, '90698825'),
|
|
78
|
-
(20000000000, '77737706'),
|
|
79
|
-
),
|
|
80
|
-
|
|
81
|
-
(hashlib.sha512, b'1234567890123456789012345678901234567890123456789012345678901234'): (
|
|
82
|
-
(59, 90693936),
|
|
83
|
-
(1111111109, '25091201'),
|
|
84
|
-
(1111111111, '99943326'),
|
|
85
|
-
(1234567890, '93441116'),
|
|
86
|
-
(2000000000, '38618901'),
|
|
87
|
-
(20000000000, '47863826'),
|
|
88
|
-
),
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
def test_match_rfc(self):
|
|
92
|
-
for digest, secret in self.RFC_VALUES:
|
|
93
|
-
totp = pyotp.TOTP(base64.b32encode(secret), 8, digest)
|
|
94
|
-
for utime, code in self.RFC_VALUES[(digest, secret)]:
|
|
95
|
-
value = totp.at(utime)
|
|
96
|
-
msg = "%s != %s (%s, time=%d)"
|
|
97
|
-
msg %= (value, code, digest().name, utime)
|
|
98
|
-
self.assertEqual(value, str(code), msg)
|
|
99
|
-
|
|
100
|
-
def test_match_rfc_digit_length(self):
|
|
101
|
-
totp = pyotp.TOTP('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ')
|
|
102
|
-
self.assertEqual(totp.at(1111111111), '050471')
|
|
103
|
-
self.assertEqual(totp.at(1234567890), '005924')
|
|
104
|
-
self.assertEqual(totp.at(2000000000), '279037')
|
|
105
|
-
|
|
106
|
-
def test_match_google_authenticator_output(self):
|
|
107
|
-
totp = pyotp.TOTP('wrn3pqx5uqxqvnqr')
|
|
108
|
-
with Timecop(1297553958):
|
|
109
|
-
self.assertEqual(totp.now(), '102705')
|
|
110
|
-
|
|
111
|
-
def test_validate_totp(self):
|
|
112
|
-
totp = pyotp.TOTP('wrn3pqx5uqxqvnqr')
|
|
113
|
-
with Timecop(1297553958):
|
|
114
|
-
self.assertTrue(totp.verify('102705'))
|
|
115
|
-
self.assertTrue(totp.verify('102705'))
|
|
116
|
-
with Timecop(1297553958 + 30):
|
|
117
|
-
self.assertFalse(totp.verify('102705'))
|
|
118
|
-
|
|
119
|
-
def test_validate_totp_with_digit_length(self):
|
|
120
|
-
totp = pyotp.TOTP('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ')
|
|
121
|
-
with Timecop(1111111111):
|
|
122
|
-
self.assertTrue(totp.verify('050471'))
|
|
123
|
-
with Timecop(1297553958 + 30):
|
|
124
|
-
self.assertFalse(totp.verify('050471'))
|
|
125
|
-
|
|
126
|
-
def test_provisioning_uri(self):
|
|
127
|
-
totp = pyotp.TOTP('wrn3pqx5uqxqvnqr')
|
|
128
|
-
self.assertEqual(
|
|
129
|
-
totp.provisioning_uri('mark@percival'),
|
|
130
|
-
'otpauth://totp/mark@percival?secret=wrn3pqx5uqxqvnqr')
|
|
131
|
-
|
|
132
|
-
self.assertEqual(
|
|
133
|
-
totp.provisioning_uri('mark@percival', issuer_name='FooCorp!'),
|
|
134
|
-
'otpauth://totp/FooCorp%21:mark@percival?secret=wrn3pqx5uqxqvnqr&issuer=FooCorp%21')
|
|
135
|
-
|
|
136
|
-
def test_random_key_generation(self):
|
|
137
|
-
self.assertEqual(len(pyotp.random_base32()), 16)
|
|
138
|
-
self.assertEqual(len(pyotp.random_base32(length=20)), 20)
|
|
139
|
-
|
|
140
|
-
class StringComparisonTest(unittest.TestCase):
|
|
141
|
-
def test_comparisons(self):
|
|
142
|
-
self.assertTrue(pyotp.utils.strings_equal("", ""))
|
|
143
|
-
self.assertTrue(pyotp.utils.strings_equal("a", "a"))
|
|
144
|
-
self.assertTrue(pyotp.utils.strings_equal("a" * 1000, "a" * 1000))
|
|
145
|
-
|
|
146
|
-
self.assertFalse(pyotp.utils.strings_equal("", "a"))
|
|
147
|
-
self.assertFalse(pyotp.utils.strings_equal("a", ""))
|
|
148
|
-
self.assertFalse(pyotp.utils.strings_equal("a" * 999 + "b", "a" * 1000))
|
|
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))
|
|
166
|
-
|
|
167
|
-
class Timecop(object):
|
|
168
|
-
"""
|
|
169
|
-
Half-assed clone of timecop.rb, just enough to pass our tests.
|
|
170
|
-
"""
|
|
171
|
-
|
|
172
|
-
def __init__(self, freeze_timestamp):
|
|
173
|
-
self.freeze_timestamp = freeze_timestamp
|
|
174
|
-
|
|
175
|
-
def __enter__(self):
|
|
176
|
-
self.real_datetime = datetime.datetime
|
|
177
|
-
datetime.datetime = self.frozen_datetime()
|
|
178
|
-
|
|
179
|
-
def __exit__(self, type, value, traceback):
|
|
180
|
-
datetime.datetime = self.real_datetime
|
|
181
|
-
|
|
182
|
-
def frozen_datetime(self):
|
|
183
|
-
class FrozenDateTime(datetime.datetime):
|
|
184
|
-
@classmethod
|
|
185
|
-
def now(cls, **kwargs):
|
|
186
|
-
return cls.fromtimestamp(timecop.freeze_timestamp)
|
|
187
|
-
|
|
188
|
-
timecop = self
|
|
189
|
-
return FrozenDateTime
|
|
190
|
-
|
|
191
|
-
if __name__ == '__main__':
|
|
192
|
-
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|