From ec573fe344d3d2574f8a657bc701fd18d91f3d22 Mon Sep 17 00:00:00 2001 From: Eli Collins Date: Thu, 10 Nov 2016 20:17:07 -0500 Subject: totp: TOTP.normalize_token() turned into hybrid method, made public; TOTP.normalize_time() turned into class method, made public. --- docs/lib/passlib.totp.rst | 16 +++++++++------- passlib/tests/test_totp.py | 33 ++++++++++++++++++++++++++------- passlib/totp.py | 21 ++++++++++++++------- passlib/utils/__init__.py | 19 +++++++++++++++++++ 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. -- cgit v1.2.1