PyOTP 2.4.0__tar.gz → 2.5.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,6 +1,6 @@
1
1
  Metadata-Version: 1.1
2
2
  Name: pyotp
3
- Version: 2.4.0
3
+ Version: 2.5.0
4
4
  Summary: Python One Time Password Library
5
5
  Home-page: https://github.com/pyotp/pyotp
6
6
  Author: PyOTP contributors
@@ -138,7 +138,7 @@ Description: PyOTP - The Python One-Time Password Library
138
138
  ~~~~~
139
139
 
140
140
  * `Project home page (GitHub) <https://github.com/pyauth/pyotp>`_
141
- * `Documentation (Read the Docs) <https://pyotp.readthedocs.io/en/latest/>`_
141
+ * `Documentation <https://pyauth.github.io/pyotp/>`_
142
142
  * `Package distribution (PyPI) <https://pypi.python.org/pypi/pyotp>`_
143
143
  * `Change log <https://github.com/pyauth/pyotp/blob/master/Changes.rst>`_
144
144
  * `RFC 4226: HOTP: An HMAC-Based One-Time Password <https://tools.ietf.org/html/rfc4226>`_
@@ -130,7 +130,7 @@ Links
130
130
  ~~~~~
131
131
 
132
132
  * `Project home page (GitHub) <https://github.com/pyauth/pyotp>`_
133
- * `Documentation (Read the Docs) <https://pyotp.readthedocs.io/en/latest/>`_
133
+ * `Documentation <https://pyauth.github.io/pyotp/>`_
134
134
  * `Package distribution (PyPI) <https://pypi.python.org/pypi/pyotp>`_
135
135
  * `Change log <https://github.com/pyauth/pyotp/blob/master/Changes.rst>`_
136
136
  * `RFC 4226: HOTP: An HMAC-Based One-Time Password <https://tools.ietf.org/html/rfc4226>`_
@@ -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.4.0",
10
+ version="2.5.0",
11
11
  url="https://github.com/pyotp/pyotp",
12
12
  license="MIT License",
13
13
  author="PyOTP contributors",
@@ -1,7 +1,6 @@
1
- from typing import Any, Dict, Sequence
2
-
3
1
  import hashlib
4
2
  from re import split
3
+ from typing import Any, Dict, Sequence
5
4
  from urllib.parse import unquote, urlparse, parse_qsl
6
5
 
7
6
  from .compat import random
@@ -10,11 +9,9 @@ from .otp import OTP as OTP
10
9
  from .totp import TOTP as TOTP
11
10
 
12
11
 
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")
12
+ def random_base32(length: int = 26, chars: Sequence[str] = list('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')) -> str:
13
+ if length < 26:
14
+ raise ValueError("Secrets should be at least 128 bits")
18
15
 
19
16
  return ''.join(
20
17
  random.choice(chars)
@@ -22,11 +19,9 @@ def random_base32(
22
19
  )
23
20
 
24
21
 
25
- def random_hex(
26
- length: int = 32,
27
- chars: Sequence[str] = list('ABCDEF0123456789')) -> str:
22
+ def random_hex(length: int = 32, chars: Sequence[str] = list('ABCDEF0123456789')) -> str:
28
23
  if length < 32:
29
- raise Exception("Secrets should be at least 128 bits")
24
+ raise ValueError("Secrets should be at least 128 bits")
30
25
  return random_base32(length=length, chars=chars)
31
26
 
32
27
 
@@ -80,13 +75,13 @@ def parse_uri(uri: str) -> OTP:
80
75
  raise ValueError('Invalid value for algorithm, must be SHA1, SHA256 or SHA512')
81
76
  elif key == 'digits':
82
77
  digits = int(value)
83
- if digits not in [6, 8]:
84
- raise ValueError('Digits may only be 6 or 8')
78
+ if digits not in [6, 7, 8]:
79
+ raise ValueError('Digits may only be 6, 7, or 8')
85
80
  otp_data['digits'] = digits
86
81
  elif key == 'period':
87
- otp_data['interval'] = value
82
+ otp_data['interval'] = int(value)
88
83
  elif key == 'counter':
89
- otp_data['initial_count'] = value
84
+ otp_data['initial_count'] = int(value)
90
85
  else:
91
86
  raise ValueError('{} is not a valid parameter'.format(key))
92
87
 
@@ -1,3 +1,4 @@
1
+ import hashlib
1
2
  from typing import Any, Optional
2
3
 
3
4
  from . import utils
@@ -8,12 +9,18 @@ class HOTP(OTP):
8
9
  """
9
10
  Handler for HMAC-based OTP counters.
10
11
  """
11
- def __init__(self, *args: Any, initial_count: int = 0, **kwargs: Any) -> None:
12
+ def __init__(self, s: str, digits: int = 6, digest: Any = hashlib.sha1, name: Optional[str] = None,
13
+ issuer: Optional[str] = None, initial_count: int = 0) -> None:
12
14
  """
15
+ :param s: secret in base32 format
13
16
  :param initial_count: starting HMAC counter value, defaults to 0
17
+ :param digits: number of integers in the OTP. Some apps expect this to be 6 digits, others support more.
18
+ :param digest: digest function to use in the HMAC (expected to be sha1)
19
+ :param name: account name
20
+ :param issuer: issuer
14
21
  """
15
22
  self.initial_count = initial_count
16
- super(HOTP, self).__init__(*args, **kwargs)
23
+ super().__init__(s=s, digits=digits, digest=digest, name=name, issuer=issuer)
17
24
 
18
25
  def at(self, count: int) -> str:
19
26
  """
@@ -22,7 +29,7 @@ class HOTP(OTP):
22
29
  :param count: the OTP HMAC counter
23
30
  :returns: OTP
24
31
  """
25
- return self.generate_otp(count)
32
+ return self.generate_otp(self.initial_count + count)
26
33
 
27
34
  def verify(self, otp: str, counter: int) -> bool:
28
35
  """
@@ -37,7 +44,8 @@ class HOTP(OTP):
37
44
  self,
38
45
  name: Optional[str] = None,
39
46
  initial_count: Optional[int] = None,
40
- issuer_name: Optional[str] = None) -> str:
47
+ issuer_name: Optional[str] = None,
48
+ image: Optional[str] = None) -> str:
41
49
  """
42
50
  Returns the provisioning URI for the OTP. This can then be
43
51
  encoded in a QR Code and used to provision an OTP app like
@@ -58,5 +66,6 @@ class HOTP(OTP):
58
66
  initial_count=initial_count if initial_count else self.initial_count,
59
67
  issuer=issuer_name if issuer_name else self.issuer,
60
68
  algorithm=self.digest().name,
61
- digits=self.digits
69
+ digits=self.digits,
70
+ image=image,
62
71
  )
@@ -1,29 +1,15 @@
1
- from typing import Any, Optional
2
-
3
1
  import base64
4
2
  import hashlib
5
3
  import hmac
4
+ from typing import Any, Optional
6
5
 
7
6
 
8
7
  class OTP(object):
9
8
  """
10
9
  Base class for OTP handlers.
11
10
  """
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:
20
- """
21
- :param s: secret in base32 format
22
- :param digits: number of integers in the OTP. Some apps expect this to be 6 digits, others support more.
23
- :param digest: digest function to use in the HMAC (expected to be sha1)
24
- :param name: account name
25
- :param issuer: issuer
26
- """
11
+ def __init__(self, s: str, digits: int = 6, digest: Any = hashlib.sha1, name: Optional[str] = None,
12
+ issuer: Optional[str] = None) -> None:
27
13
  self.digits = digits
28
14
  self.digest = digest
29
15
  self.secret = s
@@ -1,7 +1,8 @@
1
- from typing import Any, Union, Optional
2
-
1
+ import calendar
3
2
  import datetime
3
+ import hashlib
4
4
  import time
5
+ from typing import Any, Union, Optional
5
6
 
6
7
  from . import utils
7
8
  from .otp import OTP
@@ -11,13 +12,18 @@ class TOTP(OTP):
11
12
  """
12
13
  Handler for time-based OTP counters.
13
14
  """
14
- def __init__(self, *args: Any, interval: int = 30, **kwargs: Any) -> None:
15
+ def __init__(self, s: str, digits: int = 6, digest: Any = hashlib.sha1, name: Optional[str] = None,
16
+ issuer: Optional[str] = None, interval: int = 30) -> None:
15
17
  """
16
- :param interval: the time interval in seconds
17
- for OTP. This defaults to 30.
18
+ :param s: secret in base32 format
19
+ :param interval: the time interval in seconds for OTP. This defaults to 30.
20
+ :param digits: number of integers in the OTP. Some apps expect this to be 6 digits, others support more.
21
+ :param digest: digest function to use in the HMAC (expected to be sha1)
22
+ :param name: account name
23
+ :param issuer: issuer
18
24
  """
19
25
  self.interval = interval
20
- super(TOTP, self).__init__(*args, **kwargs)
26
+ super().__init__(s=s, digits=digits, digest=digest, name=name, issuer=issuer)
21
27
 
22
28
  def at(self, for_time: Union[int, datetime.datetime], counter_offset: int = 0) -> str:
23
29
  """
@@ -64,7 +70,9 @@ class TOTP(OTP):
64
70
 
65
71
  return utils.strings_equal(str(otp), str(self.at(for_time)))
66
72
 
67
- def provisioning_uri(self, name: Optional[str] = None, issuer_name: Optional[str] = None) -> str:
73
+ def provisioning_uri(self, name: Optional[str] = None, issuer_name: Optional[str] = None,
74
+ image: Optional[str] = None) -> str:
75
+
68
76
  """
69
77
  Returns the provisioning URI for the OTP. This can then be
70
78
  encoded in a QR Code and used to provision an OTP app like
@@ -77,8 +85,16 @@ class TOTP(OTP):
77
85
  return utils.build_uri(self.secret, name if name else self.name,
78
86
  issuer=issuer_name if issuer_name else self.issuer,
79
87
  algorithm=self.digest().name,
80
- digits=self.digits, period=self.interval)
88
+ digits=self.digits, period=self.interval, image=image)
81
89
 
82
90
  def timecode(self, for_time: datetime.datetime) -> int:
83
- i = time.mktime(for_time.timetuple())
84
- return int(i / self.interval)
91
+ """
92
+ Accepts either a timezone naive (`for_time.tzinfo is None`) or
93
+ a timezone aware datetime as argument and returns the
94
+ corresponding counter value (timecode).
95
+
96
+ """
97
+ if for_time.tzinfo:
98
+ return int(calendar.timegm(for_time.utctimetuple()) / self.interval)
99
+ else:
100
+ return int(time.mktime(for_time.timetuple()) / self.interval)
@@ -1,18 +1,12 @@
1
- from typing import Dict, Optional, Union
2
-
3
1
  import unicodedata
4
2
  from hmac import compare_digest
5
- from urllib.parse import quote, urlencode
3
+ from typing import Dict, Optional, Union
4
+ from urllib.parse import quote, urlencode, urlparse
6
5
 
7
6
 
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:
7
+ def build_uri(secret: str, name: str, initial_count: Optional[int] = None, issuer: Optional[str] = None,
8
+ algorithm: Optional[str] = None, digits: Optional[int] = None, period: Optional[int] = None,
9
+ image: Optional[str] = None) -> str:
16
10
  """
17
11
  Returns the provisioning URI for the OTP; works for either TOTP or HOTP.
18
12
 
@@ -34,6 +28,7 @@ def build_uri(
34
28
  :param digits: the length of the OTP generated code.
35
29
  :param period: the number of seconds the OTP generator is set to
36
30
  expire every code.
31
+ :param image: optional logo image url
37
32
  :returns: provisioning uri
38
33
  """
39
34
  # initial_count may be 0 as a valid param
@@ -62,6 +57,11 @@ def build_uri(
62
57
  url_args['digits'] = digits
63
58
  if is_period_set:
64
59
  url_args['period'] = period
60
+ if image:
61
+ image_uri = urlparse(image)
62
+ if image_uri.scheme != 'https' or not image_uri.netloc or not image_uri.path:
63
+ raise ValueError('{} is not a valid url'.format(image_uri))
64
+ url_args['image'] = image
65
65
 
66
66
  uri = base_uri.format(otp_type, label, urlencode(url_args).replace("+", "%20"))
67
67
  return uri
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 1.1
2
2
  Name: pyotp
3
- Version: 2.4.0
3
+ Version: 2.5.0
4
4
  Summary: Python One Time Password Library
5
5
  Home-page: https://github.com/pyotp/pyotp
6
6
  Author: PyOTP contributors
@@ -138,7 +138,7 @@ Description: PyOTP - The Python One-Time Password Library
138
138
  ~~~~~
139
139
 
140
140
  * `Project home page (GitHub) <https://github.com/pyauth/pyotp>`_
141
- * `Documentation (Read the Docs) <https://pyotp.readthedocs.io/en/latest/>`_
141
+ * `Documentation <https://pyauth.github.io/pyotp/>`_
142
142
  * `Package distribution (PyPI) <https://pypi.python.org/pypi/pyotp>`_
143
143
  * `Change log <https://github.com/pyauth/pyotp/blob/master/Changes.rst>`_
144
144
  * `RFC 4226: HOTP: An HMAC-Based One-Time Password <https://tools.ietf.org/html/rfc4226>`_
@@ -291,14 +291,14 @@ class TOTPExampleValuesFromTheRFC(unittest.TestCase):
291
291
  )
292
292
 
293
293
  def test_random_key_generation(self):
294
- self.assertEqual(len(pyotp.random_base32()), 16)
295
- self.assertEqual(len(pyotp.random_base32(length=20)), 20)
294
+ self.assertEqual(len(pyotp.random_base32()), 26)
295
+ self.assertEqual(len(pyotp.random_base32(length=32)), 32)
296
296
  self.assertEqual(len(pyotp.random_hex()), 32)
297
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)
298
+ with self.assertRaises(ValueError):
299
+ pyotp.random_base32(length=25)
300
+ with self.assertRaises(ValueError):
301
+ pyotp.random_hex(length=31)
302
302
 
303
303
 
304
304
  class CompareDigestTest(unittest.TestCase):
@@ -339,6 +339,7 @@ class ValidWindowTest(unittest.TestCase):
339
339
  self.assertTrue(totp.verify("681610", 200, 1))
340
340
  self.assertFalse(totp.verify("195979", 200, 1))
341
341
 
342
+
342
343
  class ParseUriTest(unittest.TestCase):
343
344
  def test_invalids(self):
344
345
  with self.assertRaises(ValueError) as cm:
@@ -359,7 +360,7 @@ class ParseUriTest(unittest.TestCase):
359
360
 
360
361
  with self.assertRaises(ValueError) as cm:
361
362
  pyotp.parse_uri('otpauth://totp?digits=-1')
362
- self.assertEqual('Digits may only be 6 or 8', str(cm.exception))
363
+ self.assertEqual('Digits may only be 6, 7, or 8', str(cm.exception))
363
364
 
364
365
  with self.assertRaises(ValueError) as cm:
365
366
  pyotp.parse_uri('otpauth://totp/SomeIssuer:?issuer=AnotherIssuer')
@@ -369,15 +370,52 @@ class ParseUriTest(unittest.TestCase):
369
370
  pyotp.parse_uri('otpauth://totp?algorithm=aes')
370
371
  self.assertEqual('Invalid value for algorithm, must be SHA1, SHA256 or SHA512', str(cm.exception))
371
372
 
373
+ @unittest.skipIf(sys.version_info < (3, 6), "Skipping test that requires deterministic dict key enumeration")
372
374
  def test_algorithms(self):
373
- otp = pyotp.parse_uri('otpauth://totp?algorithm=SHA1&secret=123456&algorithm=SHA1')
375
+ otp = pyotp.parse_uri('otpauth://totp?algorithm=SHA1&secret=GEZDGNBV&algorithm=SHA1')
374
376
  self.assertEqual(hashlib.sha1, otp.digest)
377
+ self.assertEqual(otp.at(0), '734055')
378
+ self.assertEqual(otp.at(30), '662488')
379
+ self.assertEqual(otp.at(60), '289363')
380
+ self.assertEqual(otp.provisioning_uri(), 'otpauth://totp/Secret?secret=GEZDGNBV')
381
+ self.assertEqual(otp.provisioning_uri(name='n', issuer_name='i'), 'otpauth://totp/i:n?secret=GEZDGNBV&issuer=i')
375
382
 
376
- otp = pyotp.parse_uri('otpauth://totp?algorithm=SHA1&secret=123456&algorithm=SHA256')
383
+ otp = pyotp.parse_uri('otpauth://totp?algorithm=SHA1&secret=GEZDGNBV&algorithm=SHA1&period=60')
384
+ self.assertEqual(hashlib.sha1, otp.digest)
385
+ self.assertEqual(otp.at(30), '734055')
386
+ self.assertEqual(otp.at(60), '662488')
387
+ self.assertEqual(otp.provisioning_uri(name='n', issuer_name='i'),
388
+ 'otpauth://totp/i:n?secret=GEZDGNBV&issuer=i&period=60')
389
+
390
+ otp = pyotp.parse_uri('otpauth://hotp?algorithm=SHA1&secret=GEZDGNBV&algorithm=SHA1')
391
+ self.assertEqual(hashlib.sha1, otp.digest)
392
+ self.assertEqual(otp.at(0), '734055')
393
+ self.assertEqual(otp.at(1), '662488')
394
+ self.assertEqual(otp.at(2), '289363')
395
+ self.assertEqual(otp.provisioning_uri(name='n', issuer_name='i'),
396
+ 'otpauth://hotp/i:n?secret=GEZDGNBV&issuer=i&counter=0')
397
+
398
+ otp = pyotp.parse_uri('otpauth://hotp?algorithm=SHA1&secret=GEZDGNBV&algorithm=SHA1&counter=1')
399
+ self.assertEqual(hashlib.sha1, otp.digest)
400
+ self.assertEqual(otp.at(0), '662488')
401
+ self.assertEqual(otp.at(1), '289363')
402
+ self.assertEqual(otp.provisioning_uri(name='n', issuer_name='i'),
403
+ 'otpauth://hotp/i:n?secret=GEZDGNBV&issuer=i&counter=1')
404
+
405
+ otp = pyotp.parse_uri('otpauth://totp?algorithm=SHA1&secret=GEZDGNBV&algorithm=SHA256')
377
406
  self.assertEqual(hashlib.sha256, otp.digest)
407
+ self.assertEqual(otp.at(0), '918961')
408
+ self.assertEqual(otp.at(9000), '934470')
378
409
 
379
- otp = pyotp.parse_uri('otpauth://totp?algorithm=SHA1&secret=123456&algorithm=SHA512')
410
+ otp = pyotp.parse_uri('otpauth://totp?algorithm=SHA1&secret=GEZDGNBV&algorithm=SHA512')
380
411
  self.assertEqual(hashlib.sha512, otp.digest)
412
+ self.assertEqual(otp.at(0), '816660')
413
+ self.assertEqual(otp.at(9000), '524153')
414
+
415
+ self.assertEqual(otp.provisioning_uri(name='n', issuer_name='i', image='https://test.net/test.png'),
416
+ 'otpauth://totp/i:n?secret=GEZDGNBV&issuer=i&algorithm=SHA512&image=https%3A%2F%2Ftest.net%2Ftest.png')
417
+ with self.assertRaises(ValueError):
418
+ otp.provisioning_uri(name='n', issuer_name='i', image='nourl')
381
419
 
382
420
  class Timecop(object):
383
421
  """
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes