diff options
author | Eli Collins <elic@assurancetechnologies.com> | 2011-02-14 13:40:41 -0500 |
---|---|---|
committer | Eli Collins <elic@assurancetechnologies.com> | 2011-02-14 13:40:41 -0500 |
commit | 60557f422b8c4836fcd2f89ad9a21babca23e52f (patch) | |
tree | d28b5d97ed986251feedac89ce2ef86ee87bb032 | |
parent | 43dfc4084ada0d073556b0aa26d67952f8a2a4e0 (diff) | |
download | passlib-60557f422b8c4836fcd2f89ad9a21babca23e52f.tar.gz |
converted NTHash, PostgresMD5, SHA256Crypt, SunMD5Crypt to classes
-rw-r--r-- | passlib/hash/nthash.py | 134 | ||||
-rw-r--r-- | passlib/hash/postgres_md5.py | 92 | ||||
-rw-r--r-- | passlib/hash/sha1_crypt.py | 29 | ||||
-rw-r--r-- | passlib/hash/sha256_crypt.py | 257 | ||||
-rw-r--r-- | passlib/hash/sun_md5_crypt.py | 180 | ||||
-rw-r--r-- | passlib/tests/handler_utils.py | 4 | ||||
-rw-r--r-- | passlib/tests/test_hash_misc.py | 54 | ||||
-rw-r--r-- | passlib/tests/test_hash_postgres.py | 14 | ||||
-rw-r--r-- | passlib/tests/test_hash_sun_md5_crypt.py | 17 | ||||
-rw-r--r-- | passlib/utils/handlers.py | 117 | ||||
-rw-r--r-- | passlib/utils/pbkdf2.py | 21 |
11 files changed, 460 insertions, 459 deletions
diff --git a/passlib/hash/nthash.py b/passlib/hash/nthash.py index 3dcd3dd..b92edd9 100644 --- a/passlib/hash/nthash.py +++ b/passlib/hash/nthash.py @@ -8,16 +8,14 @@ import logging; log = logging.getLogger(__name__) from warnings import warn #site #libs +from passlib.base import register_crypt_handler from passlib.utils.md4 import md4 from passlib.utils import autodocument +from passlib.utils.handlers import BaseHandler #pkg #local __all__ = [ - "genhash", - "genconfig", - "encrypt", - "identify", - "verify", + "NTHash", ] #========================================================= @@ -29,73 +27,87 @@ def raw_nthash(secret, hex=False): return hash.hexdigest() if hex else hash.digest() #========================================================= -#algorithm information +#handler #========================================================= -name = "nthash" -#stats: 128 bit checksum, no salt +class NTHash(BaseHandler): + #========================================================= + #class attrs + #========================================================= + name = "nthash" + setting_kwds = ("ident",) -setting_kwds = () -context_kwds = () + #========================================================= + #init + #========================================================= + _extra_init_settings = ("ident",) -#========================================================= -#internal helpers -#========================================================= -_pat = re.compile(r""" - ^ - \$(?P<ident>3\$\$|NT\$) - (?P<chk>[a-f0-9]{32}) - $ - """, re.X) + @classmethod + def norm_ident(cls, value, strict=False): + if value is None: + if strict: + raise ValueError, "no ident specified" + return "3" + if value not in ("3", "NT"): + raise ValueError, "invalid ident" + return value -def parse(hash): - if not hash: - raise ValueError, "no hash specified" - m = _pat.match(hash) - if not m: - raise ValueError, "invalid nthash" - ident, chk = m.group("ident", "chk") - out = dict( - checksum=chk, - ) - ident=ident.strip("$") - if ident != "3": - out['ident'] = ident - return out + #========================================================= + #formatting + #========================================================= + @classmethod + def identify(cls, hash): + return bool(hash) and (hash.startswith("$3$") or hash.startswith("$NT$")) -def render(checksum, ident=None): - if not ident or ident == "3": - return "$3$$" + checksum - elif ident == "NT": - return "$NT$" + checksum - else: - raise ValueError, "invalid ident" + _pat = re.compile(r""" + ^ + \$(?P<ident>3\$\$|NT\$) + (?P<chk>[a-f0-9]{32}) + $ + """, re.X) -#========================================================= -#primary interface -#========================================================= -def genconfig(ident=None): - return render("0" * 32, ident) + @classmethod + def from_string(cls, hash): + if not hash: + raise ValueError, "no hash specified" + m = cls._pat.match(hash) + if not m: + raise ValueError, "invalid nthash" + ident, chk = m.group("ident", "chk") + return cls(ident=ident.strip("$"), checksum=chk, strict=True) -def genhash(secret, config): - info = parse(config) - if secret is None: - raise TypeError, "secret must be a string" - chk = raw_nthash(secret, hex=True) - return render(chk, info.get('ident')) + def to_string(self): + ident = self.ident + if ident == "3": + return "$3$$" + self.checksum + else: + assert ident == "NT" + return "$NT$" + self.checksum -#========================================================= -#secondary interface -#========================================================= -def encrypt(secret, **settings): - return genhash(secret, genconfig(**settings)) + #========================================================= + #primary interface + #========================================================= + _stub_checksum = "0" * 32 + + @classmethod + def genconfig(cls, ident=None): + return cls(ident=ident, checksum=self._stub_checksum).to_string() -def verify(secret, hash): - return hash == genhash(secret, hash) + def calc_checksum(self, secret): + if secret is None: + raise TypeError, "secret must be a string" + return raw_nthash(secret, hex=True) -def identify(hash): - return bool(hash and _pat.match(hash)) + #========================================================= + #eoc + #========================================================= -autodocument(globals()) +autodocument(NTHash, settings_doc=""" +:param ident: + This handler supports two different :ref:`modular-crypt-format` identifiers. + It defaults to ``3``, but users may specify the alternate ``NT`` identifier + which is used in some contexts. +""") +register_crypt_handler(NTHash) #========================================================= #eof #========================================================= diff --git a/passlib/hash/postgres_md5.py b/passlib/hash/postgres_md5.py index 64c92c3..cafb9b1 100644 --- a/passlib/hash/postgres_md5.py +++ b/passlib/hash/postgres_md5.py @@ -10,64 +10,74 @@ from warnings import warn #site #libs #pkg +from passlib.base import register_crypt_handler from passlib.utils import autodocument #local __all__ = [ - "genhash", - "genconfig", - "encrypt", - "identify", - "verify", + "PostgresMD5", ] #========================================================= -#backend +#handler #========================================================= +class PostgresMD5(object): + #========================================================= + #algorithm information + #========================================================= + name = "postgres_md5" + setting_kwds = () + context_kwds = ("user",) -#========================================================= -#algorithm information -#========================================================= -name = "postgres_md5" -#stats: 512 bit checksum, username used as salt + #========================================================= + #formatting + #========================================================= + _pat = re.compile(r"^md5[0-9a-f]{32}$") -setting_kwds = () -context_kwds = ("user",) + @classmethod + def identify(cls, hash): + return bool(hash and cls._pat.match(hash)) -#========================================================= -#internal helpers -#========================================================= -_pat = re.compile(r"^md5[0-9a-f]{32}$") + #========================================================= + #primary interface + #========================================================= + @classmethod + def genconfig(cls): + return None -#========================================================= -#primary interface -#========================================================= -def genconfig(): - return None - -def genhash(secret, config, user): - if config and not identify(config): - raise ValueError, "not a postgres-md5 hash" - if not user: - raise ValueError, "user keyword must be specified for this algorithm" - return "md5" + md5(secret + user).hexdigest().lower() + @classmethod + def genhash(cls, secret, config, user): + if config and not cls.identify(config): + raise ValueError, "not a postgres-md5 hash" + return cls.encrypt(secret, user) -#========================================================= -#secondary interface -#========================================================= -def encrypt(secret, user, **settings): - return genhash(secret, genconfig(**settings), user) + #========================================================= + #secondary interface + #========================================================= + @classmethod + def encrypt(cls, secret, user): + #FIXME: not sure what postgres' policy is for unicode + if not user: + raise ValueError, "user keyword must be specified for this algorithm" + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + if isinstance(user, unicode): + user = user.encode("utf-8") + return "md5" + md5(secret + user).hexdigest().lower() -def verify(secret, hash, user): - if not hash: - raise ValueError, "no hash specified" - return hash.lower() == genhash(secret, hash, user) + @classmethod + def verify(cls, secret, hash, user): + if not hash: + raise ValueError, "no hash specified" + return hash == cls.genhash(secret, hash, user) -def identify(hash): - return bool(hash and _pat.match(hash)) + #========================================================= + #eoc + #========================================================= -autodocument(globals(), context_doc="""\ +autodocument(PostgresMD5, context_doc="""\ :param user: string containing name of postgres user account this password is associated with. """) +register_crypt_handler(PostgresMD5) #========================================================= #eof #========================================================= diff --git a/passlib/hash/sha1_crypt.py b/passlib/hash/sha1_crypt.py index 3ba7f24..39b5dd5 100644 --- a/passlib/hash/sha1_crypt.py +++ b/passlib/hash/sha1_crypt.py @@ -12,40 +12,19 @@ import re import logging; log = logging.getLogger(__name__) from warnings import warn #site -try: - from M2Crypto import EVP as _EVP -except ImportError: - _EVP = None #libs from passlib.utils import norm_rounds, norm_salt, autodocument, h64 from passlib.utils.handlers import BaseHandler +from passlib.utils.pbkdf2 import hmac_sha1 from passlib.base import register_crypt_handler #pkg #local __all__ = [ ] - -#========================================================= -#backend -#========================================================= -def hmac_sha1(key, msg): - return hmac(key, msg, sha1).digest() - -if _EVP: - try: - result = _EVP.hmac('x','y') #default *should* be sha1, which saves us a wrapper, but might as well check. - except ValueError: - pass - else: - if result == ',\x1cb\xe0H\xa5\x82M\xfb>\xd6\x98\xef\x8e\xf9oQ\x85\xa3i': - hmac_sha1 = _EVP.hmac - -#TODO: should test for crypt support (NetBSD only) - #========================================================= #sha1-crypt #========================================================= -class Sha1Crypt(BaseHandler): +class SHA1Crypt(BaseHandler): #========================================================= #class attrs @@ -129,8 +108,8 @@ class Sha1Crypt(BaseHandler): #eoc #========================================================= -autodocument(Sha1Crypt) -register_crypt_handler(Sha1Crypt) +autodocument(SHA1Crypt) +register_crypt_handler(SHA1Crypt) #========================================================= #eof #========================================================= diff --git a/passlib/hash/sha256_crypt.py b/passlib/hash/sha256_crypt.py index a7cd1fd..8b53345 100644 --- a/passlib/hash/sha256_crypt.py +++ b/passlib/hash/sha256_crypt.py @@ -9,15 +9,13 @@ import logging; log = logging.getLogger(__name__) from warnings import warn #site #libs -from passlib.utils import norm_rounds, norm_salt, h64, autodocument +from passlib.base import register_crypt_handler +from passlib.utils import h64, autodocument, os_crypt +from passlib.utils.handlers import BackendBaseHandler #pkg #local __all__ = [ - "genhash", - "genconfig", - "encrypt", - "identify", - "verify", + "SHA256Crypt", ] #========================================================= @@ -170,145 +168,130 @@ _256_offsets = ( ) #========================================================= -#choose backend +#handler #========================================================= +class SHA256Crypt(BackendBaseHandler): -#fallback to default backend (defined above) -backend = "builtin" + #========================================================= + #algorithm information + #========================================================= + name = "sha256_crypt" -#check if stdlib crypt is available, and if so, if OS supports $5$ and $6$ -#XXX: is this test expensive enough it should be delayed -#until sha-crypt is requested? + setting_kwds = ("salt", "rounds", "implicit_rounds") -try: - from crypt import crypt -except ImportError: - crypt = None -else: - if crypt("test", "$5$rounds=1000$test") == "$5$rounds=1000$test$QmQADEXMG8POI5WDsaeho0P36yK3Tcrgboabng6bkb/": - backend = "os-crypt" - else: - crypt = None - -#========================================================= -#algorithm information -#========================================================= -name = "sha256_crypt" -#stats: 256 bit checksum, 96 bit salt, 1000..10e8-1 rounds - -setting_kwds = ("salt", "rounds") -context_kwds = () - -default_rounds = 40000 #current passlib default -min_rounds = 1000 -max_rounds = 999999999 -rounds_cost = "linear" - -min_salt_chars = 0 -max_salt_chars = 16 + min_salt_chars = 0 + max_salt_chars = 16 + #TODO: allow salt charset 0-255 except for "\x00\n:$" -#========================================================= -#internal helpers -#========================================================= -_pat = re.compile(r""" - ^ - \$5 - (\$rounds=(?P<rounds>\d+))? - \$ - ( - (?P<salt1>[^:$]*) - | - (?P<salt2>[^:$]{0,16}) + default_rounds = 40000 #current passlib default + min_rounds = 1000 + max_rounds = 999999999 + rounds_cost = "linear" + + #========================================================= + #init + #========================================================= + def __init__(self, implicit_rounds=None, **kwds): + if implicit_rounds is None: + implicit_rounds = True + self.implicit_rounds = implicit_rounds + super(SHA512Crypt, self).__init__(**kwds) + + #========================================================= + #parsing + #========================================================= + @classmethod + def identify(cls, hash): + return bool(hash) and hash.startswith("$5$") + + #: regexp used to parse hashes + _pat = re.compile(r""" + ^ + \$5 + (\$rounds=(?P<rounds>\d+))? \$ - (?P<chk>[A-Za-z0-9./]{43})? - ) - $ - """, re.X) - -def parse(hash): - if not hash: - raise ValueError, "no hash specified" - m = _pat.match(hash) - if not m: - raise ValueError, "invalid sha256-crypt hash" - rounds, salt1, salt2, chk = m.group("rounds", "salt1", "salt2", "chk") - if rounds and rounds.startswith("0"): - raise ValueError, "invalid sha256-crypt hash: zero-padded rounds" - return dict( - implicit_rounds = not rounds, - rounds=int(rounds) if rounds else 5000, - salt=salt1 or salt2, - checksum=chk, - ) - -def render(rounds, salt, checksum=None, implicit_rounds=True): - assert '$' not in salt - if rounds == 5000 and implicit_rounds: - return "$5$%s$%s" % (salt, checksum or '') - else: - return "$5$rounds=%d$%s$%s" % (rounds, salt, checksum or '') - -#========================================================= -#primary interface -#========================================================= -def genconfig(salt=None, rounds=None, implicit_rounds=True): - """generate sha256-crypt configuration string - - :param salt: - optional salt string to use. - - if omitted, one will be automatically generated (recommended). - - length must be 0 .. 16 characters inclusive. - characters must be in range ``A-Za-z0-9./``. - - :param rounds: - - optional number of rounds, must be between 1000 and 999999999 inclusive. - - :param implicit_rounds: - - this is an internal option which generally doesn't need to be touched. - - :returns: - sha256-crypt configuration string. - """ - #TODO: allow salt charset 0-255 except for "\x00\n:$" - salt = norm_salt(salt, min_salt_chars, max_salt_chars, name=name) - rounds = norm_rounds(rounds, default_rounds, min_rounds, max_rounds, name=name) - return render(rounds, salt, None, implicit_rounds) - -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 - if crypt: - #using system's crypt routine. + ( + (?P<salt1>[^:$]*) + | + (?P<salt2>[^:$]{0,16}) + \$ + (?P<chk>[A-Za-z0-9./]{43})? + ) + $ + """, re.X) + + @classmethod + def from_string(cls, hash): + if not hash: + raise ValueError, "no hash specified" + #TODO: write non-regexp based parser, + # and rely on norm_salt etc to handle more of the validation. + m = cls._pat.match(hash) + if not m: + raise ValueError, "invalid sha256-crypt hash" + rounds, salt1, salt2, chk = m.group("rounds", "salt1", "salt2", "chk") + if rounds and rounds.startswith("0"): + raise ValueError, "invalid sha256-crypt hash (zero-padded rounds)" + return cls( + implicit_rounds = not rounds, + rounds=int(rounds) if rounds else 5000, + salt=salt1 or salt2, + checksum=chk, + strict=bool(chk), + ) + + def to_string(self): + if self.rounds == 5000 and self.implicit_rounds: + return "$5$%s$%s" % (self.salt, self.checksum or '') + else: + return "$5$rounds=%d$%s$%s" % (self.rounds, self.salt, self.checksum or '') + + #========================================================= + #backend + #========================================================= + backends = ("os_crypt", "builtin") + + _has_backend_builtin = True + + @classproperty + def _has_backend_os_crypt(cls): + return bool( + os_crypt and + os_crypt("test", "$5$rounds=1000$test") == + "$5$rounds=1000$test$QmQADEXMG8POI5WDsaeho0P36yK3Tcrgboabng6bkb/" + ) + + def _calc_checksum_builtin(self, secret): + checksum, salt, rounds = raw_sha256_crypt(secret, self.salt, self.rounds) + assert salt == self.salt, "class doesn't agree w/ builtin backend" + assert rounds == self.rounds, "class doesn't agree w/ builtin backend" + return checksum + + def _calc_checksum_os_crypt(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") - return crypt(secret, config) - else: - #using builtin routine - info = parse(config) - checksum, salt, rounds = raw_sha256_crypt(secret, info['salt'], info['rounds']) - return render(rounds, salt, checksum, info['implicit_rounds']) - -#========================================================= -#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)) - -autodocument(globals()) + #NOTE: avoiding full parsing routine via from_string().checksum, + # and just extracting the bit we need. + result = os_crypt(secret, self.to_string()) + assert result.startswith("$5$") + chk = result[-43:] + assert '$' not in chk + return chk + + #========================================================= + #eoc + #========================================================= + +autodocument(SHA256Crypt, settings_doc=""" +:param implicit_rounds: + this is an internal option which generally doesn't need to be touched. + + this flag determines whether the hash should omit the rounds parameter + when encoding it to a string; this is only permitted by the spec for rounds=5000, + and the flag is ignored otherwise. the spec requires the two different + encodings be preserved as they are, instead of normalizing them. +""") +register_crypt_handler(SHA256Crypt) #========================================================= #eof #========================================================= diff --git a/passlib/hash/sun_md5_crypt.py b/passlib/hash/sun_md5_crypt.py index 352a344..8f30a98 100644 --- a/passlib/hash/sun_md5_crypt.py +++ b/passlib/hash/sun_md5_crypt.py @@ -25,7 +25,9 @@ import logging; log = logging.getLogger(__name__) from warnings import warn #site #libs -from passlib.utils import norm_rounds, norm_salt, h64, autodocument +from passlib.base import register_crypt_handler +from passlib.utils import h64, autodocument +from passlib.utils.handlers import BaseHandler #pkg #local __all__ = [ @@ -186,101 +188,89 @@ _chk_offsets = ( ) #========================================================= -#algorithm information +#handler #========================================================= -name = "sun_md5_crypt" -#stats: 128 bit checksum, 48 bit salt, 0..2**32-4095 rounds - -setting_kwds = ("salt", "rounds") -context_kwds = () - -min_salt_chars = 0 -max_salt_chars = 8 - -default_rounds = 5000 #current passlib default -min_rounds = 0 -max_rounds = 4294963199 ##2**32-1-4096 - #XXX: not sure what it does if past this bound... does 32 int roll over? -rounds_cost = "linear" - -#========================================================= -#internal helpers -#========================================================= -_pat = re.compile(r""" - ^ - \$md5 - ([$,]rounds=(?P<rounds>\d+))? - \$(?P<salt>[A-Za-z0-9./]{0,8}) - (\$(?P<chk>[A-Za-z0-9./]{22})?)? - $ - """, re.X) - -#NOTE: trailing "$" is supposed to be part of config string, -# supposed to take both, but render with "$" -#NOTE: seen examples with both "," or "$" as md5/rounds separator, -# not sure what official format is. -# taking both, rendering "," - -def parse(hash): - if not hash: - raise ValueError, "no hash specified" - m = _pat.match(hash) - if not m: - raise ValueError, "invalid sun-md5-crypt hash" - rounds, salt, chk = m.group("rounds", "salt", "chk") - #NOTE: this is *additional* rounds added to base 4096 specified by spec. - #XXX: should we note whether "$" or "," was used as rounds separator? - # not sure if that affects anything - return dict( - rounds=int(rounds) if rounds else 0, - salt=salt, - checksum=chk, - ) - -def render(rounds, salt, checksum=None): - "render a sun-md5-crypt hash or config string" - if rounds > 0: - return "$md5,rounds=%d$%s$%s" % (rounds, salt, checksum or '') - else: - return "$md5$%s$%s" % (salt, checksum or '') - -#========================================================= -#primary interface -#========================================================= -def genconfig(salt=None, rounds=None): - salt = norm_salt(salt, min_salt_chars, max_salt_chars, 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 - #FIXME: could eliminate uneeded render/parse call - info = parse(config) - info.pop("checksum") - config = genconfig(**info) - info = parse(config) - rounds, salt = info['rounds'], info['salt'] - - #run through builtin backend - checksum = raw_sun_md5_crypt(secret, rounds, salt) - return render(rounds, salt, checksum) - -#========================================================= -#secondary interface -#========================================================= -def encrypt(secret, **settings): - return genhash(secret, genconfig(**settings)) - -def verify(secret, hash): - #normalize hash format so strings compare - if hash and hash.startswith("$md5$rounds="): - hash = "$md5,rounds=" + hash[12:] - return hash == genhash(secret, hash) - -def identify(hash): - return bool(hash and _pat.match(hash)) - -autodocument(globals()) +class SunMD5Crypt(BaseHandler): + #========================================================= + #class attrs + #========================================================= + name = "sun_md5_crypt" + setting_kwds = ("salt", "rounds") + + min_salt_chars = 0 + max_salt_chars = 8 + + default_rounds = 5000 #current passlib default + min_rounds = 0 + max_rounds = 4294963199 ##2**32-1-4096 + #XXX: ^ not sure what it does if past this bound... does 32 int roll over? + rounds_cost = "linear" + + #========================================================= + #internal helpers + #========================================================= + @classmethod + def identify(cls, hash): + return bool(hash) and (hash.startswith("$md5$") or hash.startswith("$md5,")) + + _pat = re.compile(r""" + ^ + \$md5 + ([$,]rounds=(?P<rounds>\d+))? + \$(?P<salt>[A-Za-z0-9./]{0,8}) + (\$(?P<chk>[A-Za-z0-9./]{22})?)? + $ + """, re.X) + + #NOTE: trailing "$" is supposed to be part of config string, + # supposed to take both, but render with "$" + #NOTE: seen examples with both "," or "$" as md5/rounds separator, + # not sure what official format is. + # taking both, rendering "," + + @classmethod + def from_string(cls, hash): + if not hash: + raise ValueError, "no hash specified" + m = cls._pat.match(hash) + if not m: + raise ValueError, "invalid sun-md5-crypt hash" + rounds, salt, chk = m.group("rounds", "salt", "chk") + #NOTE: this is *additional* rounds added to base 4096 specified by spec. + #XXX: should we note whether "$" or "," was used as rounds separator? + # not sure if that affects anything + return cls( + rounds=int(rounds) if rounds else 0, + salt=salt, + checksum=chk, + strict=bool(chk) + ) + + def to_string(self): + rounds = self.rounds + if rounds > 0: + out = "$md5,rounds=%d$%s" % (rounds, self.salt) + else: + out = "$md5$%s" % (self.salt,) + chk = self.checksum + if chk: + out = "%s$%s" % (out, chk) + return out + + #========================================================= + #primary interface + #========================================================= + #TODO: if we're on solaris, check for native crypt() support + + def calc_checksum(self, secret): + return raw_sun_md5_crypt(secret, self.rounds, self.salt) + + #========================================================= + #eoc + #========================================================= + +autodocument(SunMD5Crypt) +register_crypt_handler(SunMD5Crypt) #========================================================= #eof #========================================================= diff --git a/passlib/tests/handler_utils.py b/passlib/tests/handler_utils.py index 0ecdff3..988eddd 100644 --- a/passlib/tests/handler_utils.py +++ b/passlib/tests/handler_utils.py @@ -33,8 +33,8 @@ class _HandlerTestCase(TestCase): #specify handler object here handler = None - #NOTE: would like unicode support for all hashes. until then, this flag is set for those which aren't. - supports_unicode = False + #this option is available for hashes which can't handle unicode + supports_unicode = True #maximum number of chars which hash will include in checksum #override this only if hash doesn't use all chars (the default) diff --git a/passlib/tests/test_hash_misc.py b/passlib/tests/test_hash_misc.py index f0742c6..a684bcb 100644 --- a/passlib/tests/test_hash_misc.py +++ b/passlib/tests/test_hash_misc.py @@ -14,6 +14,24 @@ from passlib.tests.utils import enable_option log = getLogger(__name__) #========================================================= +#NTHASH for unix +#========================================================= +from passlib.hash.nthash import NTHash + +class NTHashTest(_HandlerTestCase): + handler = NTHash + + known_correct = ( + ('passphrase', '$3$$7f8fe03093cc84b267b109625f6bbf4b'), + ('passphrase', '$NT$7f8fe03093cc84b267b109625f6bbf4b'), + ) + + known_identified_invalid = [ + #bad char in otherwise correct hash + '$3$$7f8fe03093cc84b267b109625f6bbfxb', + ] + +#========================================================= #PHPass Portable Crypt #========================================================= from passlib.hash import phpass @@ -33,24 +51,6 @@ class PHPassTest(_HandlerTestCase): ] #========================================================= -#NTHASH for unix -#========================================================= -from passlib.hash import nthash - -class NTHashTest(_HandlerTestCase): - handler = nthash - - known_correct = ( - ('passphrase', '$3$$7f8fe03093cc84b267b109625f6bbf4b'), - ('passphrase', '$NT$7f8fe03093cc84b267b109625f6bbf4b'), - ) - - known_invalid = ( - #bad char in otherwise correct hash - '$3$$7f8fe03093cc84b267b109625f6bbfxb', - ) - -#========================================================= # netbsd sha1 crypt #========================================================= from passlib.hash import sha1_crypt @@ -69,5 +69,23 @@ class SHA1CryptTest(_HandlerTestCase): ] #========================================================= +#sun md5 crypt +#========================================================= +from passlib.hash.sun_md5_crypt import SunMD5Crypt + +class SunMD5CryptTest(_HandlerTestCase): + handler = SunMD5Crypt + + known_correct = [ + #sample hash found at http://compgroups.net/comp.unix.solaris/password-file-in-linux-and-solaris-8-9 + ("passwd", "$md5$RPgLF6IJ$WTvAlUJ7MqH5xak2FMEwS/"), + ] + + known_identified_invalid = [ + #bad char in otherwise correct hash + "$md5$RPgL!6IJ$WTvAlUJ7MqH5xak2FMEwS/" + ] + +#========================================================= #EOF #========================================================= diff --git a/passlib/tests/test_hash_postgres.py b/passlib/tests/test_hash_postgres.py index ebef4e9..a47af4d 100644 --- a/passlib/tests/test_hash_postgres.py +++ b/passlib/tests/test_hash_postgres.py @@ -9,25 +9,25 @@ from logging import getLogger #site #pkg from passlib.tests.handler_utils import _HandlerTestCase -import passlib.hash.postgres_md5 as mod +from passlib.hash.postgres_md5 import PostgresMD5 #module log = getLogger(__name__) #========================================================= #database hashes #========================================================= -class PostgresMd5CryptTest(_HandlerTestCase): - handler = mod - known_correct = ( +class PostgresMD5CryptTest(_HandlerTestCase): + handler = PostgresMD5 + known_correct = [ # ((secret,user),hash) (('mypass', 'postgres'), 'md55fba2ea04fd36069d2574ea71c8efe9d'), (('mypass', 'root'), 'md540c31989b20437833f697e485811254b'), (("testpassword",'testuser'), 'md5d4fc5129cc2c25465a5370113ae9835f'), - ) - known_invalid = ( + ] + known_invalid = [ #bad 'z' char in otherwise correct hash 'md54zc31989b20437833f697e485811254b', - ) + ] #NOTE: used to support secret=(password, user) format, but removed it for now. ##def test_tuple_mode(self): diff --git a/passlib/tests/test_hash_sun_md5_crypt.py b/passlib/tests/test_hash_sun_md5_crypt.py index 56a6e11..964974a 100644 --- a/passlib/tests/test_hash_sun_md5_crypt.py +++ b/passlib/tests/test_hash_sun_md5_crypt.py @@ -9,26 +9,9 @@ from logging import getLogger #site #pkg from passlib.tests.handler_utils import _HandlerTestCase -import passlib.hash.sun_md5_crypt as mod #module log = getLogger(__name__) #========================================================= -#hash alg -#========================================================= -class SunMd5CryptTest(_HandlerTestCase): - handler = mod - - known_correct = [ - #sample hash found at http://compgroups.net/comp.unix.solaris/password-file-in-linux-and-solaris-8-9 - ("passwd", "$md5$RPgLF6IJ$WTvAlUJ7MqH5xak2FMEwS/"), - ] - - known_invalid = ( - #bad char in otherwise correct hash - "$md5$RPgL!6IJ$WTvAlUJ7MqH5xak2FMEwS/" - ) - -#========================================================= #EOF #========================================================= diff --git a/passlib/utils/handlers.py b/passlib/utils/handlers.py index 74c2f7b..4ae8841 100644 --- a/passlib/utils/handlers.py +++ b/passlib/utils/handlers.py @@ -194,7 +194,6 @@ class BaseHandler(object): default_rounds = None #if not specified, BaseHandler.norm_rounds() will require explicit rounds value every time rounds_cost = "linear" #common case - #---------------------------------------------- #misc BaseHandler configuration #---------------------------------------------- @@ -212,6 +211,7 @@ class BaseHandler(object): #init #========================================================= #XXX: rename strict kwd to _strict ? + #XXX: for from_string() purposes, a strict_salt kwd to override strict, might also be useful def __init__(self, checksum=None, salt=None, rounds=None, strict=False, **kwds): self.checksum = self.norm_checksum(checksum, strict=strict) self.salt = self.norm_salt(salt, strict=strict) @@ -268,8 +268,31 @@ class BaseHandler(object): raise AssertionError, "unknown rounds cost function" #========================================================= - #helpers + #init helpers #========================================================= + + #--------------------------------------------------------- + #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" + 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" + value = cls._has_rounds = 'rounds' in cls.setting_kwds + return value + + #--------------------------------------------------------- + #normalization/validation helpers + #--------------------------------------------------------- @classmethod def norm_checksum(cls, checksum, strict=False): if checksum is None: @@ -282,14 +305,6 @@ class BaseHandler(object): raise ValueError, "invalid characters in %s checksum" % (cls.name,) return checksum - @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" - value = cls._has_salt = 'salt' in cls.setting_kwds - return value - @classmethod def norm_salt(cls, salt, strict=False): "helper to normalize salt string; strict flag causes error even for correctable errors" @@ -317,14 +332,6 @@ class BaseHandler(object): return salt - @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" - value = cls._has_rounds = 'rounds' in cls.setting_kwds - return value - @classmethod def norm_rounds(cls, rounds, strict=False): "helper to normalize rounds value; strict flag causes error even for correctable errors" @@ -359,6 +366,42 @@ class BaseHandler(object): return rounds #========================================================= + #password hash api - formatting 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 from_string(cls, hash): + "return parsed instance from hash/configuration string; raising ValueError on invalid inputs" + raise NotImplementedError, "%s must implement from_string()" % (cls,) + + def to_string(self): + "render instance to hash or configuration string (depending on if checksum attr is set)" + raise NotImplementedError, "%s must implement from_string()" % (type(self),) + + ##def to_config_string(self): + ## "helper for generating configuration string (ignoring hash)" + ## chk = self.checksum + ## if chk: + ## try: + ## self.checksum = None + ## return self.to_string() + ## finally: + ## self.checksum = chk + ## else: + ## return self.to_string() + + #========================================================= #password hash api - primary interface (default implementation) #========================================================= @classmethod @@ -379,18 +422,6 @@ class BaseHandler(object): #password hash api - secondary interface (default implementation) #========================================================= @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, **settings): self = cls(**settings) self.checksum = self.calc_checksum(secret) @@ -405,31 +436,7 @@ class BaseHandler(object): return self.checksum == self.calc_checksum(secret) #========================================================= - #password hash api - parsing interface - #========================================================= - @classmethod - def from_string(cls, hash): - "return parsed instance from hash/configuration string; raising ValueError on invalid inputs" - raise NotImplementedError, "%s must implement from_string()" % (cls,) - - def to_string(self): - "render instance to hash or configuration string (depending on if checksum attr is set)" - raise NotImplementedError, "%s must implement from_string()" % (type(self),) - - def to_config_string(self): - "helper for generating configuration string (ignoring hash)" - chk = self.checksum - if chk: - try: - self.checksum = None - return self.to_string() - finally: - self.checksum = chk - else: - return self.to_string() - - #========================================================= - # + #eoc #========================================================= #========================================================= diff --git a/passlib/utils/pbkdf2.py b/passlib/utils/pbkdf2.py index 0c62ada..89f0f0e 100644 --- a/passlib/utils/pbkdf2.py +++ b/passlib/utils/pbkdf2.py @@ -12,6 +12,7 @@ import hmac import logging; log = logging.getLogger(__name__) import re from struct import pack +from warnings import warn #site try: from M2Crypto import EVP as _EVP @@ -21,10 +22,29 @@ except ImportError: from passlib.utils import xor_bytes #local __all__ = [ + "hmac_sha1", "pbkdf2", ] #================================================================================= +#hmac sha1 support +#================================================================================= +def hmac_sha1(key, msg): + "perform raw hmac-sha1 of a message" + return hmac(key, msg, sha1).digest() + +if _EVP: + #default *should* be sha1, which saves us a wrapper function, but might as well check. + try: + result = _EVP.hmac('x','y') + except ValueError: + #this is probably not a good sign if it happens. + warn("PassLib: M2Crypt.EVP.hmac() unexpected threw value error during passlib startup test") + else: + if result == ',\x1cb\xe0H\xa5\x82M\xfb>\xd6\x98\xef\x8e\xf9oQ\x85\xa3i': + hmac_sha1 = _EVP.hmac + +#================================================================================= #backend #================================================================================= MAX_BLOCKS = 0xffffffffL #2**32-1 @@ -32,7 +52,6 @@ MAX_BLOCKS = 0xffffffffL #2**32-1 def _resolve_prf(prf): "resolve prf string or callable -> func & digest_size" if isinstance(prf, str): - if prf.startswith("hmac-"): digest = prf[5:] |