PyOTP 2.1.1__tar.gz → 2.2.4__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.4}/LICENSE +3 -4
- {pyotp-2.1.1/src/pyotp.egg-info → pyotp-2.2.4}/PKG-INFO +9 -7
- {pyotp-2.1.1 → pyotp-2.2.4}/README.rst +8 -6
- pyotp-2.2.4/requirements.txt +0 -0
- {pyotp-2.1.1 → pyotp-2.2.4}/setup.cfg +4 -0
- pyotp-2.2.4/setup.py +34 -0
- pyotp-2.2.4/src/pyotp/__init__.py +17 -0
- pyotp-2.2.4/src/pyotp/compat.py +10 -0
- {pyotp-2.1.1 → pyotp-2.2.4}/src/pyotp/hotp.py +6 -4
- {pyotp-2.1.1 → pyotp-2.2.4}/src/pyotp/otp.py +7 -11
- {pyotp-2.1.1 → pyotp-2.2.4}/src/pyotp/totp.py +12 -9
- pyotp-2.2.4/src/pyotp/utils.py +101 -0
- {pyotp-2.1.1 → pyotp-2.2.4/src/pyotp.egg-info}/PKG-INFO +9 -7
- {pyotp-2.1.1 → pyotp-2.2.4}/src/pyotp.egg-info/SOURCES.txt +1 -1
- pyotp-2.2.4/test.py +293 -0
- pyotp-2.1.1/requirements.txt +0 -1
- pyotp-2.1.1/setup.py +0 -33
- 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.4}/MANIFEST.in +0 -0
- {pyotp-2.1.1 → pyotp-2.2.4}/src/pyotp.egg-info/dependency_links.txt +0 -0
- {pyotp-2.1.1 → pyotp-2.2.4}/src/pyotp.egg-info/not-zip-safe +0 -0
- {pyotp-2.1.1 → pyotp-2.2.4}/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.4
|
|
4
4
|
Summary: Python One Time Password Library
|
|
5
5
|
Home-page: https://github.com/pyotp/pyotp
|
|
6
6
|
Author: PyOTP contributors
|
|
@@ -20,6 +20,8 @@ Description: PyOTP - The Python One-Time Password Library
|
|
|
20
20
|
compatible app. Users can set up auth tokens in their apps easily by using their phone camera to scan `otpauth://
|
|
21
21
|
<https://github.com/google/google-authenticator/wiki/Key-Uri-Format>`_ QR codes provided by PyOTP.
|
|
22
22
|
|
|
23
|
+
We recommend that implementers read the `OWASP Authentication Cheat Sheet <https://www.owasp.org/index.php/Authentication_Cheat_Sheet>`_ and `NIST SP 800-63-3: Digital Authentication Guideline <https://pages.nist.gov/800-63-3/>`_ for a high level overview of authentication best practices.
|
|
24
|
+
|
|
23
25
|
Quick overview of using One Time Passwords on your phone
|
|
24
26
|
--------------------------------------------------------
|
|
25
27
|
|
|
@@ -84,13 +86,13 @@ Description: PyOTP - The Python One-Time Password Library
|
|
|
84
86
|
|
|
85
87
|
Scan the following barcode with your phone, using Google Authenticator
|
|
86
88
|
|
|
87
|
-
|
|
89
|
+
.. image:: http://chart.apis.google.com/chart?cht=qr&chs=250x250&chl=otpauth%3A%2F%2Ftotp%2Falice%40google.com%3Fsecret%3DJBSWY3DPEHPK3PXP
|
|
88
90
|
|
|
89
91
|
Now run the following and compare the output::
|
|
90
92
|
|
|
91
93
|
import pyotp
|
|
92
94
|
totp = pyotp.TOTP("JBSWY3DPEHPK3PXP")
|
|
93
|
-
print
|
|
95
|
+
print("Current OTP:", totp.now())
|
|
94
96
|
|
|
95
97
|
Links
|
|
96
98
|
~~~~~
|
|
@@ -103,15 +105,15 @@ Description: PyOTP - The Python One-Time Password Library
|
|
|
103
105
|
* `RFC 6238: TOTP: Time-Based One-Time Password Algorithm <https://tools.ietf.org/html/rfc6238>`_
|
|
104
106
|
* `ROTP <https://github.com/mdp/rotp>`_ - Original Ruby OTP library by `Mark Percival <https://github.com/mdp>`_
|
|
105
107
|
* `OTPHP <https://github.com/lelag/otphp>`_ - PHP port of ROTP by `Le Lag <https://github.com/lelag>`_
|
|
108
|
+
* `OWASP Authentication Cheat Sheet <https://www.owasp.org/index.php/Authentication_Cheat_Sheet>`_
|
|
109
|
+
* `NIST SP 800-63-3: Digital Authentication Guideline <https://pages.nist.gov/800-63-3/>`_
|
|
106
110
|
|
|
107
111
|
.. image:: https://img.shields.io/travis/pyotp/pyotp.svg
|
|
108
112
|
:target: https://travis-ci.org/pyotp/pyotp
|
|
109
|
-
.. image:: https://img.shields.io/
|
|
110
|
-
:target: https://
|
|
113
|
+
.. image:: https://img.shields.io/codecov/c/github/pyotp/pyotp/master.svg
|
|
114
|
+
:target: https://codecov.io/github/pyotp/pyotp?branch=master
|
|
111
115
|
.. image:: https://img.shields.io/pypi/v/pyotp.svg
|
|
112
116
|
: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
117
|
.. image:: https://img.shields.io/pypi/l/pyotp.svg
|
|
116
118
|
:target: https://pypi.python.org/pypi/pyotp
|
|
117
119
|
.. image:: https://readthedocs.org/projects/pyotp/badge/?version=latest
|
|
@@ -12,6 +12,8 @@ Authenticator <https://en.wikipedia.org/wiki/Google_Authenticator>`_, `Authy <ht
|
|
|
12
12
|
compatible app. Users can set up auth tokens in their apps easily by using their phone camera to scan `otpauth://
|
|
13
13
|
<https://github.com/google/google-authenticator/wiki/Key-Uri-Format>`_ QR codes provided by PyOTP.
|
|
14
14
|
|
|
15
|
+
We recommend that implementers read the `OWASP Authentication Cheat Sheet <https://www.owasp.org/index.php/Authentication_Cheat_Sheet>`_ and `NIST SP 800-63-3: Digital Authentication Guideline <https://pages.nist.gov/800-63-3/>`_ for a high level overview of authentication best practices.
|
|
16
|
+
|
|
15
17
|
Quick overview of using One Time Passwords on your phone
|
|
16
18
|
--------------------------------------------------------
|
|
17
19
|
|
|
@@ -76,13 +78,13 @@ Working example
|
|
|
76
78
|
|
|
77
79
|
Scan the following barcode with your phone, using Google Authenticator
|
|
78
80
|
|
|
79
|
-
|
|
81
|
+
.. image:: http://chart.apis.google.com/chart?cht=qr&chs=250x250&chl=otpauth%3A%2F%2Ftotp%2Falice%40google.com%3Fsecret%3DJBSWY3DPEHPK3PXP
|
|
80
82
|
|
|
81
83
|
Now run the following and compare the output::
|
|
82
84
|
|
|
83
85
|
import pyotp
|
|
84
86
|
totp = pyotp.TOTP("JBSWY3DPEHPK3PXP")
|
|
85
|
-
print
|
|
87
|
+
print("Current OTP:", totp.now())
|
|
86
88
|
|
|
87
89
|
Links
|
|
88
90
|
~~~~~
|
|
@@ -95,15 +97,15 @@ Links
|
|
|
95
97
|
* `RFC 6238: TOTP: Time-Based One-Time Password Algorithm <https://tools.ietf.org/html/rfc6238>`_
|
|
96
98
|
* `ROTP <https://github.com/mdp/rotp>`_ - Original Ruby OTP library by `Mark Percival <https://github.com/mdp>`_
|
|
97
99
|
* `OTPHP <https://github.com/lelag/otphp>`_ - PHP port of ROTP by `Le Lag <https://github.com/lelag>`_
|
|
100
|
+
* `OWASP Authentication Cheat Sheet <https://www.owasp.org/index.php/Authentication_Cheat_Sheet>`_
|
|
101
|
+
* `NIST SP 800-63-3: Digital Authentication Guideline <https://pages.nist.gov/800-63-3/>`_
|
|
98
102
|
|
|
99
103
|
.. image:: https://img.shields.io/travis/pyotp/pyotp.svg
|
|
100
104
|
:target: https://travis-ci.org/pyotp/pyotp
|
|
101
|
-
.. image:: https://img.shields.io/
|
|
102
|
-
:target: https://
|
|
105
|
+
.. image:: https://img.shields.io/codecov/c/github/pyotp/pyotp/master.svg
|
|
106
|
+
:target: https://codecov.io/github/pyotp/pyotp?branch=master
|
|
103
107
|
.. image:: https://img.shields.io/pypi/v/pyotp.svg
|
|
104
108
|
: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
109
|
.. image:: https://img.shields.io/pypi/l/pyotp.svg
|
|
108
110
|
:target: https://pypi.python.org/pypi/pyotp
|
|
109
111
|
.. image:: https://readthedocs.org/projects/pyotp/badge/?version=latest
|
|
File without changes
|
pyotp-2.2.4/setup.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from setuptools import setup
|
|
5
|
+
|
|
6
|
+
install_requires = [line.rstrip() for line in open(os.path.join(os.path.dirname(__file__), "requirements.txt"))]
|
|
7
|
+
|
|
8
|
+
setup(
|
|
9
|
+
name="pyotp",
|
|
10
|
+
version="2.2.4",
|
|
11
|
+
url="https://github.com/pyotp/pyotp",
|
|
12
|
+
license="BSD License",
|
|
13
|
+
author="PyOTP contributors",
|
|
14
|
+
author_email="kislyuk@gmail.com",
|
|
15
|
+
description="Python One Time Password Library",
|
|
16
|
+
long_description=open("README.rst").read(),
|
|
17
|
+
install_requires=install_requires,
|
|
18
|
+
packages=["pyotp"],
|
|
19
|
+
package_dir={"": "src"},
|
|
20
|
+
platforms=["MacOS X", "Posix"],
|
|
21
|
+
zip_safe=False,
|
|
22
|
+
test_suite="test",
|
|
23
|
+
classifiers=[
|
|
24
|
+
"Intended Audience :: Developers",
|
|
25
|
+
"License :: OSI Approved :: BSD License",
|
|
26
|
+
"Operating System :: MacOS :: MacOS X",
|
|
27
|
+
"Operating System :: POSIX",
|
|
28
|
+
"Programming Language :: Python",
|
|
29
|
+
"Programming Language :: Python :: 2.7",
|
|
30
|
+
"Programming Language :: Python :: 3.3",
|
|
31
|
+
"Programming Language :: Python :: 3.4",
|
|
32
|
+
"Topic :: Software Development :: Libraries :: Python Modules"
|
|
33
|
+
]
|
|
34
|
+
)
|
|
@@ -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://{0}/{1}?{2}'
|
|
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.4
|
|
4
4
|
Summary: Python One Time Password Library
|
|
5
5
|
Home-page: https://github.com/pyotp/pyotp
|
|
6
6
|
Author: PyOTP contributors
|
|
@@ -20,6 +20,8 @@ Description: PyOTP - The Python One-Time Password Library
|
|
|
20
20
|
compatible app. Users can set up auth tokens in their apps easily by using their phone camera to scan `otpauth://
|
|
21
21
|
<https://github.com/google/google-authenticator/wiki/Key-Uri-Format>`_ QR codes provided by PyOTP.
|
|
22
22
|
|
|
23
|
+
We recommend that implementers read the `OWASP Authentication Cheat Sheet <https://www.owasp.org/index.php/Authentication_Cheat_Sheet>`_ and `NIST SP 800-63-3: Digital Authentication Guideline <https://pages.nist.gov/800-63-3/>`_ for a high level overview of authentication best practices.
|
|
24
|
+
|
|
23
25
|
Quick overview of using One Time Passwords on your phone
|
|
24
26
|
--------------------------------------------------------
|
|
25
27
|
|
|
@@ -84,13 +86,13 @@ Description: PyOTP - The Python One-Time Password Library
|
|
|
84
86
|
|
|
85
87
|
Scan the following barcode with your phone, using Google Authenticator
|
|
86
88
|
|
|
87
|
-
|
|
89
|
+
.. image:: http://chart.apis.google.com/chart?cht=qr&chs=250x250&chl=otpauth%3A%2F%2Ftotp%2Falice%40google.com%3Fsecret%3DJBSWY3DPEHPK3PXP
|
|
88
90
|
|
|
89
91
|
Now run the following and compare the output::
|
|
90
92
|
|
|
91
93
|
import pyotp
|
|
92
94
|
totp = pyotp.TOTP("JBSWY3DPEHPK3PXP")
|
|
93
|
-
print
|
|
95
|
+
print("Current OTP:", totp.now())
|
|
94
96
|
|
|
95
97
|
Links
|
|
96
98
|
~~~~~
|
|
@@ -103,15 +105,15 @@ Description: PyOTP - The Python One-Time Password Library
|
|
|
103
105
|
* `RFC 6238: TOTP: Time-Based One-Time Password Algorithm <https://tools.ietf.org/html/rfc6238>`_
|
|
104
106
|
* `ROTP <https://github.com/mdp/rotp>`_ - Original Ruby OTP library by `Mark Percival <https://github.com/mdp>`_
|
|
105
107
|
* `OTPHP <https://github.com/lelag/otphp>`_ - PHP port of ROTP by `Le Lag <https://github.com/lelag>`_
|
|
108
|
+
* `OWASP Authentication Cheat Sheet <https://www.owasp.org/index.php/Authentication_Cheat_Sheet>`_
|
|
109
|
+
* `NIST SP 800-63-3: Digital Authentication Guideline <https://pages.nist.gov/800-63-3/>`_
|
|
106
110
|
|
|
107
111
|
.. image:: https://img.shields.io/travis/pyotp/pyotp.svg
|
|
108
112
|
:target: https://travis-ci.org/pyotp/pyotp
|
|
109
|
-
.. image:: https://img.shields.io/
|
|
110
|
-
:target: https://
|
|
113
|
+
.. image:: https://img.shields.io/codecov/c/github/pyotp/pyotp/master.svg
|
|
114
|
+
:target: https://codecov.io/github/pyotp/pyotp?branch=master
|
|
111
115
|
.. image:: https://img.shields.io/pypi/v/pyotp.svg
|
|
112
116
|
: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
117
|
.. image:: https://img.shields.io/pypi/l/pyotp.svg
|
|
116
118
|
:target: https://pypi.python.org/pypi/pyotp
|
|
117
119
|
.. 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.4/test.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
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, datetime, hashlib, os, sys, unittest
|
|
7
|
+
from warnings import warn
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from urllib.parse import urlparse, parse_qsl
|
|
11
|
+
except ImportError:
|
|
12
|
+
from urlparse import urlparse, parse_qsl
|
|
13
|
+
|
|
14
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
|
15
|
+
import pyotp # noqa
|
|
16
|
+
|
|
17
|
+
class HOTPExampleValuesFromTheRFC(unittest.TestCase):
|
|
18
|
+
def test_match_rfc(self):
|
|
19
|
+
# 12345678901234567890 in Bas32
|
|
20
|
+
# GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ
|
|
21
|
+
hotp = pyotp.HOTP('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ')
|
|
22
|
+
self.assertEqual(hotp.at(0), '755224')
|
|
23
|
+
self.assertEqual(hotp.at(1), '287082')
|
|
24
|
+
self.assertEqual(hotp.at(2), '359152')
|
|
25
|
+
self.assertEqual(hotp.at(3), '969429')
|
|
26
|
+
self.assertEqual(hotp.at(4), '338314')
|
|
27
|
+
self.assertEqual(hotp.at(5), '254676')
|
|
28
|
+
self.assertEqual(hotp.at(6), '287922')
|
|
29
|
+
self.assertEqual(hotp.at(7), '162583')
|
|
30
|
+
self.assertEqual(hotp.at(8), '399871')
|
|
31
|
+
self.assertEqual(hotp.at(9), '520489')
|
|
32
|
+
|
|
33
|
+
def test_verify_otp_reuse(self):
|
|
34
|
+
hotp = pyotp.HOTP('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ')
|
|
35
|
+
self.assertTrue(hotp.verify('520489', 9))
|
|
36
|
+
self.assertFalse(hotp.verify('520489', 10))
|
|
37
|
+
self.assertFalse(hotp.verify('520489', 10))
|
|
38
|
+
|
|
39
|
+
def test_provisioning_uri(self):
|
|
40
|
+
hotp = pyotp.HOTP('wrn3pqx5uqxqvnqr')
|
|
41
|
+
|
|
42
|
+
url = urlparse(
|
|
43
|
+
hotp.provisioning_uri('mark@percival'))
|
|
44
|
+
self.assertEqual(url.scheme, 'otpauth')
|
|
45
|
+
self.assertEqual(url.netloc, 'hotp')
|
|
46
|
+
self.assertEqual(url.path, '/mark%40percival')
|
|
47
|
+
self.assertEqual(dict(parse_qsl(url.query)),
|
|
48
|
+
{'secret': 'wrn3pqx5uqxqvnqr', 'counter': '0'})
|
|
49
|
+
|
|
50
|
+
url = urlparse(
|
|
51
|
+
hotp.provisioning_uri('mark@percival', initial_count=12))
|
|
52
|
+
self.assertEqual(url.scheme, 'otpauth')
|
|
53
|
+
self.assertEqual(url.netloc, 'hotp')
|
|
54
|
+
self.assertEqual(url.path, '/mark%40percival')
|
|
55
|
+
self.assertEqual(dict(parse_qsl(url.query)),
|
|
56
|
+
{'secret': 'wrn3pqx5uqxqvnqr', 'counter': '12'})
|
|
57
|
+
|
|
58
|
+
url = urlparse(
|
|
59
|
+
hotp.provisioning_uri('mark@percival', issuer_name='FooCorp!'))
|
|
60
|
+
self.assertEqual(url.scheme, 'otpauth')
|
|
61
|
+
self.assertEqual(url.netloc, 'hotp')
|
|
62
|
+
self.assertEqual(url.path, '/FooCorp%21:mark%40percival')
|
|
63
|
+
self.assertEqual(dict(parse_qsl(url.query)),
|
|
64
|
+
{'secret': 'wrn3pqx5uqxqvnqr', 'counter': '0',
|
|
65
|
+
'issuer': 'FooCorp!'})
|
|
66
|
+
|
|
67
|
+
key = 'c7uxuqhgflpw7oruedmglbrk7u6242vb'
|
|
68
|
+
hotp = pyotp.HOTP(key, digits=8, digest=hashlib.sha256)
|
|
69
|
+
url = urlparse(
|
|
70
|
+
hotp.provisioning_uri('baco@peperina', issuer_name='FooCorp'))
|
|
71
|
+
self.assertEqual(url.scheme, 'otpauth')
|
|
72
|
+
self.assertEqual(url.netloc, 'hotp')
|
|
73
|
+
self.assertEqual(url.path, '/FooCorp:baco%40peperina')
|
|
74
|
+
self.assertEqual(dict(parse_qsl(url.query)),
|
|
75
|
+
{'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb',
|
|
76
|
+
'counter': '0', 'issuer': 'FooCorp',
|
|
77
|
+
'digits': '8', 'algorithm': 'SHA256'})
|
|
78
|
+
|
|
79
|
+
hotp = pyotp.HOTP(key, digits=8)
|
|
80
|
+
url = urlparse(
|
|
81
|
+
hotp.provisioning_uri('baco@peperina', issuer_name='FooCorp',
|
|
82
|
+
initial_count=10))
|
|
83
|
+
self.assertEqual(url.scheme, 'otpauth')
|
|
84
|
+
self.assertEqual(url.netloc, 'hotp')
|
|
85
|
+
self.assertEqual(url.path, '/FooCorp:baco%40peperina')
|
|
86
|
+
self.assertEqual(dict(parse_qsl(url.query)),
|
|
87
|
+
{'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb',
|
|
88
|
+
'counter': '10', 'issuer': 'FooCorp',
|
|
89
|
+
'digits': '8'})
|
|
90
|
+
|
|
91
|
+
def test_other_secret(self):
|
|
92
|
+
hotp = pyotp.HOTP(
|
|
93
|
+
'N3OVNIBRERIO5OHGVCMDGS4V4RJ3AUZOUN34J6FRM4P6JIFCG3ZA')
|
|
94
|
+
self.assertEqual(hotp.at(0), '737863')
|
|
95
|
+
self.assertEqual(hotp.at(1), '390601')
|
|
96
|
+
self.assertEqual(hotp.at(2), '363354')
|
|
97
|
+
self.assertEqual(hotp.at(3), '936780')
|
|
98
|
+
self.assertEqual(hotp.at(4), '654019')
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class TOTPExampleValuesFromTheRFC(unittest.TestCase):
|
|
102
|
+
RFC_VALUES = {
|
|
103
|
+
(hashlib.sha1, b'12345678901234567890'): (
|
|
104
|
+
(59, '94287082'),
|
|
105
|
+
(1111111109, '07081804'),
|
|
106
|
+
(1111111111, '14050471'),
|
|
107
|
+
(1234567890, '89005924'),
|
|
108
|
+
(2000000000, '69279037'),
|
|
109
|
+
(20000000000, '65353130'),
|
|
110
|
+
),
|
|
111
|
+
|
|
112
|
+
(hashlib.sha256, b'12345678901234567890123456789012'): (
|
|
113
|
+
(59, 46119246),
|
|
114
|
+
(1111111109, '68084774'),
|
|
115
|
+
(1111111111, '67062674'),
|
|
116
|
+
(1234567890, '91819424'),
|
|
117
|
+
(2000000000, '90698825'),
|
|
118
|
+
(20000000000, '77737706'),
|
|
119
|
+
),
|
|
120
|
+
|
|
121
|
+
(hashlib.sha512,
|
|
122
|
+
b'1234567890123456789012345678901234567890123456789012345678901234'):
|
|
123
|
+
(
|
|
124
|
+
(59, 90693936),
|
|
125
|
+
(1111111109, '25091201'),
|
|
126
|
+
(1111111111, '99943326'),
|
|
127
|
+
(1234567890, '93441116'),
|
|
128
|
+
(2000000000, '38618901'),
|
|
129
|
+
(20000000000, '47863826'),
|
|
130
|
+
),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
def test_match_rfc(self):
|
|
134
|
+
for digest, secret in self.RFC_VALUES:
|
|
135
|
+
totp = pyotp.TOTP(base64.b32encode(secret), 8, digest)
|
|
136
|
+
for utime, code in self.RFC_VALUES[(digest, secret)]:
|
|
137
|
+
if utime > sys.maxsize:
|
|
138
|
+
warn("32-bit platforms use native functions to handle timestamps, so they fail this test" +
|
|
139
|
+
" (and will fail after 19 January 2038)")
|
|
140
|
+
continue
|
|
141
|
+
value = totp.at(utime)
|
|
142
|
+
msg = "%s != %s (%s, time=%d)"
|
|
143
|
+
msg %= (value, code, digest().name, utime)
|
|
144
|
+
self.assertEqual(value, str(code), msg)
|
|
145
|
+
|
|
146
|
+
def test_match_rfc_digit_length(self):
|
|
147
|
+
totp = pyotp.TOTP('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ')
|
|
148
|
+
self.assertEqual(totp.at(1111111111), '050471')
|
|
149
|
+
self.assertEqual(totp.at(1234567890), '005924')
|
|
150
|
+
self.assertEqual(totp.at(2000000000), '279037')
|
|
151
|
+
|
|
152
|
+
def test_match_google_authenticator_output(self):
|
|
153
|
+
totp = pyotp.TOTP('wrn3pqx5uqxqvnqr')
|
|
154
|
+
with Timecop(1297553958):
|
|
155
|
+
self.assertEqual(totp.now(), '102705')
|
|
156
|
+
|
|
157
|
+
def test_validate_totp(self):
|
|
158
|
+
totp = pyotp.TOTP('wrn3pqx5uqxqvnqr')
|
|
159
|
+
with Timecop(1297553958):
|
|
160
|
+
self.assertTrue(totp.verify('102705'))
|
|
161
|
+
self.assertTrue(totp.verify('102705'))
|
|
162
|
+
with Timecop(1297553958 + 30):
|
|
163
|
+
self.assertFalse(totp.verify('102705'))
|
|
164
|
+
|
|
165
|
+
def test_validate_totp_with_digit_length(self):
|
|
166
|
+
totp = pyotp.TOTP('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ')
|
|
167
|
+
with Timecop(1111111111):
|
|
168
|
+
self.assertTrue(totp.verify('050471'))
|
|
169
|
+
with Timecop(1297553958 + 30):
|
|
170
|
+
self.assertFalse(totp.verify('050471'))
|
|
171
|
+
|
|
172
|
+
def test_provisioning_uri(self):
|
|
173
|
+
totp = pyotp.TOTP('wrn3pqx5uqxqvnqr')
|
|
174
|
+
url = urlparse(
|
|
175
|
+
totp.provisioning_uri('mark@percival'))
|
|
176
|
+
self.assertEqual(url.scheme, 'otpauth')
|
|
177
|
+
self.assertEqual(url.netloc, 'totp')
|
|
178
|
+
self.assertEqual(url.path, '/mark%40percival')
|
|
179
|
+
self.assertEqual(dict(parse_qsl(url.query)),
|
|
180
|
+
{'secret': 'wrn3pqx5uqxqvnqr'})
|
|
181
|
+
|
|
182
|
+
url = urlparse(
|
|
183
|
+
totp.provisioning_uri('mark@percival', issuer_name='FooCorp!'))
|
|
184
|
+
self.assertEqual(url.scheme, 'otpauth')
|
|
185
|
+
self.assertEqual(url.netloc, 'totp')
|
|
186
|
+
self.assertEqual(url.path, '/FooCorp%21:mark%40percival')
|
|
187
|
+
self.assertEqual(dict(parse_qsl(url.query)),
|
|
188
|
+
{'secret': 'wrn3pqx5uqxqvnqr',
|
|
189
|
+
'issuer': 'FooCorp!'})
|
|
190
|
+
|
|
191
|
+
key = 'c7uxuqhgflpw7oruedmglbrk7u6242vb'
|
|
192
|
+
totp = pyotp.TOTP(key, digits=8, interval=60, digest=hashlib.sha256)
|
|
193
|
+
url = urlparse(totp.provisioning_uri('baco@peperina', issuer_name='FooCorp'))
|
|
194
|
+
self.assertEqual(url.scheme, 'otpauth')
|
|
195
|
+
self.assertEqual(url.netloc, 'totp')
|
|
196
|
+
self.assertEqual(url.path, '/FooCorp:baco%40peperina')
|
|
197
|
+
self.assertEqual(dict(parse_qsl(url.query)),
|
|
198
|
+
{'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb',
|
|
199
|
+
'issuer': 'FooCorp',
|
|
200
|
+
'digits': '8', 'period': '60',
|
|
201
|
+
'algorithm': 'SHA256'})
|
|
202
|
+
|
|
203
|
+
totp = pyotp.TOTP(key, digits=8, interval=60)
|
|
204
|
+
url = urlparse(totp.provisioning_uri('baco@peperina', issuer_name='FooCorp'))
|
|
205
|
+
self.assertEqual(url.scheme, 'otpauth')
|
|
206
|
+
self.assertEqual(url.netloc, 'totp')
|
|
207
|
+
self.assertEqual(url.path, '/FooCorp:baco%40peperina')
|
|
208
|
+
self.assertEqual(dict(parse_qsl(url.query)),
|
|
209
|
+
{'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb',
|
|
210
|
+
'issuer': 'FooCorp',
|
|
211
|
+
'digits': '8', 'period': '60'})
|
|
212
|
+
|
|
213
|
+
totp = pyotp.TOTP(key, digits=8)
|
|
214
|
+
url = urlparse(totp.provisioning_uri('baco@peperina', issuer_name='FooCorp'))
|
|
215
|
+
self.assertEqual(url.scheme, 'otpauth')
|
|
216
|
+
self.assertEqual(url.netloc, 'totp')
|
|
217
|
+
self.assertEqual(url.path, '/FooCorp:baco%40peperina')
|
|
218
|
+
self.assertEqual(dict(parse_qsl(url.query)),
|
|
219
|
+
{'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb',
|
|
220
|
+
'issuer': 'FooCorp',
|
|
221
|
+
'digits': '8'})
|
|
222
|
+
|
|
223
|
+
def test_random_key_generation(self):
|
|
224
|
+
self.assertEqual(len(pyotp.random_base32()), 16)
|
|
225
|
+
self.assertEqual(len(pyotp.random_base32(length=20)), 20)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class CompareDigestTest(unittest.TestCase):
|
|
229
|
+
method = staticmethod(pyotp.utils.compare_digest)
|
|
230
|
+
|
|
231
|
+
def test_comparisons(self):
|
|
232
|
+
self.assertTrue(self.method("", ""))
|
|
233
|
+
self.assertTrue(self.method("a", "a"))
|
|
234
|
+
self.assertTrue(self.method("a" * 1000, "a" * 1000))
|
|
235
|
+
|
|
236
|
+
self.assertFalse(self.method("", "a"))
|
|
237
|
+
self.assertFalse(self.method("a", ""))
|
|
238
|
+
self.assertFalse(self.method("a" * 999 + "b", "a" * 1000))
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class FallBackCompareDigestTest(CompareDigestTest):
|
|
242
|
+
method = staticmethod(pyotp.utils._compare_digest)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class StringComparisonTest(CompareDigestTest):
|
|
246
|
+
method = staticmethod(pyotp.utils.strings_equal)
|
|
247
|
+
|
|
248
|
+
def test_fullwidth_input(self):
|
|
249
|
+
self.assertTrue(self.method("xs12345", "xs12345"))
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class CounterOffsetTest(unittest.TestCase):
|
|
253
|
+
def test_counter_offset(self):
|
|
254
|
+
totp = pyotp.TOTP("ABCDEFGH")
|
|
255
|
+
self.assertEqual(totp.at(200), "028307")
|
|
256
|
+
self.assertTrue(totp.at(200, 1), "681610")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class ValidWindowTest(unittest.TestCase):
|
|
260
|
+
def test_valid_window(self):
|
|
261
|
+
totp = pyotp.TOTP("ABCDEFGH")
|
|
262
|
+
self.assertTrue(totp.verify("451564", 200, 1))
|
|
263
|
+
self.assertTrue(totp.verify("028307", 200, 1))
|
|
264
|
+
self.assertTrue(totp.verify("681610", 200, 1))
|
|
265
|
+
self.assertFalse(totp.verify("195979", 200, 1))
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class Timecop(object):
|
|
269
|
+
"""
|
|
270
|
+
Half-assed clone of timecop.rb, just enough to pass our tests.
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
def __init__(self, freeze_timestamp):
|
|
274
|
+
self.freeze_timestamp = freeze_timestamp
|
|
275
|
+
|
|
276
|
+
def __enter__(self):
|
|
277
|
+
self.real_datetime = datetime.datetime
|
|
278
|
+
datetime.datetime = self.frozen_datetime()
|
|
279
|
+
|
|
280
|
+
def __exit__(self, type, value, traceback):
|
|
281
|
+
datetime.datetime = self.real_datetime
|
|
282
|
+
|
|
283
|
+
def frozen_datetime(self):
|
|
284
|
+
class FrozenDateTime(datetime.datetime):
|
|
285
|
+
@classmethod
|
|
286
|
+
def now(cls, **kwargs):
|
|
287
|
+
return cls.fromtimestamp(timecop.freeze_timestamp)
|
|
288
|
+
|
|
289
|
+
timecop = self
|
|
290
|
+
return FrozenDateTime
|
|
291
|
+
|
|
292
|
+
if __name__ == '__main__':
|
|
293
|
+
unittest.main()
|
pyotp-2.1.1/requirements.txt
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
future==0.15.2
|
pyotp-2.1.1/setup.py
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python
|
|
2
|
-
|
|
3
|
-
import os, glob
|
|
4
|
-
from setuptools import setup, find_packages
|
|
5
|
-
|
|
6
|
-
install_requires = [line.rstrip() for line in open(os.path.join(os.path.dirname(__file__), "requirements.txt"))]
|
|
7
|
-
|
|
8
|
-
setup(
|
|
9
|
-
name='pyotp',
|
|
10
|
-
version='2.1.1',
|
|
11
|
-
url='https://github.com/pyotp/pyotp',
|
|
12
|
-
license='BSD License',
|
|
13
|
-
author='PyOTP contributors',
|
|
14
|
-
author_email='kislyuk@gmail.com',
|
|
15
|
-
description='Python One Time Password Library',
|
|
16
|
-
long_description=open('README.rst').read(),
|
|
17
|
-
install_requires=install_requires,
|
|
18
|
-
packages=['pyotp'],
|
|
19
|
-
package_dir={'': 'src'},
|
|
20
|
-
platforms=['MacOS X', 'Posix'],
|
|
21
|
-
zip_safe=False,
|
|
22
|
-
classifiers=[
|
|
23
|
-
'Intended Audience :: Developers',
|
|
24
|
-
'License :: OSI Approved :: BSD License',
|
|
25
|
-
'Operating System :: MacOS :: MacOS X',
|
|
26
|
-
'Operating System :: POSIX',
|
|
27
|
-
'Programming Language :: Python',
|
|
28
|
-
'Programming Language :: Python :: 2.7',
|
|
29
|
-
'Programming Language :: Python :: 3.3',
|
|
30
|
-
'Programming Language :: Python :: 3.4',
|
|
31
|
-
'Topic :: Software Development :: Libraries :: Python Modules'
|
|
32
|
-
]
|
|
33
|
-
)
|
|
@@ -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
|