summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2011-02-14 14:40:26 -0500
committerEli Collins <elic@assurancetechnologies.com>2011-02-14 14:40:26 -0500
commit2ed71b1442f20ec5b2bba1a88d73bd0480ec04b4 (patch)
treee4bddb6b1687fa47c55b3e469a7f42e0e1371c2f
parent60557f422b8c4836fcd2f89ad9a21babca23e52f (diff)
downloadpasslib-2ed71b1442f20ec5b2bba1a88d73bd0480ec04b4.tar.gz
cleanups
======== * removed from utils since they're not used: norm_salt, norm_rounds, gen_salt * commented out from utils since they're not used: abstractmethod, abstractclassmethod, memoized_class_property * removed passlib.hash.__skel - no longer used * rearranged utils.handlers: - all handler helper classes now inherit from eachother - BaseHandler (renamed from WrappedHandler) - ExtHandler (inherits from BaseHandler, was previously the one named BaseHandler) - StaticHandler (inherits from ExtHandler, renamed from PlainHandler) * converted test_handler classes to use ExtHandler & StaticHandler
-rw-r--r--docs/lib/passlib.utils.rst13
-rw-r--r--passlib/base.py2
-rw-r--r--passlib/hash/__skel.py116
-rw-r--r--passlib/hash/bcrypt.py4
-rw-r--r--passlib/hash/des_crypt.py4
-rw-r--r--passlib/hash/ext_des_crypt.py4
-rw-r--r--passlib/hash/md5_crypt.py4
-rw-r--r--passlib/hash/mysql_323.py4
-rw-r--r--passlib/hash/mysql_41.py4
-rw-r--r--passlib/hash/nthash.py4
-rw-r--r--passlib/hash/phpass.py6
-rw-r--r--passlib/hash/sha1_crypt.py6
-rw-r--r--passlib/hash/sha256_crypt.py4
-rw-r--r--passlib/hash/sha512_crypt.py4
-rw-r--r--passlib/hash/sun_md5_crypt.py4
-rw-r--r--passlib/tests/handler_utils.py6
-rw-r--r--passlib/tests/test_handler.py61
-rw-r--r--passlib/utils/__init__.py171
-rw-r--r--passlib/utils/handlers.py465
19 files changed, 326 insertions, 560 deletions
diff --git a/docs/lib/passlib.utils.rst b/docs/lib/passlib.utils.rst
index 0d6d809..5d7d067 100644
--- a/docs/lib/passlib.utils.rst
+++ b/docs/lib/passlib.utils.rst
@@ -12,8 +12,6 @@ They may also be useful when implementing custom handlers for existing legacy fo
Decorators
==========
.. autofunction:: classproperty
-.. autofunction:: abstractmethod
-.. autofunction:: abstractclassmethod
String Manipulation
===================
@@ -46,17 +44,6 @@ Object Tests
.. autofunction:: is_crypt_context
-Crypt Handler Helpers
-=====================
-The following functions are used by passlib to do input validation
-for many of the implemented password schemes:
-
-.. autofunction:: norm_rounds
-
-.. autofunction:: gen_salt(salt, charset=H64_CHARS)
-
-.. autofunction:: norm_salt(salt, min_chars, max_chars=None, charset=H64_CHARS, gen_charset=None, name=None)
-
Submodules
==========
There are also a few sub modules which provide additional utility functions:
diff --git a/passlib/base.py b/passlib/base.py
index 83931d7..4513300 100644
--- a/passlib/base.py
+++ b/passlib/base.py
@@ -27,7 +27,7 @@ from warnings import warn
from pkg_resources import resource_string
#libs
import passlib.hash as _hmod
-from passlib.utils import abstractclassmethod, Undef, is_crypt_handler, splitcomma, rng
+from passlib.utils import Undef, is_crypt_handler, splitcomma, rng
#pkg
#local
__all__ = [
diff --git a/passlib/hash/__skel.py b/passlib/hash/__skel.py
deleted file mode 100644
index bd2ddc8..0000000
--- a/passlib/hash/__skel.py
+++ /dev/null
@@ -1,116 +0,0 @@
-"""passlib.hash._skel - skeleton file for creating new hash modules
-"""
-#=========================================================
-#imports
-#=========================================================
-#core
-import re
-import logging; log = logging.getLogger(__name__)
-from warnings import warn
-#site
-#libs
-from passlib.utils import norm_rounds, norm_salt
-#pkg
-#local
-__all__ = [
- "genhash",
- "genconfig",
- "encrypt",
- "identify",
- "verify",
-]
-
-#=========================================================
-#backend
-#=========================================================
-
-#=========================================================
-#algorithm information
-#=========================================================
-name = "xxx"
-#stats: ??? bit checksum, ??? bit salt, ??? rounds, max ??? chars of secret
-
-setting_kwds = ("salt", "rounds")
-context_kwds = ()
-
-default_rounds = None #current passlib default
-min_rounds = 1
-max_rounds = 1
-
-#=========================================================
-#internal helpers
-#=========================================================
-_pat = re.compile(r"""
- ^
- \$xxx
- \$(?P<rounds>\d+)
- \$(?P<salt>[A-Za-z0-9./]{xxx})
- (\$(?P<chk>[A-Za-z0-9./]{xxx})?)?
- $
- """, re.X)
-
-def parse(hash):
- if not hash:
- raise ValueError, "no hash specified"
- m = _pat.match(hash)
- if not m:
- raise ValueError, "invalid xxx hash"
- rounds, salt, chk = m.group("rounds", "salt", "chk")
- return dict(
- rounds=int(rounds),
- salt=salt,
- checksum=chk,
- )
-
-def render(rounds, salt, checksum=None):
- return "$xxx$%d$%s$%s" % (rounds, salt, checksum or '')
-
-#=========================================================
-#primary interface
-#=========================================================
-def genconfig(salt=None, rounds=None):
- """generate xxx configuration string
-
- :param salt:
- optional salt string to use.
-
- if omitted, one will be automatically generated (recommended).
-
- length must be XXX characters.
- characters must be in range ``A-Za-z0-9./``.
-
- :param rounds:
-
- optional number of rounds, must be between XXX and XXX inclusive.
-
- :returns:
- xxx configuration string.
- """
- salt = norm_salt(salt, 22, name=name)
- rounds = norm_rounds(rounds, default_rounds, min_rounds, max_rounds, name=name)
- return render(rounds, salt, None)
-
-def genhash(secret, config):
- #parse and run through genconfig to validate configuration
- info = parse(config)
- info.pop("checksum")
- config = genconfig(**info)
-
- #run through chosen backend
- return bcrypt(secret, config)
-
-#=========================================================
-#secondary interface
-#=========================================================
-def encrypt(secret, **settings):
- return genhash(secret, genconfig(**settings))
-
-def verify(secret, hash):
- return hash == genhash(secret, hash)
-
-def identify(hash):
- return bool(hash and _pat.match(hash))
-
-#=========================================================
-#eof
-#=========================================================
diff --git a/passlib/hash/bcrypt.py b/passlib/hash/bcrypt.py
index e8aa165..c7fe307 100644
--- a/passlib/hash/bcrypt.py
+++ b/passlib/hash/bcrypt.py
@@ -23,7 +23,7 @@ except ImportError:
#libs
from passlib.base import register_crypt_handler
from passlib.utils import autodocument, os_crypt
-from passlib.utils.handlers import BackendBaseHandler
+from passlib.utils.handlers import BackendExtHandler
from passlib.utils._slow_bcrypt import raw_bcrypt as slow_raw_bcrypt
#pkg
#local
@@ -34,7 +34,7 @@ __all__ = [
#=========================================================
#handler
#=========================================================
-class BCrypt(BackendBaseHandler):
+class BCrypt(BackendExtHandler):
#=========================================================
#class attrs
#=========================================================
diff --git a/passlib/hash/des_crypt.py b/passlib/hash/des_crypt.py
index f1bff76..31628fc 100644
--- a/passlib/hash/des_crypt.py
+++ b/passlib/hash/des_crypt.py
@@ -60,7 +60,7 @@ from warnings import warn
#libs
from passlib.base import register_crypt_handler
from passlib.utils import h64, autodocument, classproperty, os_crypt
-from passlib.utils.handlers import BackendBaseHandler
+from passlib.utils.handlers import BackendExtHandler
from passlib.utils.des import mdes_encrypt_int_block
#pkg
#local
@@ -103,7 +103,7 @@ def raw_crypt(secret, salt):
#=========================================================
#handler
#=========================================================
-class DesCrypt(BackendBaseHandler):
+class DesCrypt(BackendExtHandler):
#=========================================================
#class attrs
#=========================================================
diff --git a/passlib/hash/ext_des_crypt.py b/passlib/hash/ext_des_crypt.py
index 0fcbe58..c464cfb 100644
--- a/passlib/hash/ext_des_crypt.py
+++ b/passlib/hash/ext_des_crypt.py
@@ -9,7 +9,7 @@ from warnings import warn
#site
#libs
from passlib.base import register_crypt_handler
-from passlib.utils.handlers import BaseHandler
+from passlib.utils.handlers import ExtHandler
from passlib.utils import h64, autodocument
from passlib.utils.des import mdes_encrypt_int_block
from passlib.hash.des_crypt import _crypt_secret_to_key
@@ -55,7 +55,7 @@ def raw_ext_crypt(secret, rounds, salt):
#=========================================================
#handler
#=========================================================
-class ExtDesCrypt(BaseHandler):
+class ExtDesCrypt(ExtHandler):
#=========================================================
#class attrs
#=========================================================
diff --git a/passlib/hash/md5_crypt.py b/passlib/hash/md5_crypt.py
index 3f501cb..ffcd1c3 100644
--- a/passlib/hash/md5_crypt.py
+++ b/passlib/hash/md5_crypt.py
@@ -11,7 +11,7 @@ from warnings import warn
#libs
from passlib.base import register_crypt_handler
from passlib.utils import h64, autodocument, os_crypt
-from passlib.utils.handlers import BackendBaseHandler
+from passlib.utils.handlers import BackendExtHandler
#pkg
#local
__all__ = [
@@ -119,7 +119,7 @@ _chk_offsets = (
#=========================================================
#handler
#=========================================================
-class Md5Crypt(BackendBaseHandler):
+class Md5Crypt(BackendExtHandler):
#=========================================================
#algorithm information
#=========================================================
diff --git a/passlib/hash/mysql_323.py b/passlib/hash/mysql_323.py
index cb0b9c4..ab11ab8 100644
--- a/passlib/hash/mysql_323.py
+++ b/passlib/hash/mysql_323.py
@@ -20,7 +20,7 @@ from warnings import warn
#pkg
from passlib.base import register_crypt_handler
from passlib.utils import autodocument
-from passlib.utils.handlers import WrapperHandler
+from passlib.utils.handlers import BaseHandler
#local
__all__ = [
'MySQL_323',
@@ -29,7 +29,7 @@ __all__ = [
#=========================================================
#backend
#=========================================================
-class MySQL_323(WrapperHandler):
+class MySQL_323(BaseHandler):
#=========================================================
#class attrs
#=========================================================
diff --git a/passlib/hash/mysql_41.py b/passlib/hash/mysql_41.py
index a27a13d..03d7a4a 100644
--- a/passlib/hash/mysql_41.py
+++ b/passlib/hash/mysql_41.py
@@ -21,7 +21,7 @@ from warnings import warn
#pkg
from passlib.base import register_crypt_handler
from passlib.utils import autodocument
-from passlib.utils.handlers import WrapperHandler
+from passlib.utils.handlers import BaseHandler
#local
__all__ = [
"MySQL_41",
@@ -30,7 +30,7 @@ __all__ = [
#=========================================================
#handler
#=========================================================
-class MySQL_41(WrapperHandler):
+class MySQL_41(BaseHandler):
#=========================================================
#algorithm information
#=========================================================
diff --git a/passlib/hash/nthash.py b/passlib/hash/nthash.py
index b92edd9..6efe63f 100644
--- a/passlib/hash/nthash.py
+++ b/passlib/hash/nthash.py
@@ -11,7 +11,7 @@ from warnings import warn
from passlib.base import register_crypt_handler
from passlib.utils.md4 import md4
from passlib.utils import autodocument
-from passlib.utils.handlers import BaseHandler
+from passlib.utils.handlers import ExtHandler
#pkg
#local
__all__ = [
@@ -29,7 +29,7 @@ def raw_nthash(secret, hex=False):
#=========================================================
#handler
#=========================================================
-class NTHash(BaseHandler):
+class NTHash(ExtHandler):
#=========================================================
#class attrs
#=========================================================
diff --git a/passlib/hash/phpass.py b/passlib/hash/phpass.py
index 5351155..2705a6e 100644
--- a/passlib/hash/phpass.py
+++ b/passlib/hash/phpass.py
@@ -15,8 +15,8 @@ import logging; log = logging.getLogger(__name__)
from warnings import warn
#site
#libs
-from passlib.utils import norm_rounds, norm_salt, h64, autodocument
-from passlib.utils.handlers import BaseHandler
+from passlib.utils import h64, autodocument
+from passlib.utils.handlers import ExtHandler
from passlib.base import register_crypt_handler
#pkg
#local
@@ -31,7 +31,7 @@ __all__ = [
#=========================================================
#phpass
#=========================================================
-class PHPass(BaseHandler):
+class PHPass(ExtHandler):
#=========================================================
#class attrs
diff --git a/passlib/hash/sha1_crypt.py b/passlib/hash/sha1_crypt.py
index 39b5dd5..da332bd 100644
--- a/passlib/hash/sha1_crypt.py
+++ b/passlib/hash/sha1_crypt.py
@@ -13,8 +13,8 @@ import logging; log = logging.getLogger(__name__)
from warnings import warn
#site
#libs
-from passlib.utils import norm_rounds, norm_salt, autodocument, h64
-from passlib.utils.handlers import BaseHandler
+from passlib.utils import autodocument, h64
+from passlib.utils.handlers import ExtHandler
from passlib.utils.pbkdf2 import hmac_sha1
from passlib.base import register_crypt_handler
#pkg
@@ -24,7 +24,7 @@ __all__ = [
#=========================================================
#sha1-crypt
#=========================================================
-class SHA1Crypt(BaseHandler):
+class SHA1Crypt(ExtHandler):
#=========================================================
#class attrs
diff --git a/passlib/hash/sha256_crypt.py b/passlib/hash/sha256_crypt.py
index 8b53345..8c98351 100644
--- a/passlib/hash/sha256_crypt.py
+++ b/passlib/hash/sha256_crypt.py
@@ -11,7 +11,7 @@ from warnings import warn
#libs
from passlib.base import register_crypt_handler
from passlib.utils import h64, autodocument, os_crypt
-from passlib.utils.handlers import BackendBaseHandler
+from passlib.utils.handlers import BackendExtHandler
#pkg
#local
__all__ = [
@@ -170,7 +170,7 @@ _256_offsets = (
#=========================================================
#handler
#=========================================================
-class SHA256Crypt(BackendBaseHandler):
+class SHA256Crypt(BackendExtHandler):
#=========================================================
#algorithm information
diff --git a/passlib/hash/sha512_crypt.py b/passlib/hash/sha512_crypt.py
index 2d64db8..7e30d55 100644
--- a/passlib/hash/sha512_crypt.py
+++ b/passlib/hash/sha512_crypt.py
@@ -16,7 +16,7 @@ from warnings import warn
#libs
from passlib.utils import h64, autodocument, classproperty, os_crypt
from passlib.hash.sha256_crypt import raw_sha_crypt
-from passlib.utils.handlers import BackendBaseHandler
+from passlib.utils.handlers import BackendExtHandler
from passlib.base import register_crypt_handler
#pkg
#local
@@ -65,7 +65,7 @@ _512_offsets = (
#=========================================================
#sha 512 crypt
#=========================================================
-class Sha512Crypt(BackendBaseHandler):
+class Sha512Crypt(BackendExtHandler):
#=========================================================
#algorithm information
diff --git a/passlib/hash/sun_md5_crypt.py b/passlib/hash/sun_md5_crypt.py
index 8f30a98..2728a97 100644
--- a/passlib/hash/sun_md5_crypt.py
+++ b/passlib/hash/sun_md5_crypt.py
@@ -27,7 +27,7 @@ from warnings import warn
#libs
from passlib.base import register_crypt_handler
from passlib.utils import h64, autodocument
-from passlib.utils.handlers import BaseHandler
+from passlib.utils.handlers import ExtHandler
#pkg
#local
__all__ = [
@@ -190,7 +190,7 @@ _chk_offsets = (
#=========================================================
#handler
#=========================================================
-class SunMD5Crypt(BaseHandler):
+class SunMD5Crypt(ExtHandler):
#=========================================================
#class attrs
#=========================================================
diff --git a/passlib/tests/handler_utils.py b/passlib/tests/handler_utils.py
index 988eddd..5756d71 100644
--- a/passlib/tests/handler_utils.py
+++ b/passlib/tests/handler_utils.py
@@ -8,7 +8,7 @@ import re
from nose.plugins.skip import SkipTest
#pkg
from passlib.tests.utils import TestCase, enable_option
-from passlib.utils.handlers import BaseHandler, PlainHandler, BackendMixin
+from passlib.utils.handlers import ExtHandler, BackendMixin
#module
__all__ = [
"_HandlerTestCase",
@@ -122,9 +122,9 @@ class _HandlerTestCase(TestCase):
self.assert_(re.match("^[a-z0-9_]+$", name), "name must be alphanum + underscore: %r" % (name,))
def test_01_base_handler(self):
- "run BaseHandler validation tests"
+ "run ExtHandler validation tests"
h = self.handler
- if not isinstance(h, type) or not issubclass(h, (BaseHandler, PlainHandler)):
+ if not isinstance(h, type) or not issubclass(h, ExtHandler):
raise SkipTest
h.validate_class() #should raise AssertionError if something's wrong.
diff --git a/passlib/tests/test_handler.py b/passlib/tests/test_handler.py
index 0ff39f6..a58c0b3 100644
--- a/passlib/tests/test_handler.py
+++ b/passlib/tests/test_handler.py
@@ -9,8 +9,8 @@ import hashlib
from logging import getLogger
#site
#pkg
-from passlib.utils import gen_salt
-from passlib.utils.handlers import CryptHandler
+from passlib.utils import rng, getrandstr
+from passlib.utils.handlers import ExtHandler, StaticHandler
from passlib.tests.handler_utils import _HandlerTestCase
#module
log = getLogger(__name__)
@@ -20,56 +20,53 @@ log = getLogger(__name__)
# to test the unittests themselves, as well as other
# parts of passlib. they shouldn't be used as actual password schemes.
#=========================================================
-class UnsaltedHash(CryptHandler):
+class UnsaltedHash(StaticHandler):
"example algorithm which lacks a salt"
name = "unsalted_example"
- #stats: 160 bit checksum, no salt
@classmethod
def identify(cls, hash):
return bool(hash and re.match("^[0-9a-f]{40}$", hash))
@classmethod
- def genhash(cls, secret, config):
+ def from_string(cls, hash):
+ if hash is None:
+ return cls()
+ if not cls.identify(hash):
+ raise ValueError, "not a unsalted-example hash"
+ return cls(checksum=hash, strict=True)
+
+ def to_string(self):
+ return self.checksum
+
+ def calc_checksum(self, secret):
return hashlib.sha1("boblious" + secret).hexdigest()
-class SaltedHash(CryptHandler):
+class SaltedHash(ExtHandler):
"example algorithm with a salt"
name = "salted_example"
- #stats: 160 bit checksum, 12 bit salt
-
setting_kwds = ("salt",)
+ min_salt_chars = max_salt_chars = 2
+ checksum_chars = 40
+ salt_charset = checksum_charset = "0123456789abcdef"
+
@classmethod
def identify(cls, hash):
- return bool(hash and re.match("^@salt[0-9a-zA-Z./]{2}[0-9a-f]{40}$", hash))
+ return bool(hash and re.match("^@salt[0-9a-f]{42}$", hash))
@classmethod
- def parse(cls, hash):
+ def from_string(cls, hash):
if not cls.identify(hash):
raise ValueError, "not a salted-example hash"
- return dict(
- salt=hash[5:7],
- checksum=hash[7:],
- )
+ return cls(salt=hash[5:7], checksum=hash[7:], strict=True)
- @classmethod
- def render(cls, salt, checksum):
- assert len(salt) == 2
- assert len(checksum) == 40
- return "@salt%s%s" % (salt, checksum)
+ _stub_checksum = '0' * 40
+ def to_string(self):
+ return "@salt%s%s" % (self.salt, self.checksum or self._stub_checksum)
- @classmethod
- def genconfig(cls, salt=None):
- if not salt:
- salt = gen_salt(2)
- return cls.render(salt[:2], '0' * 40)
-
- @classmethod
- def genhash(cls, secret, config):
- salt = cls.parse(config)['salt']
- checksum = hashlib.sha1(salt + secret + salt).hexdigest()
- return cls.render(salt, checksum)
+ def calc_checksum(self, secret):
+ return hashlib.sha1(self.salt + secret + self.salt).hexdigest()
#=========================================================
#test sample algorithms - really a self-test of _HandlerTestCase
@@ -81,9 +78,13 @@ class SaltedHash(CryptHandler):
class UnsaltedHashTest(_HandlerTestCase):
handler = UnsaltedHash
+ known_correct = []
+
class SaltedHashTest(_HandlerTestCase):
handler = SaltedHash
+ known_correct = []
+
#=========================================================
#
#=========================================================
diff --git a/passlib/utils/__init__.py b/passlib/utils/__init__.py
index 94a6dd9..516c5a8 100644
--- a/passlib/utils/__init__.py
+++ b/passlib/utils/__init__.py
@@ -14,23 +14,30 @@ import time
from warnings import warn
#site
#pkg
-import passlib.utils.h64
#local
__all__ = [
#decorators
"classproperty",
- "abstractmethod",
- "abstractclassmethod",
+## "memoized_class_property",
+## "abstractmethod",
+## "abstractclassmethod",
+
+ #misc
+ 'os_crypt',
+
+ #tests
+ 'is_crypt_handler',
+ 'is_crypt_context',
#byte manipulation
"bytes_to_list",
"list_to_bytes",
"xor_bytes",
- #misc helpers
- 'gen_salt',
- 'norm_salt',
- 'norm_rounds',
+ #random
+ 'rng',
+ 'getrandbytes',
+ 'getrandstr',
]
#=================================================================================
@@ -53,43 +60,46 @@ class classproperty(object):
def __get__(self, obj, cls):
return self.im_func(cls)
-
-class memoized_class_property(object):
- """function decorator which calls function as classmethod, and replaces itself with result for current and all future invocations"""
- def __init__(self, func):
- self.im_func = func
- def __get__(self, obj, cls):
- func = self.im_func
- value = func(cls)
- setattr(cls, func.__name__, value)
- return value
-
-def abstractmethod(func):
- """Method decorator which indicates this is a placeholder method which
- should be overridden by subclass.
-
- If called directly, this method will raise an :exc:`NotImplementedError`.
- """
- msg = "object %(self)r method %(name)r is abstract, and must be subclassed"
- def wrapper(self, *args, **kwds):
- text = msg % dict(self=self, name=wrapper.__name__)
- raise NotImplementedError(text)
- update_wrapper(wrapper, func)
- return wrapper
-
-def abstractclassmethod(func):
- """Class Method decorator which indicates this is a placeholder method which
- should be overridden by subclass, and must be a classmethod.
-
- If called directly, this method will raise an :exc:`NotImplementedError`.
- """
- msg = "class %(cls)r method %(name)r is abstract, and must be subclassed"
- def wrapper(cls, *args, **kwds):
- text = msg % dict(cls=cls, name=wrapper.__name__)
- raise NotImplementedError(text)
- update_wrapper(wrapper, func)
- return classmethod(wrapper)
+#works but not used
+##class memoized_class_property(object):
+## """function decorator which calls function as classmethod, and replaces itself with result for current and all future invocations"""
+## def __init__(self, func):
+## self.im_func = func
+##
+## def __get__(self, obj, cls):
+## func = self.im_func
+## value = func(cls)
+## setattr(cls, func.__name__, value)
+## return value
+
+#works but not used...
+##def abstractmethod(func):
+## """Method decorator which indicates this is a placeholder method which
+## should be overridden by subclass.
+##
+## If called directly, this method will raise an :exc:`NotImplementedError`.
+## """
+## msg = "object %(self)r method %(name)r is abstract, and must be subclassed"
+## def wrapper(self, *args, **kwds):
+## text = msg % dict(self=self, name=wrapper.__name__)
+## raise NotImplementedError(text)
+## update_wrapper(wrapper, func)
+## return wrapper
+
+#works but not used...
+##def abstractclassmethod(func):
+## """Class Method decorator which indicates this is a placeholder method which
+## should be overridden by subclass, and must be a classmethod.
+##
+## If called directly, this method will raise an :exc:`NotImplementedError`.
+## """
+## msg = "class %(cls)r method %(name)r is abstract, and must be subclassed"
+## def wrapper(cls, *args, **kwds):
+## text = msg % dict(cls=cls, name=wrapper.__name__)
+## raise NotImplementedError(text)
+## update_wrapper(wrapper, func)
+## return classmethod(wrapper)
Undef = object() #singleton used as default kwd value in some functions
@@ -371,81 +381,6 @@ def getrandstr(rng, charset, count):
#=================================================================================
#misc helpers
#=================================================================================
-def norm_rounds(rounds, default_rounds, min_rounds, max_rounds, name="this crypt"):
- """helper routine for normalizing rounds
-
- * falls back to :attr:`default_rounds`
- * raises ValueError if no fallback
- * clips to min_rounds / max_rounds
- * issues warnings if rounds exists min/max
-
- :returns: normalized rounds value
- """
- if rounds is None:
- rounds = default_rounds
- if rounds is None:
- raise ValueError, "rounds must be specified explicitly"
-
- if rounds > max_rounds:
- warn("%s algorithm does not allow more than %d rounds: %d" % (name, max_rounds, rounds))
- rounds = max_rounds
-
- if rounds < min_rounds:
- warn("%s algorithm does not allow less than %d rounds: %d" % (name, min_rounds, rounds))
- rounds = min_rounds
-
- return rounds
-
-def gen_salt(count, charset=h64.CHARS):
- "generate salt string of *count* chars using specified *charset*"
- global rng
- return getrandstr(rng, charset, count)
-
-def norm_salt(salt, min_chars, max_chars=None, default_chars=None, charset=h64.CHARS, gen_charset=None, name="specified"):
- """helper to normalize & validate user-provided salt string
-
- required salt_charset & salt_chars attrs to be filled in,
- along with optional min_salt_chars attr (defaults to salt_chars).
-
- * generates salt if none provided
- * clips salt to maximum length of salt_chars
-
- :arg salt: user-provided salt
- :arg min_chars: minimum number of chars in salt
- :arg max_chars: maximum number of chars in salt (if omitted, same as min_chars)
- :param charset: character set that salt MUST be subset of (defaults to :)
- :param gen_charset: optional character set to restrict to when generating new salts (defaults to charset)
- :param name: optional name of handler, for inserting into error messages
-
- :raises ValueError:
-
- * if salt contains chars that aren't in salt_charset.
- * if salt contains less than min_salt_chars characters.
-
- :returns:
- resulting or generated salt
- """
- #generate one if needed
- if salt is None:
- return gen_salt(default_chars or max_chars or min_chars, gen_charset or charset)
-
- #check character set
- for c in salt:
- if c not in charset:
- raise ValueError, "invalid character in %s salt: %r" % (name, c)
-
- #check min size
- if len(salt) < min_chars:
- raise ValueError, "%s salt must be at least %d chars" % (name, min_chars)
-
- if max_chars is None:
- max_chars = min_chars
- if len(salt) > max_chars:
- #automatically clip things to specified number of chars
- return salt[:max_chars]
- else:
- return salt
-
class dict_proxy(object):
def __init__(self, source):
self.__source = source
diff --git a/passlib/utils/handlers.py b/passlib/utils/handlers.py
index 4ae8841..3e41233 100644
--- a/passlib/utils/handlers.py
+++ b/passlib/utils/handlers.py
@@ -12,123 +12,140 @@ import time
import os
#site
#libs
-from passlib.utils import abstractmethod, abstractclassmethod, classproperty, h64, \
- getrandstr, rng, Undef, is_crypt_handler
+from passlib.utils import classproperty, h64, getrandstr, rng, is_crypt_handler
#pkg
#local
__all__ = [
#framework for implementing handlers
'BaseHandler',
- 'PlainHandler',
+ 'ExtHandler',
+ 'StaticHandler',
+
+ 'BackendMixin',
+ 'BackendExtHandler',
+ 'BackendStaticHandler',
]
-###==========================================================
-###base interface for all the crypt algorithm implementations
-###==========================================================
-##class CryptHandler(object):
-## """helper class for implementing a password algorithm using class methods"""
-##
-## #=========================================================
-## #class attrs
-## #=========================================================
-##
-## name = None #globally unique name to identify algorithm. should be lower case and hyphens only
-## context_kwds = () #tuple of additional kwds required for any encrypt / verify operations; eg "realm" or "user"
-## setting_kwds = () #tuple of additional kwds that encrypt accepts for configuration algorithm; eg "salt" or "rounds"
-##
-## #=========================================================
-## #primary interface - primary methods implemented by each handler
-## #=========================================================
-##
-## @abstractclassmethod
-## def genhash(cls, secret, config, **context):
-## """encrypt secret to hash"""
-##
-## @classmethod
-## def genconfig(cls, **settings):
-## """return configuration string encoding settings for hash generation"""
-## #NOTE: this implements a default method which is suitable ONLY for classes with no configuration.
-## if cls.setting_kwds:
-## raise NotImplementedError, "classes with config kwds must implement genconfig()"
-## if settings:
-## raise TypeError, "%s has no configuration options" % (cls,)
-## return None
-##
-## #=========================================================
-## #secondary interface - more useful interface for user,
-## # frequently implemented more efficiently by specific handlers
-## #=========================================================
-##
-## @classmethod
-## def identify(cls, hash):
-## """identify if a hash string belongs to this algorithm."""
-## #NOTE: this default method is going to be *really* slow for most implementations,
-## #they should override it. but if genhash() conforms to the specification, this will do.
-## if cls.context_kwds:
-## raise NotImplementedError, "classes with context kwds must implement identify()"
-## if not hash:
-## return False
-## try:
-## cls.genhash("stub", hash)
-## except ValueError:
-## return False
-## return True
-##
-## @classmethod
-## def encrypt(cls, secret, **kwds):
-## """encrypt secret, returning resulting hash string."""
-## if cls.context_kwds:
-## context = dict(
-## (k,kwds.pop(k))
-## for k in cls.context_kwds
-## if k in kwds
-## )
-## config = cls.genconfig(**kwds)
-## return cls.genhash(secret, config, **context)
-## else:
-## config = cls.genconfig(**kwds)
-## return cls.genhash(secret, config)
-##
-## @classmethod
-## def verify(cls, secret, hash, **context):
-## """verify a secret against an existing hash."""
-## #NOTE: methods whose hashes have multiple encodings should override this,
-## # as the hash will need to be normalized before comparing via string equality.
-## # alternately, the ExtCryptHandler class provides a more flexible framework.
-##
-## #ensure hash was specified - genhash() won't throw error for this
-## if not hash:
-## raise ValueError, "no hash specified"
-##
-## #the genhash() implementation for most setting-less algorithms
-## #simply ignores the config string provided; whereas most
-## #algorithms with settings have to inspect and validate it.
-## #therefore, we do this quick check IFF it's setting-less
-## if not cls.setting_kwds and not cls.identify(hash):
-## raise ValueError, "not a %s hash" % (cls.name,)
-##
-## #do simple string comparison
-## return hash == cls.genhash(secret, hash, **context)
-##
-## #=========================================================
-## #eoc
-## #=========================================================
+#=========================================================
+#base handler
+#=========================================================
+class BaseHandler(object):
+ """helper for implementing password hash handler with minimal methods
+
+ hash implementations should fill out the following:
+
+ * all required class attributes: name, setting_kwds
+ * classmethods genconfig() and genhash()
+
+ many implementations will want to override the following:
+
+ * classmethod identify() can usually be done more efficiently
+
+ most implementations can use defaults for the following:
+
+ * encrypt(), verify()
+
+ note this class does not support context kwds of any type,
+ since that is a rare enough requirement inside passlib.
+
+ implemented subclasses may call cls.validate_class() to check attribute consistency
+ (usually only required in unittests, etc)
+ """
+
+ #=====================================================
+ #required attributes
+ #=====================================================
+ name = None #required by subclass
+ setting_kwds = None #required by subclass
+ context_kwds = ()
+
+ #=====================================================
+ #init
+ #=====================================================
+ @classmethod
+ def validate_class(cls):
+ "helper to ensure class is configured property"
+ if not cls.name:
+ raise AssertionError, "class must have .name attribute set"
+
+ if cls.setting_kwds is None:
+ raise AssertionError, "class must have .setting_kwds attribute set"
+
+ #=====================================================
+ #init helpers
+ #=====================================================
+ @classproperty
+ def _has_settings(cls):
+ "attr for checking if class has ANY settings, memoizes itself on first use"
+ if cls.name is None:
+ #otherwise this would optimize itself away prematurely
+ raise RuntimeError, "_has_settings must be called on subclass only: %r" % (cls,)
+ value = cls._has_settings = bool(cls.setting_kwds)
+ return value
+
+ #=====================================================
+ #formatting (usually subclassed)
+ #=====================================================
+ @classmethod
+ def identify(cls, hash):
+ #NOTE: this relys on genhash throwing error for invalid hashes.
+ # this approach is bad because genhash may take a long time on valid hashes,
+ # so subclasses *really* should override this.
+ try:
+ cls.genhash('stub', hash)
+ return True
+ except ValueError:
+ return False
+
+ #=====================================================
+ #primary interface (must be subclassed)
+ #=====================================================
+ @classmethod
+ def genconfig(cls, **settings):
+ if cls._has_settings:
+ raise NotImplementedError, "%s subclass must implement genconfig()" % (cls,)
+ else:
+ if settings:
+ raise TypeError, "%s genconfig takes no kwds" % (cls.name,)
+ return None
+
+ @classmethod
+ def genhash(cls, secret, config):
+ raise NotImplementedError, "%s subclass must implement genhash()" % (cls,)
+
+ #=====================================================
+ #secondary interface (rarely subclassed)
+ #=====================================================
+ @classmethod
+ def encrypt(cls, secret, **settings):
+ config = cls.genconfig(**settings)
+ return cls.genhash(secret, config)
+
+ @classmethod
+ def verify(cls, secret, hash):
+ if not hash:
+ raise ValueError, "no hash specified"
+ return hash == cls.genhash(secret, hash)
+
+ #=====================================================
+ #eoc
+ #=====================================================
#=========================================================
-# BaseHandler
+# ExtHandler
# rounds+salt+xtra phpass, sha256_crypt, sha512_crypt
# rounds+salt bcrypt, ext_des_crypt, sha1_crypt, sun_md5_crypt
# salt only apr_md5_crypt, des_crypt, md5_crypt
#=========================================================
-class BaseHandler(object):
+class ExtHandler(BaseHandler):
"""helper class for implementing hash schemes
hash implementations should fill out the following:
- * all required class attributes
+ * all required class attributes:
- name, setting_kwds
- - max_salt_chars, min_salt_chars, etc - only if salt is used
- - max_rounds, min_rounds, default_roudns - only if rounds are used
+ - max_salt_chars, min_salt_chars - only if salt is used
+ - max_rounds, min_rounds, default_rounds - only if rounds are used
* classmethod from_string()
* instancemethod to_string()
* instancemethod calc_checksum()
@@ -140,6 +157,7 @@ class BaseHandler(object):
most implementations can use defaults for the following:
* genconfig(), genhash(), encrypt(), verify(), etc
* norm_checksum() usually only needs overriding if checksum has multiple encodings
+ * salt_charset, default_salt_charset, default_salt_chars - if does not match common case
note this class does not support context kwds of any type,
since that is a rare enough requirement inside passlib.
@@ -155,8 +173,8 @@ class BaseHandler(object):
#----------------------------------------------
#password hash api - required attributes
#----------------------------------------------
- name = None #required by BaseHandler
- setting_kwds = None #required by BaseHandler
+ name = None #required by ExtHandler
+ setting_kwds = None #required by ExtHandler
context_kwds = ()
#----------------------------------------------
@@ -168,7 +186,7 @@ class BaseHandler(object):
#----------------------------------------------
#salt information
#----------------------------------------------
- max_salt_chars = None #required by BaseHandler.norm_salt()
+ max_salt_chars = None #required by ExtHandler.norm_salt()
@classproperty
def min_salt_chars(cls):
@@ -190,15 +208,15 @@ class BaseHandler(object):
#rounds information
#----------------------------------------------
min_rounds = 0
- max_rounds = None #required by BaseHandler.norm_rounds()
- default_rounds = None #if not specified, BaseHandler.norm_rounds() will require explicit rounds value every time
+ max_rounds = None #required by ExtHandler.norm_rounds()
+ default_rounds = None #if not specified, ExtHandler.norm_rounds() will require explicit rounds value every time
rounds_cost = "linear" #common case
#----------------------------------------------
- #misc BaseHandler configuration
+ #misc ExtHandler configuration
#----------------------------------------------
_strict_rounds_bounds = False #if true, always raises error if specified rounds values out of range - required by spec for some hashes
- _extra_init_settings = () #settings that BaseHandler.__init__ should handle by calling norm_<key>()
+ _extra_init_settings = () #settings that ExtHandler.__init__ should handle by calling norm_<key>()
#=========================================================
#instance attributes
@@ -223,16 +241,12 @@ class BaseHandler(object):
norm = getattr(self, "norm_" + key)
value = norm(value, strict=strict)
setattr(self, key, value)
- super(BaseHandler, self).__init__(**kwds)
+ super(ExtHandler, self).__init__(**kwds)
@classmethod
def validate_class(cls):
"helper to ensure class is configured property"
- if not cls.name:
- raise AssertionError, "class must have .name attribute set"
-
- if cls.setting_kwds is None:
- raise AssertionError, "class must have .setting_kwds attribute set"
+ super(ExtHandler, cls).validate_class()
if any(k not in cls.setting_kwds for k in cls._extra_init_settings):
raise AssertionError, "_extra_init_settings must be subset of setting_kwds"
@@ -274,19 +288,20 @@ class BaseHandler(object):
#---------------------------------------------------------
#internal tests for features
#---------------------------------------------------------
+
@classproperty
def _has_salt(cls):
- "attr for checking if salts are supported, optimizes itself on first use"
- if cls is BaseHandler:
- raise RuntimeError, "not allowed for BaseHandler directly"
+ "attr for checking if salts are supported, memoizes itself on first use"
+ if cls is ExtHandler:
+ raise RuntimeError, "not allowed for ExtHandler directly"
value = cls._has_salt = 'salt' in cls.setting_kwds
return value
@classproperty
def _has_rounds(cls):
- "attr for checking if variable are supported, optimizes itself on first use"
- if cls is BaseHandler:
- raise RuntimeError, "not allowed for BaseHandler directly"
+ "attr for checking if variable are supported, memoizes itself on first use"
+ if cls is ExtHandler:
+ raise RuntimeError, "not allowed for ExtHandler directly"
value = cls._has_rounds = 'rounds' in cls.setting_kwds
return value
@@ -307,10 +322,30 @@ class BaseHandler(object):
@classmethod
def norm_salt(cls, salt, strict=False):
- "helper to normalize salt string; strict flag causes error even for correctable errors"
+ """helper to normalize & validate user-provided salt string
+
+ :arg salt: salt string or ``None``
+ :param strict: enable strict checking (see below); disabled by default
+
+ :raises ValueError:
+
+ * if ``strict=True`` and no salt is provided
+ * if ``strict=True`` and salt contains greater than :attr:`max_salt_chars` characters
+ * if salt contains chars that aren't in :attr:`salt_charset`.
+ * if salt contains less than :attr:`min_salt_chars` characters.
+
+ if no salt provided and ``strict=False``, a random salt is generated
+ using :attr:`default_salt_chars` and :attr:`default_salt_charset`.
+ if the salt is longer than :attr:`max_salt_chars` and ``strict=False``,
+ the salt string is clipped to :attr:`max_salt_chars`.
+
+ :returns:
+ normalized or generated salt
+ """
if not cls._has_salt:
+ #NOTE: special casing schemes which have no salt...
if salt is not None:
- raise ValueError, "%s does not support ``salt``" % (cls.name,)
+ raise ValueError, "%s does not support ``salt`` parameter" % (cls.name,)
return None
if salt is None:
@@ -319,23 +354,46 @@ class BaseHandler(object):
return getrandstr(rng, cls.default_salt_charset, cls.default_salt_chars)
#TODO: run salt_charset tests
+ sc = cls.salt_charset
+ if sc:
+ for c in salt:
+ if c not in sc:
+ raise ValueError, "invalid character in %s salt: %r" % (cls.name, c)
mn = cls.min_salt_chars
if mn and len(salt) < mn:
- raise ValueError, "%s salt string must be >= %d characters" % (cls.name, mn)
+ raise ValueError, "%s salt string must be at least %d characters" % (cls.name, mn)
mx = cls.max_salt_chars
if len(salt) > mx:
if strict:
- raise ValueError, "%s salt string must be <= %d characters" % (cls.name, mx)
+ raise ValueError, "%s salt string must be at most %d characters" % (cls.name, mx)
salt = salt[:mx]
return salt
@classmethod
def norm_rounds(cls, rounds, strict=False):
- "helper to normalize rounds value; strict flag causes error even for correctable errors"
+ """helper routine for normalizing rounds
+
+ :arg rounds: rounds integer or ``None``
+ :param strict: enable strict checking (see below); disabled by default
+
+ :raises ValueError:
+
+ * if rounds is ``None`` and ``strict=True``
+ * if rounds is ``None`` and no :attr:`default_rounds` are specified by class.
+ * if rounds is outside bounds of :attr:`min_rounds` and :attr:`max_rounds`, and ``strict=True``.
+
+ if rounds are not specified and ``strict=False``, uses :attr:`default_rounds`.
+ if rounds are outside bounds and ``strict=False``, rounds are clipped as appropriate,
+ but a warning is issued.
+
+ :returns:
+ normalized rounds value
+ """
if not cls._has_rounds:
+ #NOTE: special casing schemes which don't have rounds
if rounds is not None:
raise ValueError, "%s does not support ``rounds``" % (cls.name,)
return None
@@ -345,7 +403,7 @@ class BaseHandler(object):
raise ValueError, "no rounds specified"
rounds = cls.default_rounds
if rounds is None:
- raise ValueError, "%s requires an explicitly-specified rounds value" % (cls.name,)
+ raise ValueError, "%s rounds value must be specified explicitly" % (cls.name,)
return rounds
if cls._strict_rounds_bounds:
@@ -355,12 +413,14 @@ class BaseHandler(object):
if rounds < mn:
if strict:
raise ValueError, "%s rounds must be >= %d" % (cls.name, mn)
+ warn("%s does not allow less than %d rounds: %d" % (cls.name, mn, rounds))
rounds = mn
mx = cls.max_rounds
if rounds > mx:
if strict:
raise ValueError, "%s rounds must be <= %d" % (cls.name, mx)
+ warn("%s does not allow more than %d rounds: %d" % (cls.name, mx, rounds))
rounds = mx
return rounds
@@ -440,41 +500,34 @@ class BaseHandler(object):
#=========================================================
#=========================================================
-#plain - mysql_323, mysql_41, nthash, postgres_md5
+#static - mysql_323, mysql_41, nthash, postgres_md5
#=========================================================
-#XXX: rename this? StaticHandler? NoSettingHandler? and give this name to WrapperHandler
-class PlainHandler(object):
- """helper class optimized for implementing hash schemes which have NO settings whatsoever"""
- #=========================================================
- #password hash api - required attributes
- #=========================================================
- name = None #required
- setting_kwds = ()
- context_kwds = ()
+class StaticHandler(ExtHandler):
+ """helper class optimized for implementing hash schemes which have NO settings whatsoever.
+ the main thing this changes from ExtHandler:
+
+ * :attr:`setting_kwds` must be an empty tuple (set by class)
+ * :meth:`genconfig` takes no kwds, and always returns ``None``.
+ * :meth:`genhash` accepts ``config=None``.
+
+ otherwise, this requires the same methods be implemented
+ as does ExtHandler.
+ """
#=========================================================
- #helpers for norm checksum
+ #class attr
#=========================================================
- checksum_charset = None #if specified, norm_checksum() will validate this
- checksum_chars = None #if specified, norm_checksum will require this length
+ setting_kwds = ()
#=========================================================
#init
#=========================================================
- def __init__(self, checksum=None, strict=False, **kwds):
- self.checksum = self.norm_checksum(checksum, strict=strict)
- super(PlainHandler, self).__init__(**kwds)
-
@classmethod
def validate_class(cls):
"helper to validate that class has been configured properly"
- if not cls.name:
- raise AssertionError, "class must have .name attribute set"
-
- #=========================================================
- #helpers
- #=========================================================
- norm_checksum = BaseHandler.norm_checksum.im_func
+ if cls.setting_kwds:
+ raise AssertionError, "StaticHandler subclasses must not have any settings, perhaps you want ExtHandler?"
+ super(StaticHandler, cls).validate_class()
#=========================================================
#primary interface
@@ -484,116 +537,22 @@ class PlainHandler(object):
return None
@classmethod
- def genhash(cls, secret, config, **context):
- #NOTE: config is ignored
- self = cls()
- self.checksum = self.calc_checksum(secret, **context)
+ def genhash(cls, secret, config):
+ if config is None:
+ self = cls()
+ else:
+ #just to verify input is correctly formatted
+ self = cls.from_string(config)
+ self.checksum = self.calc_checksum(secret)
return self.to_string()
- calc_checksum = BaseHandler.calc_checksum.im_func
-
- #=========================================================
- #secondary interface
- #=========================================================
- @classmethod
- def identify(cls, hash):
- #NOTE: subclasses may wish to use faster / simpler identify,
- # and raise value errors only when an invalid (but identifiable) string is parsed
- if not hash:
- return False
- try:
- cls.from_string(hash)
- return True
- except ValueError:
- return False
-
- @classmethod
- def encrypt(cls, secret, **context):
- return cls.genhash(secret, None, **context)
-
- @classmethod
- def verify(cls, secret, hash, **context):
- #NOTE: classes may wish to override this
- self = cls.from_string(hash)
- return self.checksum == self.calc_checksum(secret, **context)
-
- #=========================================================
- #parser interface
- #=========================================================
- @classmethod
- def from_string(cls, hash):
- raise NotImplementedError, "implement in subclass"
-
- def to_string(cls):
- raise NotImplementedError, "implement in subclass"
-
#=========================================================
#eoc
#=========================================================
#=========================================================
-#wrapper
-#=========================================================
-class WrapperHandler(object):
- "helper for implementing wrapper of crypt-like interface, only required genconfig & genhash"
-
- #=====================================================
- #required attributes
- #=====================================================
- name = None
- setting_kwds = None
- context_kwds = ()
-
- #=====================================================
- #formatting (usually subclassed)
- #=====================================================
- @classmethod
- def identify(cls, hash):
- #NOTE: this relys on genhash throwing error for invalid hashes.
- # this approach is bad because genhash may take a long time on valid hashes,
- # so subclasses *really* should override this.
- try:
- cls.genhash('stub', hash)
- return True
- except ValueError:
- return False
-
- #=====================================================
- #primary interface (must be subclassed)
- #=====================================================
- @classmethod
- def genconfig(cls, **settings):
- if cls.setting_kwds:
- raise NotImplementedError, "%s subclass must implement genconfig()" % (cls,)
- else:
- if settings:
- raise TypeError, "%s genconfig takes no kwds" % (cls.name,)
- return None
-
- @classmethod
- def genhash(cls, secret, config):
- raise NotImplementedError, "%s subclass must implement genhash()" % (cls,)
-
- #=====================================================
- #secondary interface (rarely subclassed)
- #=====================================================
- @classmethod
- def encrypt(cls, secret, **settings):
- config = cls.genconfig(**settings)
- return cls.genhash(secret, config)
-
- @classmethod
- def verify(cls, secret, hash):
- if not hash:
- raise ValueError, "no hash specified"
- return hash == cls.genhash(secret, hash)
-
- #=====================================================
- #eoc
- #=====================================================
-
-#=========================================================
-#
+#helpful mixin which provides lazy-loading of different backends
+#to be used for calc_checksum
#=========================================================
class BackendMixin(object):
@@ -645,10 +604,10 @@ class BackendMixin(object):
assert self._backend, "set_backend() failed to load a default backend"
return self.calc_checksum(secret)
-class BackendBaseHandler(BackendMixin, BaseHandler):
+class BackendExtHandler(BackendMixin, ExtHandler):
pass
-class BackendPlainHandler(BackendMixin, PlainHandler):
+class BackendStaticHandler(BackendMixin, StaticHandler):
pass
#=========================================================