diff options
| -rw-r--r-- | docs/lib/passlib.hash.ext_des_crypt.rst | 1 | ||||
| -rw-r--r-- | passlib/hash/des_crypt.py | 170 | ||||
| -rw-r--r-- | passlib/hash/ext_des_crypt.py | 149 | ||||
| -rw-r--r-- | passlib/hash/mysql_323.py | 119 | ||||
| -rw-r--r-- | passlib/hash/phpass.py | 234 | ||||
| -rw-r--r-- | passlib/hash/sha1_crypt.py | 41 | ||||
| -rw-r--r-- | passlib/hash/sha512_crypt.py | 115 | ||||
| -rw-r--r-- | passlib/tests/test_hash_des_crypt.py | 8 | ||||
| -rw-r--r-- | passlib/tests/test_hash_misc.py | 12 | ||||
| -rw-r--r-- | passlib/tests/test_hash_mysql.py | 10 | ||||
| -rw-r--r-- | passlib/tests/test_hash_sha_crypt.py | 37 | ||||
| -rw-r--r-- | passlib/utils/__init__.py | 9 | ||||
| -rw-r--r-- | passlib/utils/handlers.py | 759 |
13 files changed, 797 insertions, 867 deletions
diff --git a/docs/lib/passlib.hash.ext_des_crypt.rst b/docs/lib/passlib.hash.ext_des_crypt.rst index 7bc676d..d0c2676 100644 --- a/docs/lib/passlib.hash.ext_des_crypt.rst +++ b/docs/lib/passlib.hash.ext_des_crypt.rst @@ -75,3 +75,4 @@ This implementation of ext-des-crypt differs from others in one way: References ========== * `<http://fuse4bsd.creo.hu/localcgi/man-cgi.cgi?crypt+3>`_ - primary source used for description of ext-des-crypt format & algorithm +* `<http://ftp.lava.net/cgi-bin/bsdi-man?proto=1.1&query=crypt&msection=3&apropos=0>`_ - another source describing algorithm diff --git a/passlib/hash/des_crypt.py b/passlib/hash/des_crypt.py index e724909..5cad5b8 100644 --- a/passlib/hash/des_crypt.py +++ b/passlib/hash/des_crypt.py @@ -9,7 +9,9 @@ import logging; log = logging.getLogger(__name__) from warnings import warn #site #libs +from passlib.base import register_crypt_handler from passlib.utils import norm_salt, h64, autodocument +from passlib.utils.handlers import BaseHandler from passlib.utils.des import mdes_encrypt_int_block #pkg #local @@ -78,98 +80,82 @@ except ImportError: crypt = None #========================================================= -#algorithm information -#========================================================= -name = "des_crypt" -#stats: 64 bit checksum, 12 bit salt, max 8 chars of secret - -setting_kwds = ("salt",) -context_kwds = () - -min_salt_chars = max_salt_chars = 2 - -#========================================================= -#internal helpers -#========================================================= -#FORMAT: 2 chars of H64-encoded salt + 11 chars of H64-encoded checksum -_pat = re.compile(r""" - ^ - (?P<salt>[./a-z0-9]{2}) - (?P<chk>[./a-z0-9]{11})? - $""", re.X|re.I) - -def parse(hash): - if not hash: - raise ValueError, "no hash specified" - m = _pat.match(hash) - if not m: - raise ValueError, "invalid des-crypt hash" - salt, chk = m.group("salt", "chk") - return dict( - salt=salt, - checksum=chk, - ) - -def render(salt, checksum=None): - if len(salt) < 2: - raise ValueError, "invalid salt" - return "%s%s" % (salt[:2], checksum or '') - -#========================================================= -#primary interface -#========================================================= -def genconfig(salt=None): - salt = norm_salt(salt, min_salt_chars, max_salt_chars, name=name) - return render(salt, None) - -def genhash(secret, config): - #parse and run through genconfig to validate configuration - info = parse(config) - info.pop("checksum") - config = genconfig(**info) - - #forbidding nul chars because linux crypt (and most C implementations) won't accept it either. - if '\x00' in secret: - raise ValueError, "null char in secret" - - #XXX: des-crypt predates unicode, not sure if there's an official policy for handing it. - #for now, just coercing to utf-8. - if isinstance(secret, unicode): - secret = secret.encode("utf-8") - - #run through chosen backend - if crypt: - #XXX: given a single letter salt, linux crypt returns a hash with the original salt doubled, - # but appears to calculate the hash based on the letter + "G" as the second byte. - # this results in a hash that won't validate, which is DEFINITELY wrong. - # need to find out it's underlying logic, and if it's part of spec, - # or just weirdness that should actually be an error. - # until then, passlib raises an error in genconfig() - - #XXX: given salt chars outside of h64.CHARS range, linux crypt - # does something unknown when decoding salt to 12 bit int, - # successfully creates a hash, but reports the original salt. - # need to find out it's underlying logic, and if it's part of spec, - # or just weirdness that should actually be an error. - # until then, passlib raises an error for bad salt chars. - return crypt(secret, config) - else: - salt = config[:2] - return render(salt, raw_crypt(secret, salt)) - -#========================================================= -#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()) +#handler +#========================================================= +class DesCrypt(BaseHandler): + #========================================================= + #class attrs + #========================================================= + name = "des_crypt" + setting_kwds = ("salt",) + min_salt_chars = max_salt_chars = 2 + + #========================================================= + #formatting + #========================================================= + #FORMAT: 2 chars of H64-encoded salt + 11 chars of H64-encoded checksum + + _pat = re.compile(r""" + ^ + (?P<salt>[./a-z0-9]{2}) + (?P<chk>[./a-z0-9]{11})? + $""", re.X|re.I) + + @classmethod + def identify(cls, hash): + return bool(hash and cls._pat.match(hash)) + + @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 des-crypt hash" + salt, chk = m.group("salt", "chk") + return cls(salt=salt, checksum=chk, strict=bool(chk)) + + def to_string(self): + return "%s%s" % (self.salt, self.checksum or '') + + #========================================================= + #backend + #========================================================= + def calc_checksum(self, secret): + #forbidding nul chars because linux crypt (and most C implementations) won't accept it either. + if '\x00' in secret: + raise ValueError, "null char in secret" + + #XXX: des-crypt predates unicode, not sure if there's an official policy for handing it. + #for now, just coercing to utf-8. + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + + #run through chosen backend + if crypt: + #XXX: given a single letter salt, linux crypt returns a hash with the original salt doubled, + # but appears to calculate the hash based on the letter + "G" as the second byte. + # this results in a hash that won't validate, which is DEFINITELY wrong. + # need to find out it's underlying logic, and if it's part of spec, + # or just weirdness that should actually be an error. + # until then, passlib raises an error in genconfig() + + #XXX: given salt chars outside of h64.CHARS range, linux crypt + # does something unknown when decoding salt to 12 bit int, + # successfully creates a hash, but reports the original salt. + # need to find out it's underlying logic, and if it's part of spec, + # or just weirdness that should actually be an error. + # until then, passlib raises an error for bad salt chars. + return self.from_string(crypt(secret, self.to_string())).checksum + else: + return raw_crypt(secret, self.salt) + + #========================================================= + #eoc + #========================================================= + +autodocument(DesCrypt) +register_crypt_handler(DesCrypt) #========================================================= #eof #========================================================= diff --git a/passlib/hash/ext_des_crypt.py b/passlib/hash/ext_des_crypt.py index e5a9baa..ab670bc 100644 --- a/passlib/hash/ext_des_crypt.py +++ b/passlib/hash/ext_des_crypt.py @@ -8,6 +8,8 @@ import logging; log = logging.getLogger(__name__) from warnings import warn #site #libs +from passlib.base import register_crypt_handler +from passlib.utils.handlers import BaseHandler from passlib.utils import norm_rounds, norm_salt, h64, autodocument from passlib.utils.des import mdes_encrypt_int_block from passlib.hash.des_crypt import _crypt_secret_to_key @@ -61,89 +63,72 @@ def raw_ext_crypt(secret, rounds, salt): #TODO: check if crypt supports ext-des-crypt. #========================================================= -#algorithm information +#handler #========================================================= -name = "ext_des_crypt" -#stats: 64 bit checksum, 24 bit salt, 0..(1<<24)-1 rounds - -setting_kwds = ("salt", "rounds") -context_kwds = () - -min_salt_chars = max_salt_chars = 4 - -default_rounds = 10000 -min_rounds = 0 #NOTE: some sources (OpenBSD login.conf) report 7250 as minimum allowed rounds -max_rounds = 16777215 # (1<<24)-1 - -rounds_cost = "linear" - -#========================================================= -#internal helpers -#========================================================= -_pat = re.compile(r""" - ^ - _ - (?P<rounds>[./a-z0-9]{4}) - (?P<salt>[./a-z0-9]{4}) - (?P<chk>[./a-z0-9]{11})? - $""", re.X|re.I) - -def parse(hash): - if not hash: - raise ValueError, "no hash specified" - m = _pat.match(hash) - if not m: - raise ValueError, "invalid ext-des-crypt hash" - rounds, salt, chk = m.group("rounds", "salt", "chk") - return dict( - rounds=h64.decode_int24(rounds), - salt=salt, - checksum=chk, - ) - -def render(rounds, salt, checksum=None): - if rounds < 0: - raise ValueError, "invalid rounds" - if len(salt) != 4: - raise ValueError, "invalid salt" - if checksum and len(checksum) != 11: - raise ValueError, "invalid checksum" - return "_%s%s%s" % (h64.encode_int24(rounds), 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 - #TODO: could *easily* optimize this to skip excess render/parse - info = parse(config) - info.pop("checksum") - config = genconfig(**info) - info = parse(config) - rounds, salt = info['rounds'], info['salt'] - - #run through chosen backend - checksum = raw_ext_crypt(secret, rounds, salt) - return render(rounds, salt, checksum) - -#========================================================= -#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()) +class ExtDesCrypt(BaseHandler): + #========================================================= + #class attrs + #========================================================= + name = "ext_des_crypt" + setting_kwds = ("salt", "rounds") + + min_salt_chars = max_salt_chars = 4 + + default_rounds = 1000 + min_rounds = 0 + max_rounds = 16777215 # (1<<24)-1 + rounds_cost = "linear" + + checksum_chars = 11 + + # NOTE: OpenBSD login.conf reports 7250 as minimum allowed rounds, + # but not sure if that's strictly enforced, or silently clipped. + + #========================================================= + #internal helpers + #========================================================= + _pat = re.compile(r""" + ^ + _ + (?P<rounds>[./a-z0-9]{4}) + (?P<salt>[./a-z0-9]{4}) + (?P<chk>[./a-z0-9]{11})? + $""", re.X|re.I) + + @classmethod + def identify(cls, hash): + return bool(hash and cls._pat.match(hash)) + + @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 ext-des-crypt hash" + rounds, salt, chk = m.group("rounds", "salt", "chk") + return cls( + rounds=h64.decode_int24(rounds), + salt=salt, + checksum=chk, + strict=bool(chk), + ) + + def to_string(self): + return "_%s%s%s" % (h64.encode_int24(self.rounds), self.salt, self.checksum or '') + + #========================================================= + #backend + #========================================================= + def calc_checksum(self, secret): + return raw_ext_crypt(secret, self.rounds, self.salt) + + #========================================================= + #eoc + #========================================================= + +autodocument(ExtDesCrypt) +register_crypt_handler(ExtDesCrypt, force=True) #========================================================= #eof #========================================================= diff --git a/passlib/hash/mysql_323.py b/passlib/hash/mysql_323.py index d4b1917..29b6b30 100644 --- a/passlib/hash/mysql_323.py +++ b/passlib/hash/mysql_323.py @@ -18,7 +18,9 @@ from warnings import warn #site #libs #pkg +from passlib.base import register_crypt_handler from passlib.utils import autodocument +from passlib.utils.handlers import PlainHandler #local __all__ = [ "genhash", @@ -31,61 +33,68 @@ __all__ = [ #========================================================= #backend #========================================================= - -#========================================================= -#algorithm information -#========================================================= -name = "mysql_323" -#stats: 62 bit checksum, no salt - -setting_kwds = () -context_kwds = () - -#========================================================= -#internal helpers -#========================================================= -_pat = re.compile(r"^[0-9a-f]{16}$", re.I) - -#========================================================= -#primary interface -#========================================================= -def genconfig(): - return None - -def genhash(secret, config): - if config and not identify(config): - raise ValueError, "not a mysql-323 hash" - - MASK_32 = 0xffffffff - MASK_31 = 0x7fffffff - - nr1 = 0x50305735 - nr2 = 0x12345671 - add = 7 - for c in secret: - if c in ' \t': - continue - tmp = ord(c) - nr1 ^= ((((nr1 & 63)+add)*tmp) + (nr1 << 8)) & MASK_32 - nr2 = (nr2+((nr2 << 8) ^ nr1)) & MASK_32 - add = (add+tmp) & MASK_32 - return "%08x%08x" % (nr1 & MASK_31, nr2 & MASK_31) - -#========================================================= -#secondary interface -#========================================================= -def encrypt(secret, **settings): - return genhash(secret, genconfig(**settings)) - -def verify(secret, hash): - if not hash: - raise ValueError, "no hash specified" - return hash.lower() == genhash(secret, hash) - -def identify(hash): - return bool(hash and _pat.match(hash)) - -autodocument(globals()) +class MySQL_323(PlainHandler): + + #========================================================= + #class attrs + #========================================================= + name = "mysql_323" + + #========================================================= + #init + #========================================================= + @classmethod + def norm_checksum(cls, chk, strict=False): + if chk: + return chk.lower() #to make upper-case strings verify properly + return None + + #========================================================= + #formatting + #========================================================= + _pat = re.compile(r"^[0-9a-f]{16}$", re.I) + + @classmethod + def identify(cls, hash): + return bool(hash and cls._pat.match(hash)) + + @classmethod + def from_string(cls, hash): + if not hash: + raise ValueError, "no hash specified" + m = cls._pat.match(hash) + if not m: + raise ValueError, "not a recognized mysql-323 hash" + return cls(checksum=hash) + + def to_string(self): + return self.checksum + + #========================================================= + #backend + #========================================================= + def calc_checksum(self, secret): + MASK_32 = 0xffffffff + MASK_31 = 0x7fffffff + + nr1 = 0x50305735 + nr2 = 0x12345671 + add = 7 + for c in secret: + if c in ' \t': + continue + tmp = ord(c) + nr1 ^= ((((nr1 & 63)+add)*tmp) + (nr1 << 8)) & MASK_32 + nr2 = (nr2+((nr2 << 8) ^ nr1)) & MASK_32 + add = (add+tmp) & MASK_32 + return "%08x%08x" % (nr1 & MASK_31, nr2 & MASK_31) + + #========================================================= + #eoc + #========================================================= + +autodocument(MySQL_323) +register_crypt_handler(MySQL_323) #========================================================= #eof #========================================================= diff --git a/passlib/hash/phpass.py b/passlib/hash/phpass.py index a771758..5351155 100644 --- a/passlib/hash/phpass.py +++ b/passlib/hash/phpass.py @@ -16,6 +16,8 @@ from warnings import warn #site #libs from passlib.utils import norm_rounds, norm_salt, h64, autodocument +from passlib.utils.handlers import BaseHandler +from passlib.base import register_crypt_handler #pkg #local __all__ = [ @@ -27,133 +29,113 @@ __all__ = [ ] #========================================================= -#algorithm information +#phpass #========================================================= -name = "phpass" -#stats: 128 bit checksum, 24 bit salt - -setting_kwds = ("salt", "rounds") -context_kwds = () - -min_salt_chars = max_salt_chars = 8 - -default_rounds = 9 -min_rounds = 7 -max_rounds = 30 - -rounds_cost = "log2" - -#========================================================= -#internal helpers -#========================================================= -#$P$9IQRaTwmfeRo7ud9Fh4E2PdI0S3r.L0 -# $P$ 9 IQRaTwmf eRo7ud9Fh4E2PdI0S3r.L0 - -_pat = re.compile(r""" - ^ - \$ - (?P<ident>[PH]) - \$ - (?P<rounds>[A-Za-z0-9./]) - (?P<salt>[A-Za-z0-9./]{8}) - (?P<chk>[A-Za-z0-9./]{22})? - $ - """, re.X) - -def parse(hash): - if not hash: - raise ValueError, "no hash specified" - m = _pat.match(hash) - if not m: - raise ValueError, "invalid phpass portable hash" - ident, rounds, salt, chk = m.group("ident", "rounds", "salt", "chk") - out = dict( - rounds=h64.decode_6bit(rounds), - salt=salt, - checksum=chk, - ) - if ident != "P": - out['ident'] = ident - return out - -def render(rounds, salt, checksum=None, ident="P"): - if rounds < 0 or rounds > 31: - raise ValueError, "invalid rounds" - return "$%s$%s%s%s" % (ident, h64.encode_6bit(rounds), salt, checksum or '') - -#========================================================= -#primary interface -#========================================================= -def genconfig(salt=None, rounds=None, ident="P"): - """generate md5-crypt configuration string - - :param salt: - optional salt string to use. - - if omitted, one will be automatically generated (recommended). - - length must be be 8 characters. - characters must be in range ``./A-Za-z0-9``. - - :param rounds: - optional rounds parameter. - - like bcrypt's rounds value, phpass' rounds value is logarithmic, - each increase of +1 will double the actual number of rounds used. - - :param ident: - - phpBB3 uses ``H`` instead of ``P`` for it's identifier. - this may be set to ``H`` in order to generate phpBB3 compatible hashes. - - :returns: - phpass configuration string. - """ - if ident not in ("P", "H"): - raise ValueError, "invalid ident: %r" % (ident,) - salt = norm_salt(salt, 8, name=name) - if rounds is None: - rounds = default_rounds - if rounds < 7 or rounds > 30: - #NOTE: PHPass raises error when encrypting if rounds are outside these bounds. - raise ValueError, "rounds must be between 7..30 inclusive" - return render(rounds, salt, None, ident) - -def genhash(secret, config): - #parse and run through genconfig to validate configuration - info = parse(config) - info.pop("checksum") - config = genconfig(**info) - info = parse(config) - ident, rounds, salt = info.get("ident","P"), info['rounds'], info['salt'] - - #FIXME: can't find definitive policy on how phpass handles non-ascii. - if isinstance(secret, unicode): - secret = secret.encode("utf-8") - - real_rounds = 1<<rounds - result = md5(salt + secret).digest() - r = 0 - while r < real_rounds: - result = md5(result + secret).digest() - r += 1 - - checksum = h64.encode_bytes(result) - return render(rounds, salt, checksum, ident) - -#========================================================= -#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()) +class PHPass(BaseHandler): + + #========================================================= + #class attrs + #========================================================= + name = "phpass" + setting_kwds = ("salt", "rounds", "ident") + + min_salt_chars = max_salt_chars = 8 + + default_rounds = 9 + min_rounds = 7 + max_rounds = 30 + rounds_cost = "log2" + + _strict_rounds_bounds = True + _extra_init_settings = ("ident",) + + #========================================================= + #instance attrs + #========================================================= + ident = None + + #========================================================= + #init + #========================================================= + @classmethod + def norm_ident(cls, ident, strict=False): + if not ident: + if strict: + raise ValueError, "no ident specified" + ident = "P" + if ident not in ("P", "H"): + raise ValueError, "invalid ident: %r" % (ident,) + return ident + + #========================================================= + #formatting + #========================================================= + + @classmethod + def identify(cls, hash): + return bool(hash) and (hash.startswith("$P$") or hash.startswith("$H$")) + + #$P$9IQRaTwmfeRo7ud9Fh4E2PdI0S3r.L0 + # $P$ + # 9 + # IQRaTwmf + # eRo7ud9Fh4E2PdI0S3r.L0 + _pat = re.compile(r""" + ^ + \$ + (?P<ident>[PH]) + \$ + (?P<rounds>[A-Za-z0-9./]) + (?P<salt>[A-Za-z0-9./]{8}) + (?P<chk>[A-Za-z0-9./]{22})? + $ + """, re.X) + + @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 phpass portable hash" + ident, rounds, salt, chk = m.group("ident", "rounds", "salt", "chk") + return cls( + ident=ident, + rounds=h64.decode_6bit(rounds), + salt=salt, + checksum=chk, + strict=bool(chk), + ) + + def to_string(self): + return "$%s$%s%s%s" % (self.ident, h64.encode_6bit(self.rounds), self.salt, self.checksum or '') + + #========================================================= + #backend + #========================================================= + def calc_checksum(self, secret): + #FIXME: can't find definitive policy on how phpass handles non-ascii. + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + real_rounds = 1<<self.rounds + result = md5(self.salt + secret).digest() + r = 0 + while r < real_rounds: + result = md5(result + secret).digest() + r += 1 + return h64.encode_bytes(result) + + #========================================================= + #eoc + #========================================================= + +autodocument(PHPass, settings_doc=""" +:param ident: + phpBB3 uses ``H`` instead of ``P`` for it's identifier, + this may be set to ``H`` in order to generate phpBB3 compatible hashes. + it defaults to ``P``. +""") +register_crypt_handler(PHPass) #========================================================= #eof #========================================================= diff --git a/passlib/hash/sha1_crypt.py b/passlib/hash/sha1_crypt.py index 00f5c58..53f6b0d 100644 --- a/passlib/hash/sha1_crypt.py +++ b/passlib/hash/sha1_crypt.py @@ -18,7 +18,7 @@ except ImportError: _EVP = None #libs from passlib.utils import norm_rounds, norm_salt, autodocument, h64 -from passlib.utils.handlers import BaseSRHandler +from passlib.utils.handlers import BaseHandler from passlib.base import register_crypt_handler #pkg #local @@ -45,16 +45,13 @@ if _EVP: #========================================================= #sha1-crypt #========================================================= -class sha1_crypt(BaseSRHandler): +class SHA1Crypt(BaseHandler): #========================================================= - #algorithm information + #class attrs #========================================================= name = "sha1_crypt" - #stats: ?? bit checksum, ?? bit salt, 2**(4..31) rounds - setting_kwds = ("salt", "rounds") - context_kwds = () default_salt_chars = 8 min_salt_chars = 0 @@ -66,14 +63,18 @@ class sha1_crypt(BaseSRHandler): rounds_cost = "linear" #========================================================= - #internal helpers + #formatting #========================================================= + @classmethod + def identify(cls, hash): + return bool(hash) and hash.startswith("$sha1$") + _pat = re.compile(r""" ^ \$sha1 \$(?P<rounds>\d+) \$(?P<salt>[A-Za-z0-9./]{0,64}) - (\$(?P<chk>[A-Za-z0-9./]{28}))? + (\$(?P<chk>[A-Za-z0-9./]{28})?)? $ """, re.X) @@ -91,6 +92,7 @@ class sha1_crypt(BaseSRHandler): rounds=int(rounds), salt=salt, checksum=chk, + strict=bool(chk), ) def to_string(self): @@ -100,22 +102,18 @@ class sha1_crypt(BaseSRHandler): return out #========================================================= - #primary interface + #backend #========================================================= - - #genconfig - uses default method that wraps cls() + to_string() - #genhash - uses default method that wraps from_string() + calc_checksum() + to_string() - def calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") rounds = self.rounds - result = self.salt + "$sha1$" + str(rounds) + result = "%s$sha1$%s" % (self.salt, rounds) r = 0 while r < rounds: result = hmac_sha1(secret, result) r += 1 - return h64.encode_transposed_bytes(result, cls._chk_offsets) + return h64.encode_transposed_bytes(result, self._chk_offsets) _chk_offsets = [ 2,1,0, @@ -128,20 +126,11 @@ class sha1_crypt(BaseSRHandler): ] #========================================================= - #secondary interface - #========================================================= - def identify(hash): - return bool(hash) and hash.startswith("$sha1$") - - #encrypt - use default method that wraps cls() + calc_checksum() + to_string() - #verify - use default method that uses equality + from_string() + calc_checksum() - - #========================================================= #eoc #========================================================= -autodocument(sha1_crypt) -register_crypt_handler(sha1_crypt) +autodocument(SHA1Crypt) +register_crypt_handler(SHA1Crypt) #========================================================= #eof #========================================================= diff --git a/passlib/hash/sha512_crypt.py b/passlib/hash/sha512_crypt.py index a6f2aee..62d5789 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 norm_rounds, norm_salt, h64, autodocument from passlib.hash.sha256_crypt import raw_sha_crypt -from passlib.utils.handlers import BaseSRHandler +from passlib.utils.handlers import BaseHandler from passlib.base import register_crypt_handler #pkg #local @@ -92,7 +92,7 @@ crypt = None #========================================================= #sha 512 crypt #========================================================= -class sha512_crypt(BaseSRHandler): +class SHA512Crypt(BaseHandler): #========================================================= #algorithm information @@ -100,7 +100,6 @@ class sha512_crypt(BaseSRHandler): name = "sha512_crypt" setting_kwds = ("salt", "rounds") - context_kwds = () min_salt_chars = 0 max_salt_chars = 16 @@ -111,8 +110,23 @@ class sha512_crypt(BaseSRHandler): 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) - #regexp used to recognize & parse hashes + #========================================================= + #parsing + #========================================================= + @classmethod + def identify(cls, hash): + return bool(hash) and hash.startswith("$6$") + + #: regexp used to parse hashes _pat = re.compile(r""" ^ \$6 @@ -128,71 +142,6 @@ class sha512_crypt(BaseSRHandler): $ """, re.X) - #========================================================= - # - #========================================================= - def __init__(self, implicit_rounds=None, **kwds): - self.__super = super(sha512_crypt, self) - self.__super.__init__(**kwds) - self.implicit_rounds = implicit_rounds - - #========================================================= - #password hash api - primary interface - #========================================================= - @classmethod - def genconfig(cls, salt=None, rounds=None, implicit_rounds=True): - """generate sha512-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: - sha512-crypt configuration string. - """ - return cls(salt=salt, rounds=rounds, implicit_rounds=implicit_rounds).to_string() - - @classmethod - def genhash(cls, secret, config): - #parse and run through genconfig to validate configuration - self = cls.from_string(config) - - #run through chosen backend - if crypt: - #using system's crypt routine. - if isinstance(secret, unicode): - secret = secret.encode("utf-8") - return crypt(secret, config) - else: - #using builtin routine - self.checksum, self.salt, self.rounds = raw_sha512_crypt(secret, self.salt, self.rounds) - return self.to_string() - - #========================================================= - #password hash api - secondary interface - #========================================================= - @classmethod - def identify(cls, hash): - return bool(hash) and hash.startswith("$6$") - - #encrypt - use default method that wraps genconfig + genhash - #verify - use default method that uses equality + genhash - - #========================================================= - #password hash api - parsing interface - #========================================================= @classmethod def from_string(cls, hash): if not hash: @@ -211,19 +160,39 @@ class sha512_crypt(BaseSRHandler): strict=bool(chk), ) - def to_string(self): #, rounds, salt, checksum=None, implicit_rounds=True): - assert '$' not in self.salt + def to_string(self): if self.rounds == 5000 and self.implicit_rounds: return "$6$%s$%s" % (self.salt, self.checksum or '') else: return "$6$rounds=%d$%s$%s" % (self.rounds, self.salt, self.checksum or '') #========================================================= + #backend + #========================================================= + def calc_checksum(self, secret): + #run through chosen backend + if crypt: + #using system's crypt routine. + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + return self.from_string(crypt(secret, self.to_string())).checksum + else: + #using builtin routine + checksum, salt, rounds = raw_sha512_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 + + #========================================================= #eoc #========================================================= -autodocument(sha512_crypt) -register_crypt_handler(sha512_crypt) + ##:param implicit_rounds: + ## + ## this is an internal option which generally doesn't need to be touched. + +autodocument(SHA512Crypt) +register_crypt_handler(SHA512Crypt) #========================================================= #eof #========================================================= diff --git a/passlib/tests/test_hash_des_crypt.py b/passlib/tests/test_hash_des_crypt.py index 78b73a8..6659138 100644 --- a/passlib/tests/test_hash_des_crypt.py +++ b/passlib/tests/test_hash_des_crypt.py @@ -20,7 +20,7 @@ log = getLogger(__name__) #========================================================= class DesCryptTest(_HandlerTestCase): "test DesCrypt algorithm" - handler = mod + handler = mod.DesCrypt secret_chars = 8 #TODO: test @@ -35,10 +35,10 @@ class DesCryptTest(_HandlerTestCase): ('AlOtBsOl', 'cEpWz5IUCShqM'), (u'hell\u00D6', 'saykDgk3BPZ9E'), ) - known_invalid = ( + known_invalid = [ #bad char in otherwise correctly formatted hash '!gAwTx2l6NADI', - ) + ] if mod.backend != "builtin" and enable_option("all-backends"): @@ -56,7 +56,7 @@ if mod.backend != "builtin" and enable_option("all-backends"): class ExtDesCryptTest(_HandlerTestCase): "test ExtDesCrypt algorithm" - handler = mod2 + handler = mod2.ExtDesCrypt known_correct = ( (" ", "_K1..crsmZxOLzfJH8iw"), ("my", '_KR/.crsmykRplHbAvwA'), #<- to detect old 12-bit rounds bug diff --git a/passlib/tests/test_hash_misc.py b/passlib/tests/test_hash_misc.py index ef5aa64..f0742c6 100644 --- a/passlib/tests/test_hash_misc.py +++ b/passlib/tests/test_hash_misc.py @@ -19,7 +19,7 @@ log = getLogger(__name__) from passlib.hash import phpass class PHPassTest(_HandlerTestCase): - handler = phpass + handler = phpass.PHPass known_correct = ( ('', '$P$7JaFQsPzJSuenezefD/3jHgt5hVfNH0'), @@ -27,10 +27,10 @@ class PHPassTest(_HandlerTestCase): ('test12345', '$P$9IQRaTwmfeRo7ud9Fh4E2PdI0S3r.L0'), #from the source ) - known_invalid = ( + known_identified_invalid = [ #bad char in otherwise correct hash '$P$9IQRaTwmfeRo7ud9Fh4E2PdI0S3r!L0', - ) + ] #========================================================= #NTHASH for unix @@ -56,17 +56,17 @@ class NTHashTest(_HandlerTestCase): from passlib.hash import sha1_crypt class SHA1CryptTest(_HandlerTestCase): - handler = sha1_crypt.sha1_crypt + handler = sha1_crypt.SHA1Crypt known_correct = ( ("password", "$sha1$19703$iVdJqfSE$v4qYKl1zqYThwpjJAoKX6UvlHq/a"), ("password", "$sha1$21773$uV7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH"), ) - known_invalid = ( + known_identified_invalid = [ #bad char in otherwise correct hash '$sha1$21773$u!7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH', - ) + ] #========================================================= #EOF diff --git a/passlib/tests/test_hash_mysql.py b/passlib/tests/test_hash_mysql.py index d0f97dd..e65544b 100644 --- a/passlib/tests/test_hash_mysql.py +++ b/passlib/tests/test_hash_mysql.py @@ -17,19 +17,19 @@ log = getLogger(__name__) #========================================================= #database hashes #========================================================= -class Mysql323CryptTest(_HandlerTestCase): - handler = mod3 +class MySQL_323Test(_HandlerTestCase): + handler = mod3.MySQL_323 - #remove single space from secrets, since mysql-10 DISCARDS WHITESPACE !?! + #remove single space from secrets, since mysql-323 ignores all whitespace (?!) standard_secrets = [ x for x in _HandlerTestCase.standard_secrets if x != ' ' ] known_correct = ( ('mypass', '6f8c114b58f2ce9e'), ) - known_invalid = ( + known_invalid = [ #bad char in otherwise correct hash '6z8c114b58f2ce9e', - ) + ] def test_whitespace(self): "check whitespace is ignored per spec" diff --git a/passlib/tests/test_hash_sha_crypt.py b/passlib/tests/test_hash_sha_crypt.py index 169a414..45dd897 100644 --- a/passlib/tests/test_hash_sha_crypt.py +++ b/passlib/tests/test_hash_sha_crypt.py @@ -63,8 +63,8 @@ if mod2.backend != "builtin" and enable_option("all-backends"): #========================================================= #test sha512-crypt #========================================================= -class Sha512CryptTest(_HandlerTestCase): - handler = mod5.sha512_crypt +class SHA512CryptTest(_HandlerTestCase): + handler = mod5.SHA512Crypt supports_unicode = True known_correct = ( @@ -74,15 +74,13 @@ class Sha512CryptTest(_HandlerTestCase): ('Compl3X AlphaNu3meric', '$6$rounds=10787$wakX8nGKEzgJ4Scy$X78uqaX1wYXcSCtS4BVYw2trWkvpa8p7lkAtS9O/6045fK4UB2/Jia0Uy/KzCpODlfVxVNZzCCoV9s2hoLfDs/'), ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', '$6$rounds=11065$5KXQoE1bztkY5IZr$Jf6krQSUKKOlKca4hSW07MSerFFzVIZt/N3rOTsUgKqp7cUdHrwV8MoIVNCk9q9WL3ZRMsdbwNXpVk0gVxKtz1'), ) - known_invalid = ( - #bad char in otherwise correct hash - '$6$rounds=11021$KsvQipYPWpr9:wWP$v7xjI4X6vyVptJjB1Y02vZC5SaSijBkGmq1uJhPr3cvqvvkd42Xvo48yLVPFt8dvhCsnlUgpX.//Cxn91H4qy1', - ) - known_identified_invalid = ( - ###zero-padded rounds + known_identified_invalid = [ + #zero-padded rounds '$6$rounds=011021$KsvQipYPWpr93wWP$v7xjI4X6vyVptJjB1Y02vZC5SaSijBkGmq1uJhPr3cvqvvkd42Xvo48yLVPFt8dvhCsnlUgpX.//Cxn91H4qy1', - ) + #bad char in otherwise correct hash + '$6$rounds=11021$KsvQipYPWpr9:wWP$v7xjI4X6vyVptJjB1Y02vZC5SaSijBkGmq1uJhPr3cvqvvkd42Xvo48yLVPFt8dvhCsnlUgpX.//Cxn91H4qy1', + ] #NOTE: these test cases taken from spec definition at http://www.akkadia.org/drepper/SHA-crypt.txt cases512 = [ @@ -121,7 +119,7 @@ class Sha512CryptTest(_HandlerTestCase): def test_spec_vectors(self): "verify sha512-crypt passes specification test vectors" - handler = mod5 + handler = self.handler #NOTE: the 'roundstoolow' vector is known to raise a warning, which we silence here if catch_warnings: @@ -130,22 +128,13 @@ class Sha512CryptTest(_HandlerTestCase): warnings.filterwarnings("ignore", "sha512_crypt algorithm does not allow less than 1000 rounds: 10") for config, secret, hash in self.cases512: - - result = handler.genhash(secret, config) - - #parse config - settings = handler.parse(config) - #make sure we got expected result back + result = handler.genhash(secret, config) self.assertEqual(result, hash, "hash=%r secret=%r:" % (hash, secret)) - #parse result and check that salt was truncated to max 16 chars - info = handler.parse(result) - if len(settings['salt']) > 16: - #spec sez we can truncate salt - self.assertEqual(info['salt'], settings['salt'][:16], "hash=%r secret=%r:" % (hash, secret)) - else: - self.assertEqual(info['salt'], settings['salt'], "hash=%r secret=%r:" % (hash, secret)) + #make sure parser truncated salts + info = handler.from_string(config) + self.assert_(len(info.salt) <= 16, "hash=%r secret=%r:" % (hash, secret)) if catch_warnings: ctx.__exit__(None,None,None) @@ -154,7 +143,7 @@ if mod5.backend != "builtin" and enable_option("all-backends"): #monkeypatch sha512-crypt mod so it uses builtin backend - class BuiltinSha512CryptTest(Sha512CryptTest): + class BuiltinSHA512CryptTest(SHA512CryptTest): case_prefix = "sha512-crypt (builtin backend)" def setUp(self): diff --git a/passlib/utils/__init__.py b/passlib/utils/__init__.py index 4cca5f1..cb42003 100644 --- a/passlib/utils/__init__.py +++ b/passlib/utils/__init__.py @@ -435,7 +435,7 @@ class dict_proxy(object): except KeyError: raise AttributeError, "attribute not found: %r" % (key,) -def autodocument(scope, salt_charset="[./0-9A-Za-z]", context_doc=''): +def autodocument(scope, salt_charset="[./0-9A-Za-z]", settings_doc='', context_doc=''): """helper to auto-generate documentation for password hash handler :arg scope: dict containing encrypt/verify/etc functions (module scope or class dict) @@ -468,6 +468,8 @@ def autodocument(scope, salt_charset="[./0-9A-Za-z]", context_doc=''): if context_doc: context_doc = context_doc.rstrip() + "\n" + if settings_doc: + settings_doc = settings_doc.rstrip() + "\n" def get_func(name): func = getattr(scope, name) @@ -481,7 +483,7 @@ def autodocument(scope, salt_charset="[./0-9A-Za-z]", context_doc=''): genconfig = get_func("genconfig") if not genconfig.__doc__: if setting_kwds: - if has_other: + if has_other and not settings_doc: raise NotImplementedError, "can't auto generate genconfig docs w/ unknown setting_kwds" d = "generate %(name)s configuration string\n\n" % dict(name=name) @@ -500,6 +502,9 @@ def autodocument(scope, salt_charset="[./0-9A-Za-z]", context_doc=''): if log_rounds: d += """ %(name)s's rounds value is logarithmic, the actual number of rounds used is ``2**rounds``.\n""" % dict(name=name) + if settings_doc: + d += settings_doc + "\n" + d += """\n:raises ValueError: if invalid settings are passed in\n\n""" d += """:returns:\n %(name)s configuration string\n""" % dict(name=name) else: diff --git a/passlib/utils/handlers.py b/passlib/utils/handlers.py index 7e38108..a515d4e 100644 --- a/passlib/utils/handlers.py +++ b/passlib/utils/handlers.py @@ -19,338 +19,156 @@ from passlib.utils import abstractmethod, abstractclassmethod, classproperty, h6 __all__ = [ #framework for implementing handlers - 'CryptHandler', - 'ExtCryptHandler', + 'BaseHandler', + 'PlainHandler', ] -#========================================================== -#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 - #========================================================= - -#========================================================= -# -#========================================================= -##class ExtCryptHandler(CryptHandler): -## """class providing an extended handler interface, -## allowing manipulation of hash & config strings. -## -## this extended interface adds methods for parsing and rendering -## a hash or config string to / from a dictionary of components. -## -## this interface is generally easier to use when *implementing* hash -## algorithms, and as such is used through passlib. it's kept separate -## from :class:`CryptHandler` itself, since it's features are not typically -## required for user-facing purposes. -## -## when implementing a hash algorithm, subclasses must implement: -## -## * parse() -## * render() -## * genconfig() - render, _norm_salt, _norm_rounds usually helpful for this -## * genhash() - parse, render usually helpful for this -## -## subclasses may optionally implement more efficient versions of -## these functions, though the defaults should be sufficient: -## -## * identify() - requires parse() -## * verify() - requires parse() -## -## some helper methods are provided for implementing genconfig, genhash & verify. -## """ +###========================================================== +###base interface for all the crypt algorithm implementations +###========================================================== +##class CryptHandler(object): +## """helper class for implementing a password algorithm using class methods""" ## ## #========================================================= ## #class attrs ## #========================================================= ## -## #--------------------------------------------------------- -## # _norm_salt() configuration -## #--------------------------------------------------------- -## -## salt_chars = None #fill in with (maxium) number of salt chars required, and _norm_salt() will handle truncating etc -## salt_charset = h64.CHARS #helper used when generating salt -## salt_charpat = None #optional regexp used by _norm_salt to validate salts -## -## #override only if minimum number of salt chars is different from salt_chars -## @classproperty -## def min_salt_chars(cls): -## return cls.salt_chars -## -## #--------------------------------------------------------- -## #_norm_rounds() configuration -## #--------------------------------------------------------- -## default_rounds = None #default number of rounds to use if none specified (can be name of a preset) -## min_rounds = None #minimum number of rounds (smaller values silently ignored) -## max_rounds = None #maximum number of rounds (larger values silently ignored) +## 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" ## ## #========================================================= -## #backend parsing routines - used by helpers below +## #primary interface - primary methods implemented by each handler ## #========================================================= ## ## @abstractclassmethod -## def parse(cls, hash): -## """parse hash or config into dictionary. -## -## :arg hash: the hash/config string to parse -## -## :raises ValueError: -## If hash/config string is empty, -## or not recognized as belonging to this algorithm -## -## :returns: -## dictionary containing a subset of the keys -## specified in :attr:`setting_kwds`. -## -## commonly used keys are ``salt``, ``rounds``. -## -## If and only if the string is a hash, the dict should also contain -## the key ``checksum``, mapping to the checksum portion of the hash. -## -## .. note:: -## Specific implementations may perform anywhere from none to full -## validation of input string; the primary goal of this method -## is to parse settings from single string into kwds -## which will be recognized by :meth:`render` and :meth:`encrypt`. -## -## :meth:`encrypt` is where validation of inputs *must* be performed. -## -## .. note:: -## If multiple encoding formats are possible, this *must* normalize -## the checksum kwd to it's canonical format, so the default -## verify() method can work properly. -## """ +## def genhash(cls, secret, config, **context): +## """encrypt secret to hash""" ## -## @abstractclassmethod -## def render(cls, checksum=None, **settings): -## """render hash from checksum & settings (as returned by :meth:`parse`). -## -## :param checksum: -## Encoded checksum portion of hash. -## -## :param settings: -## All other keywords are algorithm-specified, -## and should be listed in :attr:`setting_kwds`. -## -## :raises ValueError: -## If any values are not encodeable into hash. -## -## :raises NotImplementedError: -## If checksum is omitted and the algorithm -## doesn't have any settings (:attr:`setting_kwds` is empty), -## or doesn't support generating "salt strings" -## which contain all configuration except for the -## checksum itself. -## -## :returns: -## if checksum is specified, this should return a fully-formed hash. -## otherwise, it should return a config string containing -## the specified inputs. -## -## .. note:: -## Specific implementations may perform anywhere from none to full -## validation of inputs; the primary goal of this method -## is to render the settings into a single string -## which will be recognized by :meth:`parse`. -## -## :meth:`encrypt` is where validation of inputs *must* be performed. -## """ -## -## #========================================================= -## #genhash helper functions -## #========================================================= -## -## #NOTE: genhash() must be implemented, -## # but helper functions are provided below for common workflows... -## -## #---------------------------------------------------------------- -## #for handlers which normalize config string and hand off to external library -## #---------------------------------------------------------------- ## @classmethod -## def _norm_config(cls, config): -## """normalize & validate config string""" -## assert cls.setting_kwds, "_norm_config not designed for hashses w/o settings" -## if not config: -## raise ValueError, "no %s hash or config string specified" % (cls.name,) -## settings = cls.parse(config) #this should catch malformed entries -## settings.pop("checksum", None) #remove checksum if a hash was passed in -## return cls.genconfig(**settings) #re-generate config string, let genconfig() catch invalid values -## -## #---------------------------------------------------------------- -## #for handlers which implement the guts of the process directly -## #---------------------------------------------------------------- -## -## # render() is also usually used for implementing genhash() in this case -## -## @classmethod -## def _parse_norm_config(cls, config): -## """normalize & validate config string, return parsed dictionary""" -## return cls.parse(cls._norm_config(config)) -## -## #========================================================= -## #genconfig helpers -## #========================================================= -## -## #NOTE: genconfig() must still be implemented, -## # but helper functions provided below -## -## #render() is usually used for implementing genconfig() -## -## @classmethod -## def _norm_rounds(cls, rounds): -## return norm_rounds(rounds, cls.default_rounds, cls.min_rounds, cls.max_rounds, name=cls.name) -## -## @classmethod -## def _norm_salt(cls, salt): -## return norm_salt(salt, cls.min_salt_chars, cls.salt_chars, cls.salt_charset, name=cls.name) +## 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 ## ## #========================================================= -## #identify helpers +## #secondary interface - more useful interface for user, +## # frequently implemented more efficiently by specific handlers ## #========================================================= ## -## #NOTE: this default identify implementation is usually sufficient -## # (and better than CryptHandler.identify), -## # though implementations may override it with an even faster check, -## # such as just looking for a specific string prefix & size -## ## @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.parse(hash) +## cls.genhash("stub", hash) ## except ValueError: ## return False ## return True ## -## #========================================================= -## #encrypt helper functions -## #========================================================= -## -## #NOTE: the default encrypt() method very rarely needs overidding at all. -## -## #========================================================= -## #verify helper functions -## #========================================================= -## -## #NOTE: the default verify method provided here works for most cases, -## # though some handlers will want to implement norm_hash() if their -## # hash has multiple equivalent representations (eg: case insensitive) +## @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_kwds): -## info = cls.parse(hash) #<- should throw ValueError for us if hash is invalid -## if not info.get('checksum'): -## raise ValueError, "hash lacks checksum (did you pass a config string into verify?)" -## other_hash = cls.genhash(secret, hash, **context_kwds) -## other_info = cls.parse(other_hash) -## return info['checksum'] == other_info['checksum'] +## 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 ## #========================================================= #========================================================= -# +# BaseHandler +# 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 BaseSRHandler(object): - """helper class for implementing hash schemes which have both salt and rounds""" +class BaseHandler(object): + """helper class for implementing hash schemes + + hash implementations should fill out the following: + * 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 + * classmethod from_string() + * instancemethod to_string() + * instancemethod calc_checksum() + + many implementations will want to override the following: + * classmethod identify() can usually be done more efficiently + * checksum_charset, checksum_chars attributes may prove helpful for validation + + most implementations can use defaults for the following: + * genconfig(), genhash(), encrypt(), verify(), etc + * norm_checksum() usually only needs overriding if checksum has multiple encodings + + 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) + """ #========================================================= - #password hash api - required attributes + #class attributes #========================================================= - name = None #required - setting_kwds = ("salt", "rounds") + + #---------------------------------------------- + #password hash api - required attributes + #---------------------------------------------- + name = None #required by BaseHandler + setting_kwds = None #required by BaseHandler context_kwds = () - #========================================================= - #password hash api - optional salt attributes - #========================================================= - max_salt_chars = None #required + #---------------------------------------------- + #checksum information + #---------------------------------------------- + checksum_charset = None #if specified, norm_checksum() will validate this + checksum_chars = None #if specified, norm_checksum will require this length + + #---------------------------------------------- + #salt information + #---------------------------------------------- + max_salt_chars = None #required by BaseHandler.norm_salt() @classproperty def min_salt_chars(cls): @@ -362,97 +180,131 @@ class BaseSRHandler(object): "default salt chars (defaults to max_salt_chars if not specified by subclass)" return cls.max_salt_chars - salt_chars = h64.CHARS + salt_charset = h64.CHARS @classproperty def default_salt_charset(cls): - return cls.salt_chars + return cls.salt_charset - #========================================================= - #password hash api - optional rounds attributes - #========================================================= + #---------------------------------------------- + #rounds information + #---------------------------------------------- min_rounds = 0 - max_rounds = None #required - default_rounds = None #required + max_rounds = None #required by BaseHandler.norm_rounds() + default_rounds = None #if not specified, BaseHandler.norm_rounds() will require explicit rounds value every time rounds_cost = "linear" #common case - strict_rounds_bounds = False #if true, always raises error if specified rounds values out of range - required by spec for some hashes + + #---------------------------------------------- + #misc BaseHandler 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>() #========================================================= - #other class attrs + #instance attributes #========================================================= - #TODO: could implement norm_checksum() - ##checksum_size = None #required - checksum size - ##checksum_charset = None #required - checksum charset + checksum = None + salt = None + rounds = None #========================================================= - #handler helpers + #init #========================================================= + #XXX: rename strict kwd to _strict ? + 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) + self.rounds = self.norm_rounds(rounds, strict=strict) + extra = self._extra_init_settings + if extra: + for key in extra: + value = kwds.pop(key, None) + norm = getattr(self, "norm_" + key) + value = norm(value, strict=strict) + setattr(self, key, value) + super(BaseHandler, self).__init__(**kwds) + @classmethod def validate_class(cls): "helper to ensure class is configured property" if not cls.name: - raise ValueError, "no name specified" + raise AssertionError, "class must have .name attribute set" - if 'rounds' not in cls.setting_kwds: - raise ValueError, "rounds not in setting_kwds" + if cls.setting_kwds is None: + raise AssertionError, "class must have .setting_kwds attribute set" - if cls.max_rounds is None: - raise ValueError, "max rounds not specified" + 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" - if cls.min_rounds > cls.max_rounds: - raise ValueError, "min rounds too large" + if 'salt' in cls.setting_kwds: - if cls.default_rounds is None: - raise ValueError, "default rounds not specified" - if cls.default_rounds < cls.min_rounds: - raise ValueError, "default rounds too small" - if cls.default_rounds > cls.max_rounds: - raise ValueError, "default rounds too large" + if cls.min_salt_chars > cls.max_salt_chars: + raise AssertionError, "min salt chars too large" - if cls.rounds_cost not in ("linear", "log2"): - raise ValueError, "unknown rounds cost function" + if cls.default_salt_chars < cls.min_salt_chars: + raise AssertionError, "default salt chars too small" + if cls.default_salt_chars > cls.max_salt_chars: + raise AssertionError, "default salt chars too large" - if 'salt' not in cls.setting_kwds: - raise ValueError, "salt not in setting_kwds" + if any(c not in cls.salt_charset for c in cls.default_salt_charset): + raise AssertionError, "default salt charset not subset of salt charset" - if cls.min_salt_chars > cls.max_salt_chars: - raise ValueError, "min salt chars too large" + if 'rounds' in cls.setting_kwds: - if cls.default_salt_chars < cls.min_salt_chars: - raise ValueError, "default salt chars too small" - if cls.default_salt_chars > cls.max_salt_chars: - raise ValueError, "default salt chars too large" + if cls.max_rounds is None: + raise AssertionError, "max rounds not specified" - if any(c not in cls.salt_charset for c in cls.default_salt_charset): - raise ValueError, "default salt charset not subset of salt charset" + if cls.min_rounds > cls.max_rounds: + raise AssertionError, "min rounds too large" - #========================================================= - #instance attributes - #========================================================= - checksum = None - salt = None - rounds = None + if cls.default_rounds is not None: + if cls.default_rounds < cls.min_rounds: + raise AssertionError, "default rounds too small" + if cls.default_rounds > cls.max_rounds: + raise AssertionError, "default rounds too large" - #========================================================= - #init - #========================================================= - def __init__(self, checksum=None, salt=None, rounds=None, strict=False): - self.checksum = checksum - self.salt = self.norm_salt(salt, strict=strict) - self.rounds = self.norm_rounds(rounds, strict=strict) + if cls.rounds_cost not in ("linear", "log2"): + raise AssertionError, "unknown rounds cost function" #========================================================= #helpers #========================================================= @classmethod + def norm_checksum(cls, checksum, strict=False): + if checksum is None: + return None + cc = cls.checksum_chars + if cc and len(checksum) != cc: + raise ValueError, "%s checksum must be %d characters" % (cls.name, cc) + cs = cls.checksum_charset + if cs and any(c not in cs for c in checksum): + 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" + if not cls._has_salt: + if salt is not None: + raise ValueError, "%s does not support ``salt``" % (cls.name,) + return None + if salt is None: if strict: raise ValueError, "no salt specified" return getrandstr(rng, cls.default_salt_charset, cls.default_salt_chars) + #TODO: run salt_charset tests + mn = cls.min_salt_chars if mn and len(salt) < mn: raise ValueError, "%s salt string must be >= %d characters" % (cls.name, mn) @@ -465,15 +317,31 @@ class BaseSRHandler(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" + if not cls._has_rounds: + if rounds is not None: + raise ValueError, "%s does not support ``rounds``" % (cls.name,) + return None + if rounds is None: if strict: raise ValueError, "no rounds specified" - return cls.default_rounds + rounds = cls.default_rounds + if rounds is None: + raise ValueError, "%s requires an explicitly-specified rounds value" % (cls.name,) + return rounds - if cls.strict_rounds_bounds: + if cls._strict_rounds_bounds: strict = True mn = cls.min_rounds @@ -499,14 +367,13 @@ class BaseSRHandler(object): @classmethod def genhash(cls, secret, config): - if cls.calc_checksum: - self = cls.from_string(config) - self.checksum = self.calc_checksum(secret) - return self.to_string() - else: - raise NotImplementedError, "%s subclass must implemented genhash OR calc_checksum" % (cls.name,) + self = cls.from_string(config) + self.checksum = self.calc_checksum(secret) + return self.to_string() - calc_checksum = None #subclasses may alternately implement this instead of genhash + def calc_checksum(self, secret): + "given secret; calcuate and return encoded checksum portion of hash string, taking config from object state" + raise NotImplementedError, "%s must implement calc_checksum()" % (cls,) #========================================================= #password hash api - secondary interface (default implementation) @@ -526,42 +393,190 @@ class BaseSRHandler(object): @classmethod def encrypt(cls, secret, **settings): self = cls(**settings) - if self.calc_checksum: - #save ourselves some parsing calls if subclass provides this method - self.checksum = self.calc_checksum(secret) - return self.to_string() - else: - return cls.genhash(secret, self.to_string()) + self.checksum = self.calc_checksum(secret) + return self.to_string() @classmethod def verify(cls, secret, hash): - #NOTE: classes may wish to override this - if cls.calc_checksum: - #save ourselves some parsing calls if subclass provides this method - self = cls.from_string(hash) - return self.checksum == self.calc_checksum(secret) - else: - return hash == cls.genhash(secret, hash) + #NOTE: classes with multiple checksum encodings (rare) + # may wish to either override this, or override norm_checksum + # to normalize any checksums provided by from_string() + self = cls.from_string(hash) + return self.checksum == self.calc_checksum(secret) #========================================================= #password hash api - parsing interface #========================================================= - @abstractclassmethod + @classmethod def from_string(cls, hash): "return parsed instance from hash/configuration string; raising ValueError on invalid inputs" - #MUST BE IMPLEMENTED BY SUBCLASS - pass + raise NotImplementedError, "%s must implement from_string()" % (cls,) - @abstractmethod def to_string(self): "render instance to hash or configuration string (depending on if checksum attr is set)" - #MUST BE IMPLEMENTED BY SUBCLASS - pass + 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() #========================================================= # #========================================================= #========================================================= +#plain - mysql_323, mysql_41, nthash, postgres_md5 +#========================================================= +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 = () + + #========================================================= + #helpers for norm checksum + #========================================================= + checksum_charset = None #if specified, norm_checksum() will validate this + checksum_chars = None #if specified, norm_checksum will require this length + + #========================================================= + #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 + + #========================================================= + #primary interface + #========================================================= + @classmethod + def genconfig(cls): + return None + + @classmethod + def genhash(cls, secret, config, **context): + #NOTE: config is ignored + self = cls() + self.checksum = self.calc_checksum(secret, **context) + 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 = () + + #===================================================== + #required methods + #===================================================== + @classmethod + def genconfig(cls, **settings): + raise NotImplementedError, "%s subclass must implement genconfig()" % (cls,) + + @classmethod + def genhash(cls, secret, config): + raise NotImplementedError, "%s subclass must implement genhash()" % (cls,) + + #===================================================== + #default methods (usually subclassed) + #===================================================== + @classmethod + def identify(cls, hash): + #NOTE: relying on genhash throwing error for invalid, + # but takes a long time for non-invalid. + # subclasses *really* should override this. + try: + cls.genhash('stub', hash) + return True + except ValueError: + return False + + #===================================================== + #default methods (rarely subclassed) + #===================================================== + @classmethod + def encrypt(cls, secret, **settings): + config = cls.genconfig(**settings) + return cls.genhash(secret, config) + + @classmethod + def verify(cls, secret, hash): + return hash == cls.genhash(secret, hash) + + #===================================================== + #eoc + #===================================================== + +#========================================================= # eof #========================================================= |
