[Python-modules-commits] [python-pyotp] 01/03: Imported Upstream version 2.2.1
Hugo Lefeuvre
hle at moszumanska.debian.org
Fri Sep 9 16:46:15 UTC 2016
This is an automated email from the git hooks/post-receive script.
hle pushed a commit to branch master
in repository python-pyotp.
commit 5cfd91b036162b0dd6d189943de55255841b2499
Author: Hugo Lefeuvre <hle at debian.org>
Date: Fri Sep 9 17:41:48 2016 +0200
Imported Upstream version 2.2.1
---
LICENSE | 7 +-
PKG-INFO | 12 ++-
README.rst | 10 +--
requirements.txt | 1 -
setup.cfg | 4 +
setup.py | 7 +-
src/pyotp.egg-info/PKG-INFO | 12 ++-
src/pyotp.egg-info/SOURCES.txt | 2 +-
src/pyotp.egg-info/requires.txt | 1 -
src/pyotp/__init__.py | 12 +--
src/pyotp/compat.py | 10 +++
src/pyotp/hotp.py | 10 ++-
src/pyotp/otp.py | 18 ++---
src/pyotp/totp.py | 21 ++---
src/pyotp/utils.py | 89 +++++++++++++---------
test.py | 164 ++++++++++++++++++++++++++++++++--------
16 files changed, 256 insertions(+), 124 deletions(-)
diff --git a/LICENSE b/LICENSE
index c6b2e22..6e2abc1 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,5 @@
-Copyright (C) 2011 by Mark Percival <m at mdp.im>
-
-Python port copyright 2011 by Nathan Reynolds <email at nreynolds.co.uk>
+Copyright (C) 2011-2016 Mark Percival <m at mdp.im>,
+Nathan Reynolds <email at nreynolds.co.uk>, and PyOTP contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
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
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
\ No newline at end of file
+THE SOFTWARE.
diff --git a/PKG-INFO b/PKG-INFO
index 301da3e..1a62fa1 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: pyotp
-Version: 2.1.1
+Version: 2.2.1
Summary: Python One Time Password Library
Home-page: https://github.com/pyotp/pyotp
Author: PyOTP contributors
@@ -84,13 +84,13 @@ Description: PyOTP - The Python One-Time Password Library
Scan the following barcode with your phone, using Google Authenticator
- 
+ .. image:: http://chart.apis.google.com/chart?cht=qr&chs=250x250&chl=otpauth%3A%2F%2Ftotp%2Falice%40google.com%3Fsecret%3DJBSWY3DPEHPK3PXP
Now run the following and compare the output::
import pyotp
totp = pyotp.TOTP("JBSWY3DPEHPK3PXP")
- print "Current OTP: %s" % totp.now()
+ print("Current OTP:", totp.now())
Links
~~~~~
@@ -106,12 +106,10 @@ Description: PyOTP - The Python One-Time Password Library
.. image:: https://img.shields.io/travis/pyotp/pyotp.svg
:target: https://travis-ci.org/pyotp/pyotp
- .. image:: https://img.shields.io/coveralls/pyotp/pyotp.svg
- :target: https://coveralls.io/r/pyotp/pyotp?branch=master
+ .. image:: https://img.shields.io/codecov/c/github/pyotp/pyotp/master.svg
+ :target: https://codecov.io/github/pyotp/pyotp?branch=master
.. image:: https://img.shields.io/pypi/v/pyotp.svg
:target: https://pypi.python.org/pypi/pyotp
- .. image:: https://img.shields.io/pypi/dm/pyotp.svg
- :target: https://pypi.python.org/pypi/pyotp
.. image:: https://img.shields.io/pypi/l/pyotp.svg
:target: https://pypi.python.org/pypi/pyotp
.. image:: https://readthedocs.org/projects/pyotp/badge/?version=latest
diff --git a/README.rst b/README.rst
index 8d89f0b..7c5d90a 100644
--- a/README.rst
+++ b/README.rst
@@ -76,13 +76,13 @@ Working example
Scan the following barcode with your phone, using Google Authenticator
-
+.. image:: http://chart.apis.google.com/chart?cht=qr&chs=250x250&chl=otpauth%3A%2F%2Ftotp%2Falice%40google.com%3Fsecret%3DJBSWY3DPEHPK3PXP
Now run the following and compare the output::
import pyotp
totp = pyotp.TOTP("JBSWY3DPEHPK3PXP")
- print "Current OTP: %s" % totp.now()
+ print("Current OTP:", totp.now())
Links
~~~~~
@@ -98,12 +98,10 @@ Links
.. image:: https://img.shields.io/travis/pyotp/pyotp.svg
:target: https://travis-ci.org/pyotp/pyotp
-.. image:: https://img.shields.io/coveralls/pyotp/pyotp.svg
- :target: https://coveralls.io/r/pyotp/pyotp?branch=master
+.. image:: https://img.shields.io/codecov/c/github/pyotp/pyotp/master.svg
+ :target: https://codecov.io/github/pyotp/pyotp?branch=master
.. image:: https://img.shields.io/pypi/v/pyotp.svg
:target: https://pypi.python.org/pypi/pyotp
-.. image:: https://img.shields.io/pypi/dm/pyotp.svg
- :target: https://pypi.python.org/pypi/pyotp
.. image:: https://img.shields.io/pypi/l/pyotp.svg
:target: https://pypi.python.org/pypi/pyotp
.. image:: https://readthedocs.org/projects/pyotp/badge/?version=latest
diff --git a/requirements.txt b/requirements.txt
index 970c6de..e69de29 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +0,0 @@
-future==0.15.2
diff --git a/setup.cfg b/setup.cfg
index 6f08d0e..4644c25 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,10 @@
[bdist_wheel]
universal = 1
+[flake8]
+max-line-length = 120
+ignore = E301, E302, E401, E226, F841
+
[egg_info]
tag_build =
tag_date = 0
diff --git a/setup.py b/setup.py
index 1d7ed2c..27cf6a3 100755
--- a/setup.py
+++ b/setup.py
@@ -1,13 +1,13 @@
#!/usr/bin/env python
-import os, glob
-from setuptools import setup, find_packages
+import os
+from setuptools import setup
install_requires = [line.rstrip() for line in open(os.path.join(os.path.dirname(__file__), "requirements.txt"))]
setup(
name='pyotp',
- version='2.1.1',
+ version='2.2.1',
url='https://github.com/pyotp/pyotp',
license='BSD License',
author='PyOTP contributors',
@@ -19,6 +19,7 @@ setup(
package_dir={'': 'src'},
platforms=['MacOS X', 'Posix'],
zip_safe=False,
+ test_suite="test",
classifiers=[
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
diff --git a/src/pyotp.egg-info/PKG-INFO b/src/pyotp.egg-info/PKG-INFO
index 301da3e..1a62fa1 100644
--- a/src/pyotp.egg-info/PKG-INFO
+++ b/src/pyotp.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: pyotp
-Version: 2.1.1
+Version: 2.2.1
Summary: Python One Time Password Library
Home-page: https://github.com/pyotp/pyotp
Author: PyOTP contributors
@@ -84,13 +84,13 @@ Description: PyOTP - The Python One-Time Password Library
Scan the following barcode with your phone, using Google Authenticator
- 
+ .. image:: http://chart.apis.google.com/chart?cht=qr&chs=250x250&chl=otpauth%3A%2F%2Ftotp%2Falice%40google.com%3Fsecret%3DJBSWY3DPEHPK3PXP
Now run the following and compare the output::
import pyotp
totp = pyotp.TOTP("JBSWY3DPEHPK3PXP")
- print "Current OTP: %s" % totp.now()
+ print("Current OTP:", totp.now())
Links
~~~~~
@@ -106,12 +106,10 @@ Description: PyOTP - The Python One-Time Password Library
.. image:: https://img.shields.io/travis/pyotp/pyotp.svg
:target: https://travis-ci.org/pyotp/pyotp
- .. image:: https://img.shields.io/coveralls/pyotp/pyotp.svg
- :target: https://coveralls.io/r/pyotp/pyotp?branch=master
+ .. image:: https://img.shields.io/codecov/c/github/pyotp/pyotp/master.svg
+ :target: https://codecov.io/github/pyotp/pyotp?branch=master
.. image:: https://img.shields.io/pypi/v/pyotp.svg
:target: https://pypi.python.org/pypi/pyotp
- .. image:: https://img.shields.io/pypi/dm/pyotp.svg
- :target: https://pypi.python.org/pypi/pyotp
.. image:: https://img.shields.io/pypi/l/pyotp.svg
:target: https://pypi.python.org/pypi/pyotp
.. image:: https://readthedocs.org/projects/pyotp/badge/?version=latest
diff --git a/src/pyotp.egg-info/SOURCES.txt b/src/pyotp.egg-info/SOURCES.txt
index 2ded9a4..e1e9677 100644
--- a/src/pyotp.egg-info/SOURCES.txt
+++ b/src/pyotp.egg-info/SOURCES.txt
@@ -6,6 +6,7 @@ setup.cfg
setup.py
test.py
src/pyotp/__init__.py
+src/pyotp/compat.py
src/pyotp/hotp.py
src/pyotp/otp.py
src/pyotp/totp.py
@@ -14,5 +15,4 @@ src/pyotp.egg-info/PKG-INFO
src/pyotp.egg-info/SOURCES.txt
src/pyotp.egg-info/dependency_links.txt
src/pyotp.egg-info/not-zip-safe
-src/pyotp.egg-info/requires.txt
src/pyotp.egg-info/top_level.txt
\ No newline at end of file
diff --git a/src/pyotp.egg-info/requires.txt b/src/pyotp.egg-info/requires.txt
deleted file mode 100644
index 970c6de..0000000
--- a/src/pyotp.egg-info/requires.txt
+++ /dev/null
@@ -1 +0,0 @@
-future==0.15.2
diff --git a/src/pyotp/__init__.py b/src/pyotp/__init__.py
index deafdc6..35b6509 100644
--- a/src/pyotp/__init__.py
+++ b/src/pyotp/__init__.py
@@ -1,11 +1,13 @@
-from __future__ import print_function, unicode_literals, division, absolute_import
+from __future__ import (absolute_import, division,
+ print_function, unicode_literals)
import random as _random
-from pyotp.hotp import HOTP
-from pyotp.otp import OTP
-from pyotp.totp import TOTP
-from . import utils
+from pyotp.hotp import HOTP # noqa
+from pyotp.otp import OTP # noqa
+from pyotp.totp import TOTP # noqa
+from . import utils # noqa
+
def random_base32(length=16, random=_random.SystemRandom(),
chars=list('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')):
diff --git a/src/pyotp/compat.py b/src/pyotp/compat.py
new file mode 100644
index 0000000..5649909
--- /dev/null
+++ b/src/pyotp/compat.py
@@ -0,0 +1,10 @@
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+import sys
+
+USING_PYTHON2 = True if sys.version_info < (3, 0) else False
+
+if USING_PYTHON2:
+ str = unicode # noqa
+else:
+ str = str
diff --git a/src/pyotp/hotp.py b/src/pyotp/hotp.py
index 603e8a5..40ddcfa 100644
--- a/src/pyotp/hotp.py
+++ b/src/pyotp/hotp.py
@@ -1,8 +1,8 @@
-from __future__ import print_function, unicode_literals, division, absolute_import
+from __future__ import absolute_import, division, print_function, unicode_literals
-from pyotp.otp import OTP
-from pyotp import utils
-from future.builtins import str
+from . import utils
+from .otp import OTP
+from .compat import str
class HOTP(OTP):
def at(self, count):
@@ -37,4 +37,6 @@ class HOTP(OTP):
name,
initial_count=initial_count,
issuer_name=issuer_name,
+ algorithm=self.digest().name,
+ digits=self.digits
)
diff --git a/src/pyotp/otp.py b/src/pyotp/otp.py
index f3144d1..f0fe49f 100644
--- a/src/pyotp/otp.py
+++ b/src/pyotp/otp.py
@@ -1,9 +1,9 @@
-from __future__ import print_function, unicode_literals, division, absolute_import
+from __future__ import absolute_import, division, print_function, unicode_literals
import base64
import hashlib
import hmac
-from future.builtins import str
+from .compat import str
class OTP(object):
def __init__(self, s, digits=6, digest=hashlib.sha1):
@@ -27,13 +27,8 @@ class OTP(object):
Usually either the counter, or the computed integer
based on the Unix timestamp
"""
- hmac_hash = hmac.new(
- self.byte_secret(),
- self.int_to_bytestring(input),
- self.digest,
- ).digest()
-
- hmac_hash = bytearray(hmac_hash)
+ hasher = hmac.new(self.byte_secret(), self.int_to_bytestring(input), self.digest)
+ hmac_hash = bytearray(hasher.digest())
offset = hmac_hash[-1] & 0xf
code = ((hmac_hash[offset] & 0x7f) << 24 |
(hmac_hash[offset + 1] & 0xff) << 16 |
@@ -62,6 +57,7 @@ class OTP(object):
while i != 0:
result.append(i & 0xFF)
i >>= 8
- # It's necessary to convert the final result from bytearray to bytes because
- # the hmac functions in python 2.6 and 3.3 don't work with bytearray
+ # It's necessary to convert the final result from bytearray to bytes
+ # because the hmac functions in python 2.6 and 3.3 don't work with
+ # bytearray
return bytes(bytearray(reversed(result)).rjust(padding, b'\0'))
diff --git a/src/pyotp/totp.py b/src/pyotp/totp.py
index 3026fe0..27f81b8 100644
--- a/src/pyotp/totp.py
+++ b/src/pyotp/totp.py
@@ -1,11 +1,11 @@
-from __future__ import print_function, unicode_literals, division, absolute_import
+from __future__ import absolute_import, division, print_function, unicode_literals
import datetime
import time
-from pyotp import utils
-from pyotp.otp import OTP
-from future.builtins import str
+from . import utils
+from .otp import OTP
+from .compat import str
class TOTP(OTP):
def __init__(self, *args, **kwargs):
@@ -19,9 +19,9 @@ class TOTP(OTP):
def at(self, for_time, counter_offset=0):
"""
Accepts either a Unix timestamp integer or a Time object.
- Time objects will be adjusted to UTC automatically
@param [Time/Integer] time the time to generate an OTP for
- @param [Integer] counter_offset an amount of ticks to add to the time counter
+ @param [Integer] counter_offset an amount of ticks to add to the time
+ counter
"""
if not isinstance(for_time, datetime.datetime):
for_time = datetime.datetime.fromtimestamp(int(for_time))
@@ -38,11 +38,12 @@ class TOTP(OTP):
"""
Verifies the OTP passed in against the current time OTP
@param [String/Integer] otp the OTP to check against
- @param [Integer] valid_window extends the validity to this many counter ticks before and after the current one
+ @param [Integer] valid_window extends the validity to this many counter
+ ticks before and after the current one
"""
if for_time is None:
for_time = datetime.datetime.now()
-
+
if valid_window:
for i in range(-valid_window, valid_window + 1):
if utils.strings_equal(str(otp), str(self.at(for_time, i))):
@@ -59,7 +60,9 @@ class TOTP(OTP):
@param [String] name of the account
@return [String] provisioning uri
"""
- return utils.build_uri(self.secret, name, issuer_name=issuer_name)
+ return utils.build_uri(self.secret, name, issuer_name=issuer_name,
+ algorithm=self.digest().name,
+ digits=self.digits, period=self.interval)
def timecode(self, for_time):
i = time.mktime(for_time.timetuple())
diff --git a/src/pyotp/utils.py b/src/pyotp/utils.py
index f816703..16a13d5 100644
--- a/src/pyotp/utils.py
+++ b/src/pyotp/utils.py
@@ -1,13 +1,19 @@
-from __future__ import print_function, unicode_literals, division, absolute_import
+from __future__ import absolute_import, division, print_function, unicode_literals
import unicodedata
+try:
+ from itertools import izip_longest
+except ImportError:
+ from itertools import zip_longest as izip_longest
try:
- from urllib.parse import quote
+ from urllib.parse import quote, urlencode
except ImportError:
- from urllib import quote
+ from urllib import quote, urlencode
+
-def build_uri(secret, name, initial_count=None, issuer_name=None):
+def build_uri(secret, name, initial_count=None, issuer_name=None,
+ algorithm=None, digits=None, period=None):
"""
Returns the provisioning URI for the OTP; works for either TOTP or HOTP.
@@ -17,7 +23,7 @@ def build_uri(secret, name, initial_count=None, issuer_name=None):
For module-internal use.
See also:
- http://code.google.com/p/google-authenticator/wiki/KeyUriFormat
+ https://github.com/google/google-authenticator/wiki/Key-Uri-Format
@param [String] the hotp/totp secret used to generate the URI
@param [String] name of the account
@@ -25,31 +31,60 @@ def build_uri(secret, name, initial_count=None, issuer_name=None):
If none, the OTP type will be assumed as TOTP.
@param [String] the name of the OTP issuer; this will be the
organization title of the OTP entry in Authenticator
+ @param [String] the algorithm used in the OTP generation.
+ @param [Integer] the length of the OTP generated code.
+ @param [Integer] the number of seconds the OTP generator is set to
+ expire every code.
@return [String] provisioning uri
"""
# initial_count may be 0 as a valid param
is_initial_count_present = (initial_count is not None)
+ # Handling values different from defaults
+ is_algorithm_set = (algorithm is not None and algorithm != 'sha1')
+ is_digits_set = (digits is not None and digits != 6)
+ is_period_set = (period is not None and period != 30)
+
otp_type = 'hotp' if is_initial_count_present else 'totp'
- base = 'otpauth://%s/' % otp_type
+ base_uri = 'otpauth://{}/{}?{}'
- if issuer_name:
- issuer_name = quote(issuer_name)
- base += '%s:' % issuer_name
+ url_args = {'secret': secret}
- uri = '%(base)s%(name)s?secret=%(secret)s' % {
- 'name': quote(name, safe='@'),
- 'secret': secret,
- 'base': base,
- }
+ label = quote(name)
+ if issuer_name is not None:
+ label = quote(issuer_name) + ':' + label
+ url_args['issuer'] = issuer_name
if is_initial_count_present:
- uri += '&counter=%s' % initial_count
+ url_args['counter'] = initial_count
+ if is_algorithm_set:
+ url_args['algorithm'] = algorithm.upper()
+ if is_digits_set:
+ url_args['digits'] = digits
+ if is_period_set:
+ url_args['period'] = period
+
+ uri = base_uri.format(otp_type, label, urlencode(url_args))
+ return uri
- if issuer_name:
- uri += '&issuer=%s' % issuer_name
- return uri
+def _compare_digest(s1, s2):
+ differences = 0
+ for c1, c2 in izip_longest(s1, s2):
+ if c1 is None or c2 is None:
+ differences = 1
+ continue
+ differences |= ord(c1) ^ ord(c2)
+ return differences == 0
+
+try:
+ # Python 3.3+ and 2.7.7+ include a timing-attack-resistant
+ # comparison function, which is probably more reliable than ours.
+ # Use it if available.
+ from hmac import compare_digest
+
+except ImportError:
+ compare_digest = _compare_digest
def strings_equal(s1, s2):
@@ -63,20 +98,4 @@ def strings_equal(s1, s2):
"""
s1 = unicodedata.normalize('NFKC', s1)
s2 = unicodedata.normalize('NFKC', s2)
- try:
- # Python 3.3+ and 2.7.7+ include a timing-attack-resistant
- # comparison function, which is probably more reliable than ours.
- # Use it if available.
- from hmac import compare_digest
-
- return compare_digest(s1, s2)
- except ImportError:
- pass
-
- if len(s1) != len(s2):
- return False
-
- differences = 0
- for c1, c2 in zip(s1, s2):
- differences |= ord(c1) ^ ord(c2)
- return differences == 0
+ return compare_digest(s1, s2)
diff --git a/test.py b/test.py
index 3615836..395d475 100755
--- a/test.py
+++ b/test.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# coding: utf-8
-from __future__ import print_function, unicode_literals, division, absolute_import
+from __future__ import absolute_import, division, print_function, unicode_literals
import base64
import datetime
@@ -9,9 +9,13 @@ import hashlib
import os
import sys
import unittest
+try:
+ from urllib.parse import urlparse, parse_qsl
+except ImportError:
+ from urlparse import urlparse, parse_qsl
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
-import pyotp
+import pyotp # noqa
class HOTPExampleValuesFromTheRFC(unittest.TestCase):
def test_match_rfc(self):
@@ -38,26 +42,65 @@ class HOTPExampleValuesFromTheRFC(unittest.TestCase):
def test_provisioning_uri(self):
hotp = pyotp.HOTP('wrn3pqx5uqxqvnqr')
- self.assertEqual(
- hotp.provisioning_uri('mark at percival'),
- 'otpauth://hotp/mark@percival?secret=wrn3pqx5uqxqvnqr&counter=0')
-
- self.assertEqual(
- hotp.provisioning_uri('mark at percival', initial_count=12),
- 'otpauth://hotp/mark@percival?secret=wrn3pqx5uqxqvnqr&counter=12')
-
- self.assertEqual(
- hotp.provisioning_uri('mark at percival', issuer_name='FooCorp!'),
- 'otpauth://hotp/FooCorp%21:mark@percival?secret=wrn3pqx5uqxqvnqr&counter=0&issuer=FooCorp%21')
+ url = urlparse(
+ hotp.provisioning_uri('mark at percival'))
+ self.assertEqual(url.scheme, 'otpauth')
+ self.assertEqual(url.netloc, 'hotp')
+ self.assertEqual(url.path, '/mark%40percival')
+ self.assertEqual(dict(parse_qsl(url.query)),
+ {'secret': 'wrn3pqx5uqxqvnqr', 'counter': '0'})
+
+ url = urlparse(
+ hotp.provisioning_uri('mark at percival', initial_count=12))
+ self.assertEqual(url.scheme, 'otpauth')
+ self.assertEqual(url.netloc, 'hotp')
+ self.assertEqual(url.path, '/mark%40percival')
+ self.assertEqual(dict(parse_qsl(url.query)),
+ {'secret': 'wrn3pqx5uqxqvnqr', 'counter': '12'})
+
+ url = urlparse(
+ hotp.provisioning_uri('mark at percival', issuer_name='FooCorp!'))
+ self.assertEqual(url.scheme, 'otpauth')
+ self.assertEqual(url.netloc, 'hotp')
+ self.assertEqual(url.path, '/FooCorp%21:mark%40percival')
+ self.assertEqual(dict(parse_qsl(url.query)),
+ {'secret': 'wrn3pqx5uqxqvnqr', 'counter': '0',
+ 'issuer': 'FooCorp!'})
+
+ key = 'c7uxuqhgflpw7oruedmglbrk7u6242vb'
+ hotp = pyotp.HOTP(key, digits=8, digest=hashlib.sha256)
+ url = urlparse(
+ hotp.provisioning_uri('baco at peperina', issuer_name='FooCorp'))
+ self.assertEqual(url.scheme, 'otpauth')
+ self.assertEqual(url.netloc, 'hotp')
+ self.assertEqual(url.path, '/FooCorp:baco%40peperina')
+ self.assertEqual(dict(parse_qsl(url.query)),
+ {'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb',
+ 'counter': '0', 'issuer': 'FooCorp',
+ 'digits': '8', 'algorithm': 'SHA256'})
+
+ hotp = pyotp.HOTP(key, digits=8)
+ url = urlparse(
+ hotp.provisioning_uri('baco at peperina', issuer_name='FooCorp',
+ initial_count=10))
+ self.assertEqual(url.scheme, 'otpauth')
+ self.assertEqual(url.netloc, 'hotp')
+ self.assertEqual(url.path, '/FooCorp:baco%40peperina')
+ self.assertEqual(dict(parse_qsl(url.query)),
+ {'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb',
+ 'counter': '10', 'issuer': 'FooCorp',
+ 'digits': '8'})
def test_other_secret(self):
- hotp = pyotp.HOTP('N3OVNIBRERIO5OHGVCMDGS4V4RJ3AUZOUN34J6FRM4P6JIFCG3ZA')
+ hotp = pyotp.HOTP(
+ 'N3OVNIBRERIO5OHGVCMDGS4V4RJ3AUZOUN34J6FRM4P6JIFCG3ZA')
self.assertEqual(hotp.at(0), '737863')
self.assertEqual(hotp.at(1), '390601')
self.assertEqual(hotp.at(2), '363354')
self.assertEqual(hotp.at(3), '936780')
self.assertEqual(hotp.at(4), '654019')
+
class TOTPExampleValuesFromTheRFC(unittest.TestCase):
RFC_VALUES = {
(hashlib.sha1, b'12345678901234567890'): (
@@ -78,7 +121,9 @@ class TOTPExampleValuesFromTheRFC(unittest.TestCase):
(20000000000, '77737706'),
),
- (hashlib.sha512, b'1234567890123456789012345678901234567890123456789012345678901234'): (
+ (hashlib.sha512,
+ b'1234567890123456789012345678901234567890123456789012345678901234'):
+ (
(59, 90693936),
(1111111109, '25091201'),
(1111111111, '99943326'),
@@ -92,6 +137,10 @@ class TOTPExampleValuesFromTheRFC(unittest.TestCase):
for digest, secret in self.RFC_VALUES:
totp = pyotp.TOTP(base64.b32encode(secret), 8, digest)
for utime, code in self.RFC_VALUES[(digest, secret)]:
+ # 32-bit platforms use native functions to handle timestamps, so they fail this test
+ # (and will fail after 19 January 2038)
+ if utime > sys.maxsize:
+ continue
value = totp.at(utime)
msg = "%s != %s (%s, time=%d)"
msg %= (value, code, digest().name, utime)
@@ -125,30 +174,83 @@ class TOTPExampleValuesFromTheRFC(unittest.TestCase):
def test_provisioning_uri(self):
totp = pyotp.TOTP('wrn3pqx5uqxqvnqr')
- self.assertEqual(
- totp.provisioning_uri('mark at percival'),
- 'otpauth://totp/mark@percival?secret=wrn3pqx5uqxqvnqr')
-
- self.assertEqual(
- totp.provisioning_uri('mark at percival', issuer_name='FooCorp!'),
- 'otpauth://totp/FooCorp%21:mark@percival?secret=wrn3pqx5uqxqvnqr&issuer=FooCorp%21')
+ url = urlparse(
+ totp.provisioning_uri('mark at percival'))
+ self.assertEqual(url.scheme, 'otpauth')
+ self.assertEqual(url.netloc, 'totp')
+ self.assertEqual(url.path, '/mark%40percival')
+ self.assertEqual(dict(parse_qsl(url.query)),
+ {'secret': 'wrn3pqx5uqxqvnqr'})
+
+ url = urlparse(
+ totp.provisioning_uri('mark at percival', issuer_name='FooCorp!'))
+ self.assertEqual(url.scheme, 'otpauth')
+ self.assertEqual(url.netloc, 'totp')
+ self.assertEqual(url.path, '/FooCorp%21:mark%40percival')
+ self.assertEqual(dict(parse_qsl(url.query)),
+ {'secret': 'wrn3pqx5uqxqvnqr',
+ 'issuer': 'FooCorp!'})
+
+ key = 'c7uxuqhgflpw7oruedmglbrk7u6242vb'
+ totp = pyotp.TOTP(key, digits=8, interval=60, digest=hashlib.sha256)
+ url = urlparse(totp.provisioning_uri('baco at peperina', issuer_name='FooCorp'))
+ self.assertEqual(url.scheme, 'otpauth')
+ self.assertEqual(url.netloc, 'totp')
+ self.assertEqual(url.path, '/FooCorp:baco%40peperina')
+ self.assertEqual(dict(parse_qsl(url.query)),
+ {'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb',
+ 'issuer': 'FooCorp',
+ 'digits': '8', 'period': '60',
+ 'algorithm': 'SHA256'})
+
+ totp = pyotp.TOTP(key, digits=8, interval=60)
+ url = urlparse(totp.provisioning_uri('baco at peperina', issuer_name='FooCorp'))
+ self.assertEqual(url.scheme, 'otpauth')
+ self.assertEqual(url.netloc, 'totp')
+ self.assertEqual(url.path, '/FooCorp:baco%40peperina')
+ self.assertEqual(dict(parse_qsl(url.query)),
+ {'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb',
+ 'issuer': 'FooCorp',
+ 'digits': '8', 'period': '60'})
+
+ totp = pyotp.TOTP(key, digits=8)
+ url = urlparse(totp.provisioning_uri('baco at peperina', issuer_name='FooCorp'))
+ self.assertEqual(url.scheme, 'otpauth')
+ self.assertEqual(url.netloc, 'totp')
+ self.assertEqual(url.path, '/FooCorp:baco%40peperina')
+ self.assertEqual(dict(parse_qsl(url.query)),
+ {'secret': 'c7uxuqhgflpw7oruedmglbrk7u6242vb',
+ 'issuer': 'FooCorp',
+ 'digits': '8'})
def test_random_key_generation(self):
self.assertEqual(len(pyotp.random_base32()), 16)
self.assertEqual(len(pyotp.random_base32(length=20)), 20)
-class StringComparisonTest(unittest.TestCase):
+
+class CompareDigestTest(unittest.TestCase):
+ method = staticmethod(pyotp.utils.compare_digest)
+
def test_comparisons(self):
- self.assertTrue(pyotp.utils.strings_equal("", ""))
- self.assertTrue(pyotp.utils.strings_equal("a", "a"))
- self.assertTrue(pyotp.utils.strings_equal("a" * 1000, "a" * 1000))
+ self.assertTrue(self.method("", ""))
+ self.assertTrue(self.method("a", "a"))
+ self.assertTrue(self.method("a" * 1000, "a" * 1000))
+
+ self.assertFalse(self.method("", "a"))
+ self.assertFalse(self.method("a", ""))
+ self.assertFalse(self.method("a" * 999 + "b", "a" * 1000))
- self.assertFalse(pyotp.utils.strings_equal("", "a"))
- self.assertFalse(pyotp.utils.strings_equal("a", ""))
- self.assertFalse(pyotp.utils.strings_equal("a" * 999 + "b", "a" * 1000))
+
+class FallBackCompareDigestTest(CompareDigestTest):
+ method = staticmethod(pyotp.utils._compare_digest)
+
+
+class StringComparisonTest(CompareDigestTest):
+ method = staticmethod(pyotp.utils.strings_equal)
def test_fullwidth_input(self):
- self.assertTrue(pyotp.utils.strings_equal("xs12345", "xs12345"))
+ self.assertTrue(self.method("xs12345", "xs12345"))
+
class CounterOffsetTest(unittest.TestCase):
def test_counter_offset(self):
@@ -156,6 +258,7 @@ class CounterOffsetTest(unittest.TestCase):
self.assertEqual(totp.at(200), "028307")
self.assertTrue(totp.at(200, 1), "681610")
+
class ValidWindowTest(unittest.TestCase):
def test_valid_window(self):
totp = pyotp.TOTP("ABCDEFGH")
@@ -164,6 +267,7 @@ class ValidWindowTest(unittest.TestCase):
self.assertTrue(totp.verify("681610", 200, 1))
self.assertFalse(totp.verify("195979", 200, 1))
+
class Timecop(object):
"""
Half-assed clone of timecop.rb, just enough to pass our tests.
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-pyotp.git
More information about the Python-modules-commits
mailing list