diff options
author | Eli Collins <elic@assurancetechnologies.com> | 2011-02-14 14:40:26 -0500 |
---|---|---|
committer | Eli Collins <elic@assurancetechnologies.com> | 2011-02-14 14:40:26 -0500 |
commit | 2ed71b1442f20ec5b2bba1a88d73bd0480ec04b4 (patch) | |
tree | e4bddb6b1687fa47c55b3e469a7f42e0e1371c2f | |
parent | 60557f422b8c4836fcd2f89ad9a21babca23e52f (diff) | |
download | passlib-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.rst | 13 | ||||
-rw-r--r-- | passlib/base.py | 2 | ||||
-rw-r--r-- | passlib/hash/__skel.py | 116 | ||||
-rw-r--r-- | passlib/hash/bcrypt.py | 4 | ||||
-rw-r--r-- | passlib/hash/des_crypt.py | 4 | ||||
-rw-r--r-- | passlib/hash/ext_des_crypt.py | 4 | ||||
-rw-r--r-- | passlib/hash/md5_crypt.py | 4 | ||||
-rw-r--r-- | passlib/hash/mysql_323.py | 4 | ||||
-rw-r--r-- | passlib/hash/mysql_41.py | 4 | ||||
-rw-r--r-- | passlib/hash/nthash.py | 4 | ||||
-rw-r--r-- | passlib/hash/phpass.py | 6 | ||||
-rw-r--r-- | passlib/hash/sha1_crypt.py | 6 | ||||
-rw-r--r-- | passlib/hash/sha256_crypt.py | 4 | ||||
-rw-r--r-- | passlib/hash/sha512_crypt.py | 4 | ||||
-rw-r--r-- | passlib/hash/sun_md5_crypt.py | 4 | ||||
-rw-r--r-- | passlib/tests/handler_utils.py | 6 | ||||
-rw-r--r-- | passlib/tests/test_handler.py | 61 | ||||
-rw-r--r-- | passlib/utils/__init__.py | 171 | ||||
-rw-r--r-- | passlib/utils/handlers.py | 465 |
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 #========================================================= |