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.
@@ -1,6 +1,5 @@
1
- Copyright (C) 2011 by Mark Percival <m@mdp.im>
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.1.1
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
- ![QR Code for OTP](http://chart.apis.google.com/chart?cht=qr&chs=250x250&chl=otpauth%3A%2F%2Ftotp%2Falice%40google.com%3Fsecret%3DJBSWY3DPEHPK3PXP)
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 "Current OTP: %s" % totp.now()
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/coveralls/pyotp/pyotp.svg
110
- :target: https://coveralls.io/r/pyotp/pyotp?branch=master
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
- ![QR Code for OTP](http://chart.apis.google.com/chart?cht=qr&chs=250x250&chl=otpauth%3A%2F%2Ftotp%2Falice%40google.com%3Fsecret%3DJBSWY3DPEHPK3PXP)
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 "Current OTP: %s" % totp.now()
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/coveralls/pyotp/pyotp.svg
102
- :target: https://coveralls.io/r/pyotp/pyotp?branch=master
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,6 +1,10 @@
1
1
  [bdist_wheel]
2
2
  universal = 1
3
3
 
4
+ [flake8]
5
+ max-line-length = 120
6
+ ignore = E301, E302, E401, E226, F841
7
+
4
8
  [egg_info]
5
9
  tag_build =
6
10
  tag_date = 0
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env python
2
2
 
3
- import os, glob
4
- from setuptools import setup, find_packages
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.1.1',
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
+ )
@@ -0,0 +1,10 @@
1
+ from __future__ import absolute_import, division, print_function, unicode_literals
2
+
3
+ import sys
4
+
5
+ USING_PYTHON2 = True if sys.version_info < (3, 0) else False
6
+
7
+ if USING_PYTHON2:
8
+ str = unicode # noqa
9
+ else:
10
+ str = str
@@ -1,8 +1,8 @@
1
- from __future__ import print_function, unicode_literals, division, absolute_import
1
+ from __future__ import absolute_import, division, print_function, unicode_literals
2
2
 
3
- from pyotp.otp import OTP
4
- from pyotp import utils
5
- from future.builtins import str
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 print_function, unicode_literals, division, absolute_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 future.builtins import str
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
- hmac_hash = hmac.new(
31
- self.byte_secret(),
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 because
66
- # the hmac functions in python 2.6 and 3.3 don't work with bytearray
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 print_function, unicode_literals, division, absolute_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 pyotp import utils
7
- from pyotp.otp import OTP
8
- from future.builtins import str
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 counter
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 ticks before and after the current one
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.1.1
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
- ![QR Code for OTP](http://chart.apis.google.com/chart?cht=qr&chs=250x250&chl=otpauth%3A%2F%2Ftotp%2Falice%40google.com%3Fsecret%3DJBSWY3DPEHPK3PXP)
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 "Current OTP: %s" % totp.now()
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/coveralls/pyotp/pyotp.svg
110
- :target: https://coveralls.io/r/pyotp/pyotp?branch=master
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()
@@ -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
- )
@@ -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