summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2016-11-10 20:17:07 -0500
committerEli Collins <elic@assurancetechnologies.com>2016-11-10 20:17:07 -0500
commitec573fe344d3d2574f8a657bc701fd18d91f3d22 (patch)
tree7de5e80a595ae23087d2f37746bcca0634b06bb1
parent47b1d1524dbde0e2ff004f6b2c76f9038cf5958f (diff)
downloadpasslib-ec573fe344d3d2574f8a657bc701fd18d91f3d22.tar.gz
totp: TOTP.normalize_token() turned into hybrid method, made public;
TOTP.normalize_time() turned into class method, made public.
-rw-r--r--docs/lib/passlib.totp.rst16
-rw-r--r--passlib/tests/test_totp.py33
-rw-r--r--passlib/totp.py21
-rw-r--r--passlib/utils/__init__.py19
4 files changed, 68 insertions, 21 deletions
diff --git a/docs/lib/passlib.totp.rst b/docs/lib/passlib.totp.rst
index d9cd5cc..7a891fb 100644
--- a/docs/lib/passlib.totp.rst
+++ b/docs/lib/passlib.totp.rst
@@ -66,13 +66,6 @@ Most of this information will be serialized by :meth:`TOTP.to_uri` and :meth:`TO
.. autoattribute:: TOTP.alg
.. autoattribute:: TOTP.period
-..
- Undocumented Helper Methods
- ---------------------------
-
- .. automethod:: TOTP.normalize_token
- .. automethod:: TOTP.normalize_time
-
Token Generation
================
Token generation is generally useful client-side, and for generating
@@ -157,6 +150,15 @@ these methods will automatically encrypt the resulting keys.
* The :ref:`totp-storing-instances` tutorial for more details.
+Helper Methods
+==============
+While :meth:`TOTP.generate`, :meth:`TOTP.match`, and :meth:`TOTP.verify`
+automatically handle normalizing tokens & time values, the following methods
+are exposed in case they are useful in other contexts:
+
+.. automethod:: TOTP.normalize_token
+.. automethod:: TOTP.normalize_time
+
AppWallet
=========
The :class:`!AppWallet` class is used internally by the :meth:`TOTP.using` method
diff --git a/passlib/tests/test_totp.py b/passlib/tests/test_totp.py
index 1c7c27e..41cc25e 100644
--- a/passlib/tests/test_totp.py
+++ b/passlib/tests/test_totp.py
@@ -699,25 +699,40 @@ class TotpTest(TestCase):
# internal method tests
#=============================================================================
- def test_normalize_token(self):
- """normalize_token()"""
- otp = self.randotp(digits=7)
+ def test_normalize_token_instance(self, otp=None):
+ """normalize_token() -- instance method"""
+ if otp is None:
+ otp = self.randotp(digits=7)
- self.assertEqual(otp.normalize_token('1234567'), '1234567')
+ # unicode & bytes
+ self.assertEqual(otp.normalize_token(u('1234567')), '1234567')
self.assertEqual(otp.normalize_token(b'1234567'), '1234567')
+ # int
self.assertEqual(otp.normalize_token(1234567), '1234567')
+
+ # int which needs 0 padding
self.assertEqual(otp.normalize_token(234567), '0234567')
+ # reject wrong types (float, None)
self.assertRaises(TypeError, otp.normalize_token, 1234567.0)
self.assertRaises(TypeError, otp.normalize_token, None)
+ # too few digits
self.assertRaises(exc.MalformedTokenError, otp.normalize_token, '123456')
+
+ # too many digits
self.assertRaises(exc.MalformedTokenError, otp.normalize_token, '01234567')
+ self.assertRaises(exc.MalformedTokenError, otp.normalize_token, 12345678)
+
+ def test_normalize_token_class(self):
+ """normalize_token() -- class method"""
+ self.test_normalize_token_instance(otp=TOTP.using(digits=7))
def test_normalize_time(self):
"""normalize_time()"""
- otp = self.randotp()
+ TotpFactory = TOTP.using()
+ otp = self.randotp(TotpFactory)
for _ in range(10):
time = randtime()
@@ -731,8 +746,12 @@ class TotpTest(TestCase):
dt = datetime.datetime.utcfromtimestamp(time)
self.assertEqual(otp.normalize_time(dt), tint)
- otp.now = lambda: time
- self.assertEqual(otp.normalize_time(None), tint)
+ orig = TotpFactory.now
+ try:
+ TotpFactory.now = staticmethod(lambda: time)
+ self.assertEqual(otp.normalize_time(None), tint)
+ finally:
+ TotpFactory.now = orig
self.assertRaises(TypeError, otp.normalize_time, '1234')
diff --git a/passlib/totp.py b/passlib/totp.py
index f097e7c..9c8c7ec 100644
--- a/passlib/totp.py
+++ b/passlib/totp.py
@@ -35,7 +35,7 @@ except ImportError:
# pkg
from passlib import exc
from passlib.exc import TokenError, MalformedTokenError, InvalidTokenError, UsedTokenError
-from passlib.utils import (to_unicode, to_bytes, consteq, memoized_property,
+from passlib.utils import (to_unicode, to_bytes, consteq, memoized_property, hybrid_method,
getrandbytes, rng, SequenceMixin, xor_bytes, getrandstr, BASE64_CHARS)
from passlib.utils.compat import (u, unicode, native_string_types, bascii_to_str, int_types, num_types,
irange, byte_elem_value, UnicodeIO, suppress_cause)
@@ -1003,7 +1003,8 @@ class TOTP(object):
# time & token parsing
#=============================================================================
- def normalize_time(self, time):
+ @classmethod
+ def normalize_time(cls, time):
"""
Normalize time value to unix epoch seconds.
@@ -1021,7 +1022,7 @@ class TOTP(object):
elif isinstance(time, float):
return int(time)
elif time is None:
- return int(self.now())
+ return int(cls.now())
elif hasattr(time, "utctimetuple"):
# coerce datetime to UTC timestamp
# NOTE: utctimetuple() assumes naive datetimes are in UTC
@@ -1042,12 +1043,18 @@ class TOTP(object):
"""
return counter * self.period
- def normalize_token(self, token):
+ @hybrid_method
+ def normalize_token(self_or_cls, token):
"""
- normalize OTP token representation:
+ Normalize OTP token representation:
strips whitespace, converts integers to a zero-padded string,
validates token content & number of digits.
+ This is a hybrid method -- it can be called at the class level,
+ as ``TOTP.normalize_token()``, or the instance level as ``TOTP().normalize_token()``.
+ It will normalize to the instance-specific number of :attr:`~TOTP.digits`,
+ or use the class default.
+
:arg token:
token as ascii bytes, unicode, or an integer.
@@ -1055,9 +1062,9 @@ class TOTP(object):
if token has wrong number of digits, or contains non-numeric characters.
:returns:
- token as unicode string containing only digits 0-9.
+ token as :class:`!unicode` string, containing only digits 0-9.
"""
- digits = self.digits
+ digits = self_or_cls.digits
if isinstance(token, int_types):
token = u("%0*d") % (digits, token)
else:
diff --git a/passlib/utils/__init__.py b/passlib/utils/__init__.py
index 726d3ce..0bcf40f 100644
--- a/passlib/utils/__init__.py
+++ b/passlib/utils/__init__.py
@@ -30,6 +30,7 @@ else:
import time
if stringprep:
import unicodedata
+import types
from warnings import warn
# site
# pkg
@@ -274,6 +275,24 @@ class memoized_property(object):
## def __func__(self):
## "py3 compatible alias"
+class hybrid_method(object):
+ """
+ decorator which invokes function with class if called as class method,
+ and with object if called at instance level.
+ """
+
+ def __init__(self, func):
+ self.func = func
+ update_wrapper(self, func)
+
+ def __get__(self, obj, cls):
+ if obj is None:
+ obj = cls
+ if PY3:
+ return types.MethodType(self.func, obj)
+ else:
+ return types.MethodType(self.func, obj, cls)
+
class SequenceMixin(object):
"""
helper which lets result object act like a fixed-length sequence.