PyOTP 2.2.8__tar.gz → 2.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
1
  include LICENSE
2
- include README.markdown
2
+ include README.rst
3
3
  include requirements.txt
4
4
  include test.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 1.1
2
2
  Name: pyotp
3
- Version: 2.2.8
3
+ Version: 2.4.0
4
4
  Summary: Python One Time Password Library
5
5
  Home-page: https://github.com/pyotp/pyotp
6
6
  Author: PyOTP contributors
@@ -84,11 +84,15 @@ Description: PyOTP - The Python One-Time Password Library
84
84
  hotp.verify('316439', 1401) # => True
85
85
  hotp.verify('316439', 1402) # => False
86
86
 
87
- Generating a base32 Secret Key
88
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
89
- ::
87
+ Generating a Secret Key
88
+ ~~~~~~~~~~~~~~~~~~~~~~~
89
+ A helper function is provided to generate a 16 character base32 secret, compatible with Google Authenticator and other OTP apps::
90
+
91
+ pyotp.random_base32()
90
92
 
91
- pyotp.random_base32() # returns a 16 character base32 secret. Compatible with Google Authenticator and other OTP apps
93
+ Some applications want the secret key to be formatted as a hex-encoded string::
94
+
95
+ pyotp.random_hex() # returns a 32-character hex-encoded secret
92
96
 
93
97
  Google Authenticator Compatible
94
98
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -96,17 +100,27 @@ Description: PyOTP - The Python One-Time Password Library
96
100
  PyOTP works with the Google Authenticator iPhone and Android app, as well as other OTP apps like Authy. PyOTP includes the
97
101
  ability to generate provisioning URIs for use with the QR Code scanner built into these MFA client apps::
98
102
 
99
- pyotp.totp.TOTP('JBSWY3DPEHPK3PXP').provisioning_uri("alice@google.com", issuer_name="Secure App")
103
+ pyotp.totp.TOTP('JBSWY3DPEHPK3PXP').provisioning_uri(name='alice@google.com', issuer_name='Secure App')
100
104
 
101
105
  >>> 'otpauth://totp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App'
102
106
 
103
- pyotp.hotp.HOTP('JBSWY3DPEHPK3PXP').provisioning_uri("alice@google.com", initial_count=0, issuer_name="Secure App")
107
+ pyotp.hotp.HOTP('JBSWY3DPEHPK3PXP').provisioning_uri(name="alice@google.com", issuer_name="Secure App", initial_count=0)
104
108
 
105
109
  >>> 'otpauth://hotp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App&counter=0'
106
110
 
107
111
  This URL can then be rendered as a QR Code (for example, using https://github.com/neocotic/qrious) which can then be scanned
108
112
  and added to the users list of OTP credentials.
109
113
 
114
+ Parsing these URLs is also supported::
115
+
116
+ pyotp.parse_uri('otpauth://totp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App')
117
+
118
+ >>> <pyotp.totp.TOTP object at 0xFFFFFFFF>
119
+
120
+ pyotp.parse_uri('otpauth://hotp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App&counter=0'
121
+
122
+ >>> <pyotp.totp.HOTP object at 0xFFFFFFFF>
123
+
110
124
  Working example
111
125
  ~~~~~~~~~~~~~~~
112
126
 
@@ -123,10 +137,10 @@ Description: PyOTP - The Python One-Time Password Library
123
137
  Links
124
138
  ~~~~~
125
139
 
126
- * `Project home page (GitHub) <https://github.com/pyotp/pyotp>`_
140
+ * `Project home page (GitHub) <https://github.com/pyauth/pyotp>`_
127
141
  * `Documentation (Read the Docs) <https://pyotp.readthedocs.io/en/latest/>`_
128
142
  * `Package distribution (PyPI) <https://pypi.python.org/pypi/pyotp>`_
129
- * `Change log <https://github.com/pyotp/pyotp/blob/master/Changes.rst>`_
143
+ * `Change log <https://github.com/pyauth/pyotp/blob/master/Changes.rst>`_
130
144
  * `RFC 4226: HOTP: An HMAC-Based One-Time Password <https://tools.ietf.org/html/rfc4226>`_
131
145
  * `RFC 6238: TOTP: Time-Based One-Time Password Algorithm <https://tools.ietf.org/html/rfc6238>`_
132
146
  * `ROTP <https://github.com/mdp/rotp>`_ - Original Ruby OTP library by `Mark Percival <https://github.com/mdp>`_
@@ -139,10 +153,10 @@ Description: PyOTP - The Python One-Time Password Library
139
153
  * `WebAuthn <https://www.w3.org/TR/webauthn/>`_
140
154
  * `PyWARP <https://github.com/pyauth/pywarp>`_
141
155
 
142
- .. image:: https://img.shields.io/travis/pyotp/pyotp.svg
143
- :target: https://travis-ci.org/pyotp/pyotp
144
- .. image:: https://img.shields.io/codecov/c/github/pyotp/pyotp/master.svg
145
- :target: https://codecov.io/github/pyotp/pyotp?branch=master
156
+ .. image:: https://github.com/pyauth/pyotp/workflows/Python%20package/badge.svg
157
+ :target: https://github.com/pyauth/pyotp/actions
158
+ .. image:: https://img.shields.io/codecov/c/github/pyauth/pyotp/master.svg
159
+ :target: https://codecov.io/github/pyauth/pyotp?branch=master
146
160
  .. image:: https://img.shields.io/pypi/v/pyotp.svg
147
161
  :target: https://pypi.python.org/pypi/pyotp
148
162
  .. image:: https://img.shields.io/pypi/l/pyotp.svg
@@ -157,7 +171,6 @@ Classifier: License :: OSI Approved :: MIT License
157
171
  Classifier: Operating System :: MacOS :: MacOS X
158
172
  Classifier: Operating System :: POSIX
159
173
  Classifier: Programming Language :: Python
160
- Classifier: Programming Language :: Python :: 2.7
161
174
  Classifier: Programming Language :: Python :: 3.4
162
175
  Classifier: Programming Language :: Python :: 3.5
163
176
  Classifier: Programming Language :: Python :: 3.6
@@ -76,11 +76,15 @@ Counter-based OTPs
76
76
  hotp.verify('316439', 1401) # => True
77
77
  hotp.verify('316439', 1402) # => False
78
78
 
79
- Generating a base32 Secret Key
80
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
81
- ::
79
+ Generating a Secret Key
80
+ ~~~~~~~~~~~~~~~~~~~~~~~
81
+ A helper function is provided to generate a 16 character base32 secret, compatible with Google Authenticator and other OTP apps::
82
+
83
+ pyotp.random_base32()
82
84
 
83
- pyotp.random_base32() # returns a 16 character base32 secret. Compatible with Google Authenticator and other OTP apps
85
+ Some applications want the secret key to be formatted as a hex-encoded string::
86
+
87
+ pyotp.random_hex() # returns a 32-character hex-encoded secret
84
88
 
85
89
  Google Authenticator Compatible
86
90
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -88,17 +92,27 @@ Google Authenticator Compatible
88
92
  PyOTP works with the Google Authenticator iPhone and Android app, as well as other OTP apps like Authy. PyOTP includes the
89
93
  ability to generate provisioning URIs for use with the QR Code scanner built into these MFA client apps::
90
94
 
91
- pyotp.totp.TOTP('JBSWY3DPEHPK3PXP').provisioning_uri("alice@google.com", issuer_name="Secure App")
95
+ pyotp.totp.TOTP('JBSWY3DPEHPK3PXP').provisioning_uri(name='alice@google.com', issuer_name='Secure App')
92
96
 
93
97
  >>> 'otpauth://totp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App'
94
98
 
95
- pyotp.hotp.HOTP('JBSWY3DPEHPK3PXP').provisioning_uri("alice@google.com", initial_count=0, issuer_name="Secure App")
99
+ pyotp.hotp.HOTP('JBSWY3DPEHPK3PXP').provisioning_uri(name="alice@google.com", issuer_name="Secure App", initial_count=0)
96
100
 
97
101
  >>> 'otpauth://hotp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App&counter=0'
98
102
 
99
103
  This URL can then be rendered as a QR Code (for example, using https://github.com/neocotic/qrious) which can then be scanned
100
104
  and added to the users list of OTP credentials.
101
105
 
106
+ Parsing these URLs is also supported::
107
+
108
+ pyotp.parse_uri('otpauth://totp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App')
109
+
110
+ >>> <pyotp.totp.TOTP object at 0xFFFFFFFF>
111
+
112
+ pyotp.parse_uri('otpauth://hotp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App&counter=0'
113
+
114
+ >>> <pyotp.totp.HOTP object at 0xFFFFFFFF>
115
+
102
116
  Working example
103
117
  ~~~~~~~~~~~~~~~
104
118
 
@@ -115,10 +129,10 @@ Now run the following and compare the output::
115
129
  Links
116
130
  ~~~~~
117
131
 
118
- * `Project home page (GitHub) <https://github.com/pyotp/pyotp>`_
132
+ * `Project home page (GitHub) <https://github.com/pyauth/pyotp>`_
119
133
  * `Documentation (Read the Docs) <https://pyotp.readthedocs.io/en/latest/>`_
120
134
  * `Package distribution (PyPI) <https://pypi.python.org/pypi/pyotp>`_
121
- * `Change log <https://github.com/pyotp/pyotp/blob/master/Changes.rst>`_
135
+ * `Change log <https://github.com/pyauth/pyotp/blob/master/Changes.rst>`_
122
136
  * `RFC 4226: HOTP: An HMAC-Based One-Time Password <https://tools.ietf.org/html/rfc4226>`_
123
137
  * `RFC 6238: TOTP: Time-Based One-Time Password Algorithm <https://tools.ietf.org/html/rfc6238>`_
124
138
  * `ROTP <https://github.com/mdp/rotp>`_ - Original Ruby OTP library by `Mark Percival <https://github.com/mdp>`_
@@ -131,10 +145,10 @@ For new applications:
131
145
  * `WebAuthn <https://www.w3.org/TR/webauthn/>`_
132
146
  * `PyWARP <https://github.com/pyauth/pywarp>`_
133
147
 
134
- .. image:: https://img.shields.io/travis/pyotp/pyotp.svg
135
- :target: https://travis-ci.org/pyotp/pyotp
136
- .. image:: https://img.shields.io/codecov/c/github/pyotp/pyotp/master.svg
137
- :target: https://codecov.io/github/pyotp/pyotp?branch=master
148
+ .. image:: https://github.com/pyauth/pyotp/workflows/Python%20package/badge.svg
149
+ :target: https://github.com/pyauth/pyotp/actions
150
+ .. image:: https://img.shields.io/codecov/c/github/pyauth/pyotp/master.svg
151
+ :target: https://codecov.io/github/pyauth/pyotp?branch=master
138
152
  .. image:: https://img.shields.io/pypi/v/pyotp.svg
139
153
  :target: https://pypi.python.org/pypi/pyotp
140
154
  .. image:: https://img.shields.io/pypi/l/pyotp.svg
@@ -3,7 +3,7 @@ universal = 1
3
3
 
4
4
  [flake8]
5
5
  max-line-length = 120
6
- ignore = E301, E302, W504
6
+ ignore = E401, W504
7
7
 
8
8
  [egg_info]
9
9
  tag_build =
@@ -7,7 +7,7 @@ install_requires = [line.rstrip() for line in open(os.path.join(os.path.dirname(
7
7
 
8
8
  setup(
9
9
  name="pyotp",
10
- version="2.2.8",
10
+ version="2.4.0",
11
11
  url="https://github.com/pyotp/pyotp",
12
12
  license="MIT License",
13
13
  author="PyOTP contributors",
@@ -17,6 +17,7 @@ setup(
17
17
  install_requires=install_requires,
18
18
  packages=["pyotp"],
19
19
  package_dir={"": "src"},
20
+ package_data={"pyotp": ["py.typed"]},
20
21
  platforms=["MacOS X", "Posix"],
21
22
  zip_safe=False,
22
23
  test_suite="test",
@@ -26,7 +27,6 @@ setup(
26
27
  "Operating System :: MacOS :: MacOS X",
27
28
  "Operating System :: POSIX",
28
29
  "Programming Language :: Python",
29
- "Programming Language :: Python :: 2.7",
30
30
  "Programming Language :: Python :: 3.4",
31
31
  "Programming Language :: Python :: 3.5",
32
32
  "Programming Language :: Python :: 3.6",
@@ -0,0 +1,102 @@
1
+ from typing import Any, Dict, Sequence
2
+
3
+ import hashlib
4
+ from re import split
5
+ from urllib.parse import unquote, urlparse, parse_qsl
6
+
7
+ from .compat import random
8
+ from .hotp import HOTP as HOTP
9
+ from .otp import OTP as OTP
10
+ from .totp import TOTP as TOTP
11
+
12
+
13
+ def random_base32(
14
+ length: int = 16,
15
+ chars: Sequence[str] = list('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')) -> str:
16
+ if length < 16:
17
+ raise Exception("Secrets should be at least 128 bits")
18
+
19
+ return ''.join(
20
+ random.choice(chars)
21
+ for _ in range(length)
22
+ )
23
+
24
+
25
+ def random_hex(
26
+ length: int = 32,
27
+ chars: Sequence[str] = list('ABCDEF0123456789')) -> str:
28
+ if length < 32:
29
+ raise Exception("Secrets should be at least 128 bits")
30
+ return random_base32(length=length, chars=chars)
31
+
32
+
33
+ def parse_uri(uri: str) -> OTP:
34
+ """
35
+ Parses the provisioning URI for the OTP; works for either TOTP or HOTP.
36
+
37
+ See also:
38
+ https://github.com/google/google-authenticator/wiki/Key-Uri-Format
39
+
40
+ :param uri: the hotp/totp URI to parse
41
+ :returns: OTP object
42
+ """
43
+
44
+ # Secret (to be filled in later)
45
+ secret = None
46
+
47
+ # Data we'll parse to the correct constructor
48
+ otp_data = {} # type: Dict[str, Any]
49
+
50
+ # Parse with URLlib
51
+ parsed_uri = urlparse(unquote(uri))
52
+
53
+ if parsed_uri.scheme != 'otpauth':
54
+ raise ValueError('Not an otpauth URI')
55
+
56
+ # Parse issuer/accountname info
57
+ accountinfo_parts = split(':|%3A', parsed_uri.path[1:], maxsplit=1)
58
+ if len(accountinfo_parts) == 1:
59
+ otp_data['name'] = accountinfo_parts[0]
60
+ else:
61
+ otp_data['issuer'] = accountinfo_parts[0]
62
+ otp_data['name'] = accountinfo_parts[1]
63
+
64
+ # Parse values
65
+ for key, value in parse_qsl(parsed_uri.query):
66
+ if key == 'secret':
67
+ secret = value
68
+ elif key == 'issuer':
69
+ if 'issuer' in otp_data and otp_data['issuer'] is not None and otp_data['issuer'] != value:
70
+ raise ValueError('If issuer is specified in both label and parameters, it should be equal.')
71
+ otp_data['issuer'] = value
72
+ elif key == 'algorithm':
73
+ if value == 'SHA1':
74
+ otp_data['digest'] = hashlib.sha1
75
+ elif value == 'SHA256':
76
+ otp_data['digest'] = hashlib.sha256
77
+ elif value == 'SHA512':
78
+ otp_data['digest'] = hashlib.sha512
79
+ else:
80
+ raise ValueError('Invalid value for algorithm, must be SHA1, SHA256 or SHA512')
81
+ elif key == 'digits':
82
+ digits = int(value)
83
+ if digits not in [6, 8]:
84
+ raise ValueError('Digits may only be 6 or 8')
85
+ otp_data['digits'] = digits
86
+ elif key == 'period':
87
+ otp_data['interval'] = value
88
+ elif key == 'counter':
89
+ otp_data['initial_count'] = value
90
+ else:
91
+ raise ValueError('{} is not a valid parameter'.format(key))
92
+
93
+ if not secret:
94
+ raise ValueError('No secret found in URI')
95
+
96
+ # Create objects
97
+ if parsed_uri.netloc == 'totp':
98
+ return TOTP(secret, **otp_data)
99
+ elif parsed_uri.netloc == 'hotp':
100
+ return HOTP(secret, **otp_data)
101
+
102
+ raise ValueError('Not a supported OTP type')
@@ -0,0 +1,7 @@
1
+ # Use secrets module if available (Python version >= 3.6) per PEP 506
2
+ try:
3
+ from secrets import SystemRandom # type: ignore
4
+ except ImportError:
5
+ from random import SystemRandom
6
+
7
+ random = SystemRandom()
@@ -1,36 +1,43 @@
1
- from __future__ import absolute_import, division, print_function, unicode_literals
1
+ from typing import Any, Optional
2
2
 
3
3
  from . import utils
4
4
  from .otp import OTP
5
- from .compat import str
5
+
6
6
 
7
7
  class HOTP(OTP):
8
8
  """
9
9
  Handler for HMAC-based OTP counters.
10
10
  """
11
- def at(self, count):
11
+ def __init__(self, *args: Any, initial_count: int = 0, **kwargs: Any) -> None:
12
+ """
13
+ :param initial_count: starting HMAC counter value, defaults to 0
14
+ """
15
+ self.initial_count = initial_count
16
+ super(HOTP, self).__init__(*args, **kwargs)
17
+
18
+ def at(self, count: int) -> str:
12
19
  """
13
20
  Generates the OTP for the given count.
14
21
 
15
22
  :param count: the OTP HMAC counter
16
- :type count: int
17
23
  :returns: OTP
18
- :rtype: str
19
24
  """
20
25
  return self.generate_otp(count)
21
26
 
22
- def verify(self, otp, counter):
27
+ def verify(self, otp: str, counter: int) -> bool:
23
28
  """
24
29
  Verifies the OTP passed in against the current counter OTP.
25
30
 
26
31
  :param otp: the OTP to check against
27
- :type otp: str
28
- :param count: the OTP HMAC counter
29
- :type count: int
32
+ :param counter: the OTP HMAC counter
30
33
  """
31
34
  return utils.strings_equal(str(otp), str(self.at(counter)))
32
35
 
33
- def provisioning_uri(self, name, initial_count=0, issuer_name=None):
36
+ def provisioning_uri(
37
+ self,
38
+ name: Optional[str] = None,
39
+ initial_count: Optional[int] = None,
40
+ issuer_name: Optional[str] = None) -> str:
34
41
  """
35
42
  Returns the provisioning URI for the OTP. This can then be
36
43
  encoded in a QR Code and used to provision an OTP app like
@@ -40,19 +47,16 @@ class HOTP(OTP):
40
47
  https://github.com/google/google-authenticator/wiki/Key-Uri-Format
41
48
 
42
49
  :param name: name of the user account
43
- :type name: str
44
50
  :param initial_count: starting HMAC counter value, defaults to 0
45
- :type initial_count: int
46
51
  :param issuer_name: the name of the OTP issuer; this will be the
47
52
  organization title of the OTP entry in Authenticator
48
53
  :returns: provisioning URI
49
- :rtype: str
50
54
  """
51
55
  return utils.build_uri(
52
56
  self.secret,
53
- name,
54
- initial_count=initial_count,
55
- issuer_name=issuer_name,
57
+ name=name if name else self.name,
58
+ initial_count=initial_count if initial_count else self.initial_count,
59
+ issuer=issuer_name if issuer_name else self.issuer,
56
60
  algorithm=self.digest().name,
57
61
  digits=self.digits
58
62
  )
@@ -1,32 +1,39 @@
1
- from __future__ import absolute_import, division, print_function, unicode_literals
1
+ from typing import Any, Optional
2
2
 
3
3
  import base64
4
4
  import hashlib
5
5
  import hmac
6
- from .compat import str
6
+
7
7
 
8
8
  class OTP(object):
9
9
  """
10
10
  Base class for OTP handlers.
11
11
  """
12
- def __init__(self, s, digits=6, digest=hashlib.sha1):
12
+ def __init__(
13
+ self,
14
+ s: str,
15
+ digits: int = 6,
16
+ digest: Any = hashlib.sha1,
17
+ name: Optional[str] = None,
18
+ issuer: Optional[str] = None
19
+ ) -> None:
13
20
  """
14
21
  :param s: secret in base32 format
15
- :type s: str
16
22
  :param digits: number of integers in the OTP. Some apps expect this to be 6 digits, others support more.
17
- :type digits: int
18
23
  :param digest: digest function to use in the HMAC (expected to be sha1)
19
- :type digest: callable
24
+ :param name: account name
25
+ :param issuer: issuer
20
26
  """
21
27
  self.digits = digits
22
28
  self.digest = digest
23
29
  self.secret = s
30
+ self.name = name or 'Secret'
31
+ self.issuer = issuer
24
32
 
25
- def generate_otp(self, input):
33
+ def generate_otp(self, input: int) -> str:
26
34
  """
27
35
  :param input: the HMAC counter value to use as the OTP input.
28
36
  Usually either the counter, or the computed integer based on the Unix timestamp
29
- :type input: int
30
37
  """
31
38
  if input < 0:
32
39
  raise ValueError('input must be positive integer')
@@ -43,14 +50,14 @@ class OTP(object):
43
50
 
44
51
  return str_code
45
52
 
46
- def byte_secret(self):
53
+ def byte_secret(self) -> bytes:
47
54
  missing_padding = len(self.secret) % 8
48
55
  if missing_padding != 0:
49
56
  self.secret += '=' * (8 - missing_padding)
50
57
  return base64.b32decode(self.secret, casefold=True)
51
58
 
52
59
  @staticmethod
53
- def int_to_bytestring(i, padding=8):
60
+ def int_to_bytestring(i: int, padding: int = 8) -> bytes:
54
61
  """
55
62
  Turns an integer to the OATH specified
56
63
  bytestring, which is fed to the HMAC
File without changes
@@ -1,60 +1,57 @@
1
- from __future__ import absolute_import, division, print_function, unicode_literals
1
+ from typing import Any, Union, Optional
2
2
 
3
3
  import datetime
4
4
  import time
5
5
 
6
6
  from . import utils
7
7
  from .otp import OTP
8
- from .compat import str
8
+
9
9
 
10
10
  class TOTP(OTP):
11
11
  """
12
12
  Handler for time-based OTP counters.
13
13
  """
14
- def __init__(self, *args, **kwargs):
14
+ def __init__(self, *args: Any, interval: int = 30, **kwargs: Any) -> None:
15
15
  """
16
16
  :param interval: the time interval in seconds
17
17
  for OTP. This defaults to 30.
18
- :type interval: int
19
18
  """
20
- self.interval = kwargs.pop('interval', 30)
19
+ self.interval = interval
21
20
  super(TOTP, self).__init__(*args, **kwargs)
22
21
 
23
- def at(self, for_time, counter_offset=0):
22
+ def at(self, for_time: Union[int, datetime.datetime], counter_offset: int = 0) -> str:
24
23
  """
25
24
  Accepts either a Unix timestamp integer or a datetime object.
26
25
 
26
+ To get the time until the next timecode change (seconds until the current OTP expires), use this instead::
27
+
28
+ totp = pyotp.TOTP(...)
29
+ time_remaining = totp.interval - datetime.datetime.now().timestamp() % totp.interval
30
+
27
31
  :param for_time: the time to generate an OTP for
28
- :type for_time: int or datetime
29
32
  :param counter_offset: the amount of ticks to add to the time counter
30
33
  :returns: OTP value
31
- :rtype: str
32
34
  """
33
35
  if not isinstance(for_time, datetime.datetime):
34
36
  for_time = datetime.datetime.fromtimestamp(int(for_time))
35
37
  return self.generate_otp(self.timecode(for_time) + counter_offset)
36
38
 
37
- def now(self):
39
+ def now(self) -> str:
38
40
  """
39
41
  Generate the current time OTP
40
42
 
41
43
  :returns: OTP value
42
- :rtype: str
43
44
  """
44
45
  return self.generate_otp(self.timecode(datetime.datetime.now()))
45
46
 
46
- def verify(self, otp, for_time=None, valid_window=0):
47
+ def verify(self, otp: str, for_time: Optional[datetime.datetime] = None, valid_window: int = 0) -> bool:
47
48
  """
48
49
  Verifies the OTP passed in against the current time OTP.
49
50
 
50
51
  :param otp: the OTP to check against
51
- :type otp: str
52
52
  :param for_time: Time to check OTP at (defaults to now)
53
- :type for_time: int or datetime
54
53
  :param valid_window: extends the validity to this many counter ticks before and after the current one
55
- :type valid_window: int
56
54
  :returns: True if verification succeeded, False otherwise
57
- :rtype: bool
58
55
  """
59
56
  if for_time is None:
60
57
  for_time = datetime.datetime.now()
@@ -67,7 +64,7 @@ class TOTP(OTP):
67
64
 
68
65
  return utils.strings_equal(str(otp), str(self.at(for_time)))
69
66
 
70
- def provisioning_uri(self, name, issuer_name=None):
67
+ def provisioning_uri(self, name: Optional[str] = None, issuer_name: Optional[str] = None) -> str:
71
68
  """
72
69
  Returns the provisioning URI for the OTP. This can then be
73
70
  encoded in a QR Code and used to provision an OTP app like
@@ -76,17 +73,12 @@ class TOTP(OTP):
76
73
  See also:
77
74
  https://github.com/google/google-authenticator/wiki/Key-Uri-Format
78
75
 
79
- :param name: name of the user account
80
- :type name: str
81
- :param issuer_name: the name of the OTP issuer; this will be the
82
- organization title of the OTP entry in Authenticator
83
- :returns: provisioning URI
84
- :rtype: str
85
76
  """
86
- return utils.build_uri(self.secret, name, issuer_name=issuer_name,
77
+ return utils.build_uri(self.secret, name if name else self.name,
78
+ issuer=issuer_name if issuer_name else self.issuer,
87
79
  algorithm=self.digest().name,
88
80
  digits=self.digits, period=self.interval)
89
81
 
90
- def timecode(self, for_time):
82
+ def timecode(self, for_time: datetime.datetime) -> int:
91
83
  i = time.mktime(for_time.timetuple())
92
84
  return int(i / self.interval)
@@ -1,19 +1,18 @@
1
- from __future__ import absolute_import, division, print_function, unicode_literals
1
+ from typing import Dict, Optional, Union
2
2
 
3
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):
4
+ from hmac import compare_digest
5
+ from urllib.parse import quote, urlencode
6
+
7
+
8
+ def build_uri(
9
+ secret: str,
10
+ name: str,
11
+ initial_count: Optional[int] = None,
12
+ issuer: Optional[str] = None,
13
+ algorithm: Optional[str] = None,
14
+ digits: Optional[int] = None,
15
+ period: Optional[int] = None) -> str:
17
16
  """
18
17
  Returns the provisioning URI for the OTP; works for either TOTP or HOTP.
19
18
 
@@ -26,24 +25,16 @@ def build_uri(secret, name, initial_count=None, issuer_name=None,
26
25
  https://github.com/google/google-authenticator/wiki/Key-Uri-Format
27
26
 
28
27
  :param secret: the hotp/totp secret used to generate the URI
29
- :type secret: str
30
28
  :param name: name of the account
31
- :type name: str
32
29
  :param initial_count: starting counter value, defaults to None.
33
30
  If none, the OTP type will be assumed as TOTP.
34
- :type initial_count: int
35
- :param issuer_name: the name of the OTP issuer; this will be the
31
+ :param issuer: the name of the OTP issuer; this will be the
36
32
  organization title of the OTP entry in Authenticator
37
- :type issuer_name: str
38
33
  :param algorithm: the algorithm used in the OTP generation.
39
- :type algorithm: str
40
34
  :param digits: the length of the OTP generated code.
41
- :type digits: int
42
35
  :param period: the number of seconds the OTP generator is set to
43
36
  expire every code.
44
- :type period: int
45
37
  :returns: provisioning uri
46
- :rtype: str
47
38
  """
48
39
  # initial_count may be 0 as a valid param
49
40
  is_initial_count_present = (initial_count is not None)
@@ -56,17 +47,17 @@ def build_uri(secret, name, initial_count=None, issuer_name=None,
56
47
  otp_type = 'hotp' if is_initial_count_present else 'totp'
57
48
  base_uri = 'otpauth://{0}/{1}?{2}'
58
49
 
59
- url_args = {'secret': secret}
50
+ url_args = {'secret': secret} # type: Dict[str, Union[None, int, str]]
60
51
 
61
52
  label = quote(name)
62
- if issuer_name is not None:
63
- label = quote(issuer_name) + ':' + label
64
- url_args['issuer'] = issuer_name
53
+ if issuer is not None:
54
+ label = quote(issuer) + ':' + label
55
+ url_args['issuer'] = issuer
65
56
 
66
57
  if is_initial_count_present:
67
58
  url_args['counter'] = initial_count
68
59
  if is_algorithm_set:
69
- url_args['algorithm'] = algorithm.upper()
60
+ url_args['algorithm'] = algorithm.upper() # type: ignore
70
61
  if is_digits_set:
71
62
  url_args['digits'] = digits
72
63
  if is_period_set:
@@ -76,26 +67,7 @@ def build_uri(secret, name, initial_count=None, issuer_name=None,
76
67
  return uri
77
68
 
78
69
 
79
- def _compare_digest(s1, s2):
80
- differences = 0
81
- for c1, c2 in izip_longest(s1, s2):
82
- if c1 is None or c2 is None:
83
- differences = 1
84
- continue
85
- differences |= ord(c1) ^ ord(c2)
86
- return differences == 0
87
-
88
-
89
- try:
90
- # Python 3.3+ and 2.7.7+ include a timing-attack-resistant
91
- # comparison function, which is probably more reliable than ours.
92
- # Use it if available.
93
- from hmac import compare_digest
94
- except ImportError:
95
- compare_digest = _compare_digest
96
-
97
-
98
- def strings_equal(s1, s2):
70
+ def strings_equal(s1: str, s2: str) -> bool:
99
71
  """
100
72
  Timing-attack resistant string comparison.
101
73
 
@@ -106,4 +78,4 @@ def strings_equal(s1, s2):
106
78
  """
107
79
  s1 = unicodedata.normalize('NFKC', s1)
108
80
  s2 = unicodedata.normalize('NFKC', s2)
109
- return compare_digest(s1.encode(), s2.encode())
81
+ return compare_digest(s1.encode("utf-8"), s2.encode("utf-8"))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 1.1
2
2
  Name: pyotp
3
- Version: 2.2.8
3
+ Version: 2.4.0
4
4
  Summary: Python One Time Password Library
5
5
  Home-page: https://github.com/pyotp/pyotp
6
6
  Author: PyOTP contributors
@@ -84,11 +84,15 @@ Description: PyOTP - The Python One-Time Password Library
84
84
  hotp.verify('316439', 1401) # => True
85
85
  hotp.verify('316439', 1402) # => False
86
86
 
87
- Generating a base32 Secret Key
88
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
89
- ::
87
+ Generating a Secret Key
88
+ ~~~~~~~~~~~~~~~~~~~~~~~
89
+ A helper function is provided to generate a 16 character base32 secret, compatible with Google Authenticator and other OTP apps::
90
+
91
+ pyotp.random_base32()
90
92
 
91
- pyotp.random_base32() # returns a 16 character base32 secret. Compatible with Google Authenticator and other OTP apps
93
+ Some applications want the secret key to be formatted as a hex-encoded string::
94
+
95
+ pyotp.random_hex() # returns a 32-character hex-encoded secret
92
96
 
93
97
  Google Authenticator Compatible
94
98
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -96,17 +100,27 @@ Description: PyOTP - The Python One-Time Password Library
96
100
  PyOTP works with the Google Authenticator iPhone and Android app, as well as other OTP apps like Authy. PyOTP includes the
97
101
  ability to generate provisioning URIs for use with the QR Code scanner built into these MFA client apps::
98
102
 
99
- pyotp.totp.TOTP('JBSWY3DPEHPK3PXP').provisioning_uri("alice@google.com", issuer_name="Secure App")
103
+ pyotp.totp.TOTP('JBSWY3DPEHPK3PXP').provisioning_uri(name='alice@google.com', issuer_name='Secure App')
100
104
 
101
105
  >>> 'otpauth://totp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App'
102
106
 
103
- pyotp.hotp.HOTP('JBSWY3DPEHPK3PXP').provisioning_uri("alice@google.com", initial_count=0, issuer_name="Secure App")
107
+ pyotp.hotp.HOTP('JBSWY3DPEHPK3PXP').provisioning_uri(name="alice@google.com", issuer_name="Secure App", initial_count=0)
104
108
 
105
109
  >>> 'otpauth://hotp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App&counter=0'
106
110
 
107
111
  This URL can then be rendered as a QR Code (for example, using https://github.com/neocotic/qrious) which can then be scanned
108
112
  and added to the users list of OTP credentials.
109
113
 
114
+ Parsing these URLs is also supported::
115
+
116
+ pyotp.parse_uri('otpauth://totp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App')
117
+
118
+ >>> <pyotp.totp.TOTP object at 0xFFFFFFFF>
119
+
120
+ pyotp.parse_uri('otpauth://hotp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App&counter=0'
121
+
122
+ >>> <pyotp.totp.HOTP object at 0xFFFFFFFF>
123
+
110
124
  Working example
111
125
  ~~~~~~~~~~~~~~~
112
126
 
@@ -123,10 +137,10 @@ Description: PyOTP - The Python One-Time Password Library
123
137
  Links
124
138
  ~~~~~
125
139
 
126
- * `Project home page (GitHub) <https://github.com/pyotp/pyotp>`_
140
+ * `Project home page (GitHub) <https://github.com/pyauth/pyotp>`_
127
141
  * `Documentation (Read the Docs) <https://pyotp.readthedocs.io/en/latest/>`_
128
142
  * `Package distribution (PyPI) <https://pypi.python.org/pypi/pyotp>`_
129
- * `Change log <https://github.com/pyotp/pyotp/blob/master/Changes.rst>`_
143
+ * `Change log <https://github.com/pyauth/pyotp/blob/master/Changes.rst>`_
130
144
  * `RFC 4226: HOTP: An HMAC-Based One-Time Password <https://tools.ietf.org/html/rfc4226>`_
131
145
  * `RFC 6238: TOTP: Time-Based One-Time Password Algorithm <https://tools.ietf.org/html/rfc6238>`_
132
146
  * `ROTP <https://github.com/mdp/rotp>`_ - Original Ruby OTP library by `Mark Percival <https://github.com/mdp>`_
@@ -139,10 +153,10 @@ Description: PyOTP - The Python One-Time Password Library
139
153
  * `WebAuthn <https://www.w3.org/TR/webauthn/>`_
140
154
  * `PyWARP <https://github.com/pyauth/pywarp>`_
141
155
 
142
- .. image:: https://img.shields.io/travis/pyotp/pyotp.svg
143
- :target: https://travis-ci.org/pyotp/pyotp
144
- .. image:: https://img.shields.io/codecov/c/github/pyotp/pyotp/master.svg
145
- :target: https://codecov.io/github/pyotp/pyotp?branch=master
156
+ .. image:: https://github.com/pyauth/pyotp/workflows/Python%20package/badge.svg
157
+ :target: https://github.com/pyauth/pyotp/actions
158
+ .. image:: https://img.shields.io/codecov/c/github/pyauth/pyotp/master.svg
159
+ :target: https://codecov.io/github/pyauth/pyotp?branch=master
146
160
  .. image:: https://img.shields.io/pypi/v/pyotp.svg
147
161
  :target: https://pypi.python.org/pypi/pyotp
148
162
  .. image:: https://img.shields.io/pypi/l/pyotp.svg
@@ -157,7 +171,6 @@ Classifier: License :: OSI Approved :: MIT License
157
171
  Classifier: Operating System :: MacOS :: MacOS X
158
172
  Classifier: Operating System :: POSIX
159
173
  Classifier: Programming Language :: Python
160
- Classifier: Programming Language :: Python :: 2.7
161
174
  Classifier: Programming Language :: Python :: 3.4
162
175
  Classifier: Programming Language :: Python :: 3.5
163
176
  Classifier: Programming Language :: Python :: 3.6
@@ -9,6 +9,7 @@ src/pyotp/__init__.py
9
9
  src/pyotp/compat.py
10
10
  src/pyotp/hotp.py
11
11
  src/pyotp/otp.py
12
+ src/pyotp/py.typed
12
13
  src/pyotp/totp.py
13
14
  src/pyotp/utils.py
14
15
  src/pyotp.egg-info/PKG-INFO
@@ -1,18 +1,14 @@
1
1
  #!/usr/bin/env python
2
2
  # coding: utf-8
3
3
 
4
- from __future__ import absolute_import, division, print_function, unicode_literals
5
-
6
4
  import base64, datetime, hashlib, os, sys, unittest
7
5
  from warnings import warn
8
6
 
9
- try:
10
- from urllib.parse import urlparse, parse_qsl
11
- except ImportError:
12
- from urlparse import urlparse, parse_qsl
7
+ from urllib.parse import urlparse, parse_qsl
13
8
 
14
9
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
15
- import pyotp # noqa
10
+ import pyotp
11
+
16
12
 
17
13
  class HOTPExampleValuesFromTheRFC(unittest.TestCase):
18
14
  def test_match_rfc(self):
@@ -42,37 +38,55 @@ class HOTPExampleValuesFromTheRFC(unittest.TestCase):
42
38
  self.assertFalse(hotp.verify('520489', 10))
43
39
 
44
40
  def test_provisioning_uri(self):
45
- hotp = pyotp.HOTP('wrn3pqx5uqxqvnqr')
41
+ hotp = pyotp.HOTP('wrn3pqx5uqxqvnqr', name='mark@percival')
46
42
 
47
- url = urlparse(
48
- hotp.provisioning_uri('mark@percival'))
43
+ url = urlparse(hotp.provisioning_uri())
49
44
  self.assertEqual(url.scheme, 'otpauth')
50
45
  self.assertEqual(url.netloc, 'hotp')
51
46
  self.assertEqual(url.path, '/mark%40percival')
52
47
  self.assertEqual(dict(parse_qsl(url.query)),
53
48
  {'secret': 'wrn3pqx5uqxqvnqr', 'counter': '0'})
54
-
55
- url = urlparse(
56
- hotp.provisioning_uri('mark@percival', initial_count=12))
49
+ self.assertEqual(
50
+ hotp.provisioning_uri(),
51
+ pyotp.parse_uri(
52
+ hotp.provisioning_uri()
53
+ ).provisioning_uri()
54
+ )
55
+
56
+ hotp = pyotp.HOTP('wrn3pqx5uqxqvnqr', name='mark@percival', initial_count=12)
57
+ url = urlparse(hotp.provisioning_uri())
57
58
  self.assertEqual(url.scheme, 'otpauth')
58
59
  self.assertEqual(url.netloc, 'hotp')
59
60
  self.assertEqual(url.path, '/mark%40percival')
60
61
  self.assertEqual(dict(parse_qsl(url.query)),
61
62
  {'secret': 'wrn3pqx5uqxqvnqr', 'counter': '12'})
62
-
63
- url = urlparse(
64
- hotp.provisioning_uri('mark@percival', issuer_name='FooCorp!'))
63
+ self.assertEqual(
64
+ hotp.provisioning_uri(),
65
+ pyotp.parse_uri(
66
+ hotp.provisioning_uri()
67
+ ).provisioning_uri()
68
+ )
69
+
70
+ hotp = pyotp.HOTP('wrn3pqx5uqxqvnqr', name='mark@percival',
71
+ issuer='FooCorp!')
72
+ url = urlparse(hotp.provisioning_uri())
65
73
  self.assertEqual(url.scheme, 'otpauth')
66
74
  self.assertEqual(url.netloc, 'hotp')
67
75
  self.assertEqual(url.path, '/FooCorp%21:mark%40percival')
68
76
  self.assertEqual(dict(parse_qsl(url.query)),
69
77
  {'secret': 'wrn3pqx5uqxqvnqr', 'counter': '0',
70
78
  'issuer': 'FooCorp!'})
79
+ self.assertEqual(
80
+ hotp.provisioning_uri(),
81
+ pyotp.parse_uri(
82
+ hotp.provisioning_uri()
83
+ ).provisioning_uri()
84
+ )
71
85
 
72
86
  key = 'c7uxuqhgflpw7oruedmglbrk7u6242vb'
73
- hotp = pyotp.HOTP(key, digits=8, digest=hashlib.sha256)
74
- url = urlparse(
75
- hotp.provisioning_uri('baco@peperina', issuer_name='FooCorp'))
87
+ hotp = pyotp.HOTP(key, digits=8, digest=hashlib.sha256,
88
+ name='baco@peperina', issuer='FooCorp')
89
+ url = urlparse(hotp.provisioning_uri())
76
90
  self.assertEqual(url.scheme, 'otpauth')
77
91
  self.assertEqual(url.netloc, 'hotp')
78
92
  self.assertEqual(url.path, '/FooCorp:baco%40peperina')
@@ -80,11 +94,16 @@ class HOTPExampleValuesFromTheRFC(unittest.TestCase):
80
94
  {'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb',
81
95
  'counter': '0', 'issuer': 'FooCorp',
82
96
  'digits': '8', 'algorithm': 'SHA256'})
83
-
84
- hotp = pyotp.HOTP(key, digits=8)
85
- url = urlparse(
86
- hotp.provisioning_uri('baco@peperina', issuer_name='Foo Corp',
87
- initial_count=10))
97
+ self.assertEqual(
98
+ hotp.provisioning_uri(),
99
+ pyotp.parse_uri(
100
+ hotp.provisioning_uri()
101
+ ).provisioning_uri()
102
+ )
103
+
104
+ hotp = pyotp.HOTP(key, digits=8, name='baco@peperina',
105
+ issuer='Foo Corp', initial_count=10)
106
+ url = urlparse(hotp.provisioning_uri())
88
107
  self.assertEqual(url.scheme, 'otpauth')
89
108
  self.assertEqual(url.netloc, 'hotp')
90
109
  self.assertEqual(url.path, '/Foo%20Corp:baco%40peperina')
@@ -92,6 +111,12 @@ class HOTPExampleValuesFromTheRFC(unittest.TestCase):
92
111
  {'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb',
93
112
  'counter': '10', 'issuer': 'Foo Corp',
94
113
  'digits': '8'})
114
+ self.assertEqual(
115
+ hotp.provisioning_uri(),
116
+ pyotp.parse_uri(
117
+ hotp.provisioning_uri()
118
+ ).provisioning_uri()
119
+ )
95
120
 
96
121
  def test_other_secret(self):
97
122
  hotp = pyotp.HOTP(
@@ -183,27 +208,40 @@ class TOTPExampleValuesFromTheRFC(unittest.TestCase):
183
208
  self.assertFalse(totp.verify('050471'))
184
209
 
185
210
  def test_provisioning_uri(self):
186
- totp = pyotp.TOTP('wrn3pqx5uqxqvnqr')
187
- url = urlparse(
188
- totp.provisioning_uri('mark@percival'))
211
+ totp = pyotp.TOTP('wrn3pqx5uqxqvnqr', name='mark@percival')
212
+ url = urlparse(totp.provisioning_uri())
189
213
  self.assertEqual(url.scheme, 'otpauth')
190
214
  self.assertEqual(url.netloc, 'totp')
191
215
  self.assertEqual(url.path, '/mark%40percival')
192
216
  self.assertEqual(dict(parse_qsl(url.query)),
193
217
  {'secret': 'wrn3pqx5uqxqvnqr'})
194
-
195
- url = urlparse(
196
- totp.provisioning_uri('mark@percival', issuer_name='FooCorp!'))
218
+ self.assertEqual(
219
+ totp.provisioning_uri(),
220
+ pyotp.parse_uri(
221
+ totp.provisioning_uri()
222
+ ).provisioning_uri()
223
+ )
224
+
225
+ totp = pyotp.TOTP('wrn3pqx5uqxqvnqr', name='mark@percival',
226
+ issuer='FooCorp!')
227
+ url = urlparse(totp.provisioning_uri())
197
228
  self.assertEqual(url.scheme, 'otpauth')
198
229
  self.assertEqual(url.netloc, 'totp')
199
230
  self.assertEqual(url.path, '/FooCorp%21:mark%40percival')
200
231
  self.assertEqual(dict(parse_qsl(url.query)),
201
232
  {'secret': 'wrn3pqx5uqxqvnqr',
202
233
  'issuer': 'FooCorp!'})
234
+ self.assertEqual(
235
+ totp.provisioning_uri(),
236
+ pyotp.parse_uri(
237
+ totp.provisioning_uri()
238
+ ).provisioning_uri()
239
+ )
203
240
 
204
241
  key = 'c7uxuqhgflpw7oruedmglbrk7u6242vb'
205
- totp = pyotp.TOTP(key, digits=8, interval=60, digest=hashlib.sha256)
206
- url = urlparse(totp.provisioning_uri('baco@peperina', issuer_name='FooCorp'))
242
+ totp = pyotp.TOTP(key, digits=8, interval=60, digest=hashlib.sha256,
243
+ name='baco@peperina', issuer='FooCorp')
244
+ url = urlparse(totp.provisioning_uri())
207
245
  self.assertEqual(url.scheme, 'otpauth')
208
246
  self.assertEqual(url.netloc, 'totp')
209
247
  self.assertEqual(url.path, '/FooCorp:baco%40peperina')
@@ -212,9 +250,16 @@ class TOTPExampleValuesFromTheRFC(unittest.TestCase):
212
250
  'issuer': 'FooCorp',
213
251
  'digits': '8', 'period': '60',
214
252
  'algorithm': 'SHA256'})
215
-
216
- totp = pyotp.TOTP(key, digits=8, interval=60)
217
- url = urlparse(totp.provisioning_uri('baco@peperina', issuer_name='FooCorp'))
253
+ self.assertEqual(
254
+ totp.provisioning_uri(),
255
+ pyotp.parse_uri(
256
+ totp.provisioning_uri()
257
+ ).provisioning_uri()
258
+ )
259
+
260
+ totp = pyotp.TOTP(key, digits=8, interval=60,
261
+ name='baco@peperina', issuer='FooCorp')
262
+ url = urlparse(totp.provisioning_uri())
218
263
  self.assertEqual(url.scheme, 'otpauth')
219
264
  self.assertEqual(url.netloc, 'totp')
220
265
  self.assertEqual(url.path, '/FooCorp:baco%40peperina')
@@ -222,9 +267,15 @@ class TOTPExampleValuesFromTheRFC(unittest.TestCase):
222
267
  {'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb',
223
268
  'issuer': 'FooCorp',
224
269
  'digits': '8', 'period': '60'})
225
-
226
- totp = pyotp.TOTP(key, digits=8)
227
- url = urlparse(totp.provisioning_uri('baco@peperina', issuer_name='FooCorp'))
270
+ self.assertEqual(
271
+ totp.provisioning_uri(),
272
+ pyotp.parse_uri(
273
+ totp.provisioning_uri()
274
+ ).provisioning_uri()
275
+ )
276
+
277
+ totp = pyotp.TOTP(key, digits=8, name='baco@peperina', issuer='FooCorp')
278
+ url = urlparse(totp.provisioning_uri())
228
279
  self.assertEqual(url.scheme, 'otpauth')
229
280
  self.assertEqual(url.netloc, 'totp')
230
281
  self.assertEqual(url.path, '/FooCorp:baco%40peperina')
@@ -232,10 +283,22 @@ class TOTPExampleValuesFromTheRFC(unittest.TestCase):
232
283
  {'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb',
233
284
  'issuer': 'FooCorp',
234
285
  'digits': '8'})
286
+ self.assertEqual(
287
+ totp.provisioning_uri(),
288
+ pyotp.parse_uri(
289
+ totp.provisioning_uri()
290
+ ).provisioning_uri()
291
+ )
235
292
 
236
293
  def test_random_key_generation(self):
237
294
  self.assertEqual(len(pyotp.random_base32()), 16)
238
295
  self.assertEqual(len(pyotp.random_base32(length=20)), 20)
296
+ self.assertEqual(len(pyotp.random_hex()), 32)
297
+ self.assertEqual(len(pyotp.random_hex(length=64)), 64)
298
+ with self.assertRaises(Exception):
299
+ pyotp.random_base32(length=15)
300
+ with self.assertRaises(Exception):
301
+ pyotp.random_hex(length=24)
239
302
 
240
303
 
241
304
  class CompareDigestTest(unittest.TestCase):
@@ -251,10 +314,6 @@ class CompareDigestTest(unittest.TestCase):
251
314
  self.assertFalse(self.method("a" * 999 + "b", "a" * 1000))
252
315
 
253
316
 
254
- class FallBackCompareDigestTest(CompareDigestTest):
255
- method = staticmethod(pyotp.utils._compare_digest)
256
-
257
-
258
317
  class StringComparisonTest(CompareDigestTest):
259
318
  method = staticmethod(pyotp.utils.strings_equal)
260
319
 
@@ -280,6 +339,45 @@ class ValidWindowTest(unittest.TestCase):
280
339
  self.assertTrue(totp.verify("681610", 200, 1))
281
340
  self.assertFalse(totp.verify("195979", 200, 1))
282
341
 
342
+ class ParseUriTest(unittest.TestCase):
343
+ def test_invalids(self):
344
+ with self.assertRaises(ValueError) as cm:
345
+ pyotp.parse_uri('http://hello.com')
346
+ self.assertEqual('Not an otpauth URI', str(cm.exception))
347
+
348
+ with self.assertRaises(ValueError) as cm:
349
+ pyotp.parse_uri('otpauth://totp')
350
+ self.assertEqual('No secret found in URI', str(cm.exception))
351
+
352
+ with self.assertRaises(ValueError) as cm:
353
+ pyotp.parse_uri('otpauth://derp?secret=foo')
354
+ self.assertEqual('Not a supported OTP type', str(cm.exception))
355
+
356
+ with self.assertRaises(ValueError) as cm:
357
+ pyotp.parse_uri('otpauth://totp?foo=secret')
358
+ self.assertEqual('foo is not a valid parameter', str(cm.exception))
359
+
360
+ with self.assertRaises(ValueError) as cm:
361
+ pyotp.parse_uri('otpauth://totp?digits=-1')
362
+ self.assertEqual('Digits may only be 6 or 8', str(cm.exception))
363
+
364
+ with self.assertRaises(ValueError) as cm:
365
+ pyotp.parse_uri('otpauth://totp/SomeIssuer:?issuer=AnotherIssuer')
366
+ self.assertEqual('If issuer is specified in both label and parameters, it should be equal.', str(cm.exception))
367
+
368
+ with self.assertRaises(ValueError) as cm:
369
+ pyotp.parse_uri('otpauth://totp?algorithm=aes')
370
+ self.assertEqual('Invalid value for algorithm, must be SHA1, SHA256 or SHA512', str(cm.exception))
371
+
372
+ def test_algorithms(self):
373
+ otp = pyotp.parse_uri('otpauth://totp?algorithm=SHA1&secret=123456&algorithm=SHA1')
374
+ self.assertEqual(hashlib.sha1, otp.digest)
375
+
376
+ otp = pyotp.parse_uri('otpauth://totp?algorithm=SHA1&secret=123456&algorithm=SHA256')
377
+ self.assertEqual(hashlib.sha256, otp.digest)
378
+
379
+ otp = pyotp.parse_uri('otpauth://totp?algorithm=SHA1&secret=123456&algorithm=SHA512')
380
+ self.assertEqual(hashlib.sha512, otp.digest)
283
381
 
284
382
  class Timecop(object):
285
383
  """
@@ -305,5 +403,6 @@ class Timecop(object):
305
403
  timecop = self
306
404
  return FrozenDateTime
307
405
 
406
+
308
407
  if __name__ == '__main__':
309
408
  unittest.main()
@@ -1,23 +0,0 @@
1
- from __future__ import (absolute_import, division,
2
- print_function, unicode_literals)
3
-
4
- from pyotp.hotp import HOTP # noqa
5
- from pyotp.otp import OTP # noqa
6
- from pyotp.totp import TOTP # noqa
7
- from . import utils # noqa
8
-
9
- def random_base32(length=16, random=None,
10
- chars=list('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')):
11
-
12
- # Use secrets module if available (Python version >= 3.6) per PEP 506
13
- try:
14
- import secrets
15
- random = secrets.SystemRandom()
16
- except ImportError:
17
- import random as _random
18
- random = _random.SystemRandom()
19
-
20
- return ''.join(
21
- random.choice(chars)
22
- for _ in range(length)
23
- )
@@ -1,10 +0,0 @@
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
File without changes
File without changes