diff options
| author | Eli Collins <elic@assurancetechnologies.com> | 2012-02-08 22:38:25 -0500 |
|---|---|---|
| committer | Eli Collins <elic@assurancetechnologies.com> | 2012-02-08 22:38:25 -0500 |
| commit | 86a2dc3ed68fcdf7853f5e219541a19b5fcacfff (patch) | |
| tree | 8319e8704f04a23faab1eedfad383d50ff6d3671 | |
| parent | 4c4615329b64287dabd729e3078ab03cb2bb7442 (diff) | |
| download | passlib-86a2dc3ed68fcdf7853f5e219541a19b5fcacfff.tar.gz | |
large refactor of GenericHandler internals
strict keyword
--------------
* GenericHandler's "strict" keyword had poorly defined semantics;
replaced this with "use_defaults" and "relaxed" keywords.
Most handlers' from_string() method specified strict=True.
This is now the default behavior, use_defaults=True is enabled
only for encrypt() and genconfig(). relaxed=True is enabled
only for specific handlers (and unittests) whose code requires it.
This *does* break backward compat with passlib 1.5 handlers,
but this is mostly and internal class.
* missing required settings now throws a TypeError instead of
a ValueError, to be more in line with std python behavior.
* The norm_xxx functions provided by the GenericHandler mixins
(e.g. norm_salt) have been renamed to _norm_xxx() to reflect their
private nature; and converted from class methods to instance
methods, to simplify their call signature for subclassing.
misc
----
* rewrote GenericHandler unittests to use constructor only,
instead of poking into norm_salt/norm_rounds internals.
* checksum/salt charset checks speed up using set comparison
* some small cleanups to FHSP implementation
| -rw-r--r-- | admin/benchmarks.py | 3 | ||||
| -rw-r--r-- | passlib/handlers/bcrypt.py | 29 | ||||
| -rw-r--r-- | passlib/handlers/des_crypt.py | 12 | ||||
| -rw-r--r-- | passlib/handlers/django.py | 2 | ||||
| -rw-r--r-- | passlib/handlers/fshp.py | 46 | ||||
| -rw-r--r-- | passlib/handlers/ldap_digests.py | 2 | ||||
| -rw-r--r-- | passlib/handlers/md5_crypt.py | 2 | ||||
| -rw-r--r-- | passlib/handlers/nthash.py | 2 | ||||
| -rw-r--r-- | passlib/handlers/oracle.py | 2 | ||||
| -rw-r--r-- | passlib/handlers/pbkdf2.py | 6 | ||||
| -rw-r--r-- | passlib/handlers/phpass.py | 2 | ||||
| -rw-r--r-- | passlib/handlers/scram.py | 18 | ||||
| -rw-r--r-- | passlib/handlers/sha1_crypt.py | 1 | ||||
| -rw-r--r-- | passlib/handlers/sha2_crypt.py | 10 | ||||
| -rw-r--r-- | passlib/handlers/sun_md5_crypt.py | 2 | ||||
| -rw-r--r-- | passlib/tests/test_context.py | 3 | ||||
| -rw-r--r-- | passlib/tests/test_handlers.py | 45 | ||||
| -rw-r--r-- | passlib/tests/test_utils_handlers.py | 267 | ||||
| -rw-r--r-- | passlib/tests/utils.py | 14 | ||||
| -rw-r--r-- | passlib/utils/handlers.py | 358 |
20 files changed, 467 insertions, 359 deletions
diff --git a/admin/benchmarks.py b/admin/benchmarks.py index bc88cf0..0976569 100644 --- a/admin/benchmarks.py +++ b/admin/benchmarks.py @@ -49,8 +49,7 @@ class BlankHandler(uh.HasRounds, uh.HasSalt, uh.GenericHandler): @classmethod def from_string(cls, hash): r,s,c = uh.parse_mc3(hash, cls.ident, cls.name) - r = int(r) - return cls(rounds=r, salt=s, checksum=c, strict=bool(c)) + return cls(rounds=int(r), salt=s, checksum=c) def to_string(self): return uh.render_mc3(self.ident, self.rounds, self.salt, self.checksum) diff --git a/passlib/handlers/bcrypt.py b/passlib/handlers/bcrypt.py index 7a698d4..440e39c 100644 --- a/passlib/handlers/bcrypt.py +++ b/passlib/handlers/bcrypt.py @@ -121,7 +121,7 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh. #========================================================= @classmethod - def from_string(cls, hash, strict=True): + def from_string(cls, hash): if not hash: raise ValueError("no hash specified") if isinstance(hash, bytes): @@ -133,15 +133,14 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh. raise ValueError("invalid bcrypt hash") rounds, data = hash[len(ident):].split(u("$")) rval = int(rounds) - if strict and rounds != u('%02d') % (rval,): - raise ValueError("invalid bcrypt hash (no rounds padding)") + if rounds != u('%02d') % (rval,): + raise ValueError("invalid bcrypt hash (rounds not zero-padded)") salt, chk = data[:22], data[22:] return cls( rounds=rval, salt=salt, checksum=chk or None, ident=ident, - strict=strict and bool(chk), ) def to_string(self): @@ -169,20 +168,19 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh. def normhash(cls, hash): "helper to normalize hash, correcting any bcrypt padding bits" if cls.identify(hash): - return cls.from_string(hash, strict=False).to_string() + return cls.from_string(hash).to_string() else: return hash - @classmethod - def generate_salt(cls, salt_size=None, strict=False): - assert cls.min_salt_size == cls.max_salt_size == cls.default_salt_size == 22 - if salt_size is not None and salt_size != 22: - raise ValueError("bcrypt salts must be 22 characters in length") + # TODO: extract some of this code out into Base64Engine.repair_padding() + # or some such method, so we can re-use it other places. + + def _generate_salt(self, salt_size): + assert salt_size == 22 return getrandstr(rng, BCHARS, 21) + getrandstr(rng, BSLAST, 1) - @classmethod - def norm_salt(cls, *args, **kwds): - salt = super(bcrypt, cls).norm_salt(*args, **kwds) + def _norm_salt(self, salt, **kwds): + salt = super(bcrypt, self)._norm_salt(salt, **kwds) if salt and salt[-1] not in BSLAST: salt = salt[:-1] + BCHARS[BCHARS.index(salt[-1]) & ~15] assert salt[-1] in BSLAST @@ -193,9 +191,8 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh. PasslibHandlerWarning) return salt - @classmethod - def norm_checksum(cls, *args, **kwds): - checksum = super(bcrypt, cls).norm_checksum(*args, **kwds) + def _norm_checksum(self, checksum): + checksum = super(bcrypt, self)._norm_checksum(checksum) if checksum and checksum[-1] not in BHLAST: checksum = checksum[:-1] + BCHARS[BCHARS.index(checksum[-1]) & ~3] assert checksum[-1] in BHLAST diff --git a/passlib/handlers/des_crypt.py b/passlib/handlers/des_crypt.py index b326e8e..a5582f6 100644 --- a/passlib/handlers/des_crypt.py +++ b/passlib/handlers/des_crypt.py @@ -191,7 +191,7 @@ class des_crypt(uh.HasManyBackends, uh.HasSalt, uh.GenericHandler): if isinstance(hash, bytes): hash = hash.decode("ascii") salt, chk = hash[:2], hash[2:] - return cls(salt=salt, checksum=chk or None, strict=bool(chk)) + return cls(salt=salt, checksum=chk or None) def to_string(self): hash = u("%s%s") % (self.salt, self.checksum or u('')) @@ -313,7 +313,6 @@ class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler rounds=h64.decode_int24(rounds.encode("ascii")), salt=salt, checksum=chk, - strict=bool(chk), ) def to_string(self): @@ -406,15 +405,14 @@ class bigcrypt(uh.HasSalt, uh.GenericHandler): if not m: raise ValueError("invalid bigcrypt hash") salt, chk = m.group("salt", "chk") - return cls(salt=salt, checksum=chk, strict=bool(chk)) + return cls(salt=salt, checksum=chk) def to_string(self): hash = u("%s%s") % (self.salt, self.checksum or u('')) return uascii_to_str(hash) - @classmethod - def norm_checksum(cls, value, strict=False): - value = super(bigcrypt, cls).norm_checksum(value, strict=strict) + def _norm_checksum(self, value): + value = super(bigcrypt, self)._norm_checksum(value) if value and len(value) % 11: raise ValueError("invalid bigcrypt hash") return value @@ -491,7 +489,7 @@ class crypt16(uh.HasSalt, uh.GenericHandler): if not m: raise ValueError("invalid crypt16 hash") salt, chk = m.group("salt", "chk") - return cls(salt=salt, checksum=chk, strict=bool(chk)) + return cls(salt=salt, checksum=chk) def to_string(self): hash = u("%s%s") % (self.salt, self.checksum or u('')) diff --git a/passlib/handlers/django.py b/passlib/handlers/django.py index 26754a3..8ead857 100644 --- a/passlib/handlers/django.py +++ b/passlib/handlers/django.py @@ -62,7 +62,7 @@ class DjangoSaltedHash(uh.HasStubChecksum, uh.HasSalt, uh.GenericHandler): if not hash.startswith(ident): raise ValueError("invalid %s hash" % (cls.name,)) _, salt, chk = hash.split(u("$")) - return cls(salt=salt, checksum=chk or None, strict=True) + return cls(salt=salt, checksum=chk or None) def to_string(self): chk = self.checksum or self._stub_checksum diff --git a/passlib/handlers/fshp.py b/passlib/handlers/fshp.py index 424bf09..adf853d 100644 --- a/passlib/handlers/fshp.py +++ b/passlib/handlers/fshp.py @@ -58,6 +58,7 @@ class fshp(uh.HasStubChecksum, uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, u name = "fshp" setting_kwds = ("salt", "salt_size", "rounds", "variant") checksum_chars = uh.PADDED_BASE64_CHARS + # checksum_size is property() that depends on variant #--HasRawSalt-- default_salt_size = 16 #current passlib default, FSHP uses 8 @@ -73,7 +74,7 @@ class fshp(uh.HasStubChecksum, uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, u #--variants-- default_variant = 1 _variant_info = { - #variant: (hash, digest size) + #variant: (hash name, digest size) 0: ("sha1", 20), 1: ("sha256", 32), 2: ("sha384", 48), @@ -92,38 +93,37 @@ class fshp(uh.HasStubChecksum, uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, u #========================================================= #init #========================================================= - def __init__(self, variant=None, strict=False, **kwds): - self.variant = self.norm_variant(variant, strict=strict) - super(fshp, self).__init__(strict=strict, **kwds) + def __init__(self, variant=None, **kwds): + # NOTE: variant must be set first, since it controls checksum size, etc. + self.use_defaults = kwds.get("use_defaults") # load this early + self.variant = self._norm_variant(variant) + super(fshp, self).__init__(**kwds) - @classmethod - def norm_variant(cls, variant, strict=False): + def _norm_variant(self, variant): if variant is None: - if strict: - raise ValueError("no variant specified") - variant = cls.default_variant + if not self.use_defaults: + raise TypeError("no variant specified") + variant = self.default_variant if isinstance(variant, bytes): variant = variant.decode("ascii") if isinstance(variant, unicode): try: - variant = cls._variant_aliases[variant] + variant = self._variant_aliases[variant] except KeyError: raise ValueError("invalid fshp variant") if not isinstance(variant, int): raise TypeError("fshp variant must be int or known alias") - if variant not in cls._variant_info: + if variant not in self._variant_info: raise TypeError("unknown fshp variant") return variant - def norm_checksum(self, checksum, strict=False): - checksum = super(fshp, self).norm_checksum(checksum, strict) - if checksum is not None and len(checksum) != self._variant_info[self.variant][1]: - raise ValueError("invalid checksum length for FSHP variant") - return checksum + @property + def checksum_alg(self): + return self._variant_info[self.variant][0] @property - def _info(self): - return self._variant_info[self.variant] + def checksum_size(self): + return self._variant_info[self.variant][1] #========================================================= #formatting @@ -154,12 +154,11 @@ class fshp(uh.HasStubChecksum, uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, u raise ValueError("malformed FSHP hash") salt = data[:salt_size] chk = data[salt_size:] - return cls(checksum=chk, salt=salt, rounds=rounds, - variant=variant, strict=True) + return cls(salt=salt, checksum=chk, rounds=rounds, variant=variant) @property def _stub_checksum(self): - return b('\x00') * self._info[1] + return b('\x00') * self.checksum_size def to_string(self): chk = self.checksum or self._stub_checksum @@ -172,7 +171,6 @@ class fshp(uh.HasStubChecksum, uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, u #========================================================= def calc_checksum(self, secret): - hash, klen = self._info if isinstance(secret, unicode): secret = secret.encode("utf-8") #NOTE: for some reason, FSHP uses pbkdf1 with password & salt reversed. @@ -182,8 +180,8 @@ class fshp(uh.HasStubChecksum, uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, u secret=self.salt, salt=secret, rounds=self.rounds, - keylen=klen, - hash=hash, + keylen=self.checksum_size, + hash=self.checksum_alg, ) #========================================================= diff --git a/passlib/handlers/ldap_digests.py b/passlib/handlers/ldap_digests.py index c25afd5..176ec35 100644 --- a/passlib/handlers/ldap_digests.py +++ b/passlib/handlers/ldap_digests.py @@ -89,7 +89,7 @@ class _SaltedBase64DigestHelper(uh.HasStubChecksum, uh.HasRawSalt, uh.HasRawChec raise ValueError("not a %s hash" % (cls.name,)) data = b64decode(m.group("tmp").encode("ascii")) chk, salt = data[:-4], data[-4:] - return cls(checksum=chk, salt=salt, strict=True) + return cls(checksum=chk, salt=salt) def to_string(self): data = (self.checksum or self._stub_checksum) + self.salt diff --git a/passlib/handlers/md5_crypt.py b/passlib/handlers/md5_crypt.py index e24e66e..59ba814 100644 --- a/passlib/handlers/md5_crypt.py +++ b/passlib/handlers/md5_crypt.py @@ -173,7 +173,7 @@ class _Md5Common(uh.HasSalt, uh.GenericHandler): @classmethod def from_string(cls, hash): salt, chk = uh.parse_mc2(hash, cls.ident, cls.name) - return cls(salt=salt, checksum=chk, strict=bool(chk)) + return cls(salt=salt, checksum=chk) def to_string(self): return uh.render_mc2(self.ident, self.salt, self.checksum) diff --git a/passlib/handlers/nthash.py b/passlib/handlers/nthash.py index df0021c..b3cd947 100644 --- a/passlib/handlers/nthash.py +++ b/passlib/handlers/nthash.py @@ -66,7 +66,7 @@ class nthash(uh.HasStubChecksum, uh.HasManyIdents, uh.GenericHandler): else: raise ValueError("invalid nthash") chk = hash[len(ident):] - return cls(ident=ident, checksum=chk, strict=True) + return cls(ident=ident, checksum=chk) def to_string(self): hash = self.ident + (self.checksum or self._stub_checksum) diff --git a/passlib/handlers/oracle.py b/passlib/handlers/oracle.py index 73b74ed..68dc138 100644 --- a/passlib/handlers/oracle.py +++ b/passlib/handlers/oracle.py @@ -175,7 +175,7 @@ class oracle11(uh.HasStubChecksum, uh.HasSalt, uh.GenericHandler): if not m: raise ValueError("invalid oracle-11g hash") salt, chk = m.group("salt", "chk") - return cls(salt=salt, checksum=chk.upper(), strict=True) + return cls(salt=salt, checksum=chk.upper()) def to_string(self): chk = (self.checksum or self._stub_checksum) diff --git a/passlib/handlers/pbkdf2.py b/passlib/handlers/pbkdf2.py index 8621f03..96cd47a 100644 --- a/passlib/handlers/pbkdf2.py +++ b/passlib/handlers/pbkdf2.py @@ -77,7 +77,6 @@ class Pbkdf2DigestHandler(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.Gen rounds=int_rounds, salt=raw_salt, checksum=raw_chk, - strict=bool(raw_chk), ) def to_string(self, withchk=True): @@ -217,7 +216,6 @@ class cta_pbkdf2_sha1(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.Generic rounds=rounds, salt=salt, checksum=chk, - strict=bool(chk), ) def to_string(self, withchk=True): @@ -310,7 +308,6 @@ class dlitz_pbkdf2_sha1(uh.HasRounds, uh.HasSalt, uh.GenericHandler): rounds=rounds, salt=salt, checksum=chk, - strict=bool(chk), ) def to_string(self, withchk=True): @@ -373,7 +370,7 @@ class atlassian_pbkdf2_sha1(uh.HasStubChecksum, uh.HasRawSalt, uh.HasRawChecksum raise ValueError("invalid %s hash" % (cls.name,)) data = b64decode(hash[len(ident):].encode("ascii")) salt, chk = data[:16], data[16:] - return cls(salt=salt, checksum=chk, strict=True) + return cls(salt=salt, checksum=chk) def to_string(self): data = self.salt + (self.checksum or self._stub_checksum) @@ -442,7 +439,6 @@ class grub_pbkdf2_sha512(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.Gene rounds=int_rounds, salt=raw_salt, checksum=raw_chk, - strict=bool(raw_chk), ) def to_string(self, withchk=True): diff --git a/passlib/handlers/phpass.py b/passlib/handlers/phpass.py index bd72ef8..19486a9 100644 --- a/passlib/handlers/phpass.py +++ b/passlib/handlers/phpass.py @@ -68,7 +68,6 @@ class phpass(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.GenericHandler): min_rounds = 7 max_rounds = 30 rounds_cost = "log2" - _strict_rounds_bounds = True #--HasManyIdents-- default_ident = u("$P$") @@ -103,7 +102,6 @@ class phpass(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.GenericHandler): rounds=h64.decode_int6(rounds.encode("ascii")), salt=salt, checksum=chk or None, - strict=bool(chk), ) def to_string(self): diff --git a/passlib/handlers/scram.py b/passlib/handlers/scram.py index af3481f..7024dd6 100644 --- a/passlib/handlers/scram.py +++ b/passlib/handlers/scram.py @@ -364,7 +364,6 @@ class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): salt=salt, checksum=chkmap, algs=algs, - strict=chkmap is not None, ) def to_string(self, withchk=True): @@ -384,10 +383,9 @@ class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): #========================================================= def __init__(self, algs=None, **kwds): super(scram, self).__init__(**kwds) - self.algs = self.norm_algs(algs) + self.algs = self._norm_algs(algs) - @classmethod - def norm_checksum(cls, checksum, strict=False): + def _norm_checksum(self, checksum): if checksum is None: return None for alg, digest in iteritems(checksum): @@ -404,17 +402,21 @@ class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): raise ValueError("sha-1 must be in algorithm list of scram hash") return checksum - def norm_algs(self, algs): + def _norm_algs(self, algs): "normalize algs parameter" # determine default algs value if algs is None: + # derive algs list from checksum (if present). chk = self.checksum - if chk is None: + if chk is not None: + return sorted(chk) + elif self.use_defaults: return list(self.default_algs) else: - return sorted(chk) + raise TypeError("no algs list specified") elif self.checksum is not None: raise RuntimeError("checksum & algs kwds are mutually exclusive") + # parse args value if isinstance(algs, str): algs = algs.split(",") @@ -435,7 +437,7 @@ class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): "generate a deprecation detector for CryptContext to use" # generate deprecation hook which marks hashes as deprecated # if they don't support a superset of current algs. - algs = frozenset(cls(**settings).algs) + algs = frozenset(cls(use_defaults=True, **settings).algs) def detector(hash): return not algs.issubset(cls.from_string(hash).algs) return detector diff --git a/passlib/handlers/sha1_crypt.py b/passlib/handlers/sha1_crypt.py index 2a12345..db61d12 100644 --- a/passlib/handlers/sha1_crypt.py +++ b/passlib/handlers/sha1_crypt.py @@ -87,7 +87,6 @@ class sha1_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler rounds=int(rounds), salt=salt, checksum=chk, - strict=bool(chk), ) def to_string(self): diff --git a/passlib/handlers/sha2_crypt.py b/passlib/handlers/sha2_crypt.py index 58f32a6..c1d782c 100644 --- a/passlib/handlers/sha2_crypt.py +++ b/passlib/handlers/sha2_crypt.py @@ -307,11 +307,13 @@ class sha256_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl if rounds and rounds.startswith(u("0")): raise ValueError("invalid sha256-crypt hash (zero-padded rounds)") return cls( - implicit_rounds = not rounds, + implicit_rounds=not rounds, rounds=int(rounds) if rounds else 5000, salt=salt1 or salt2, checksum=chk, - strict=bool(chk), + relaxed=not chk, # NOTE: relaxing parsing for config strings, + # since SHA2-Crypt specification treats them this + # way (at least for the rounds value) ) def to_string(self): @@ -463,7 +465,9 @@ class sha512_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl rounds=int(rounds) if rounds else 5000, salt=salt1 or salt2, checksum=chk, - strict=bool(chk), + relaxed=not chk, # NOTE: relaxing parsing for config strings, + # since SHA2-Crypt specification treats them this + # way (at least for the rounds value) ) def to_string(self): diff --git a/passlib/handlers/sun_md5_crypt.py b/passlib/handlers/sun_md5_crypt.py index 7bc5e4d..31ba01e 100644 --- a/passlib/handlers/sun_md5_crypt.py +++ b/passlib/handlers/sun_md5_crypt.py @@ -214,7 +214,6 @@ class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler): 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" - _strict_rounds_bounds = True #========================================================= #instance attrs @@ -304,7 +303,6 @@ class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler): salt=salt, checksum=chk, bare_salt=bare_salt, - strict=bool(chk), ) def to_string(self, withchk=True): diff --git a/passlib/tests/test_context.py b/passlib/tests/test_context.py index 72ee39b..2ee966a 100644 --- a/passlib/tests/test_context.py +++ b/passlib/tests/test_context.py @@ -781,11 +781,12 @@ class CryptContextTest(TestCase): c2 = cc.replace(all__vary_rounds="100%") self.assert_rounds_range(c2, "bcrypt", 15, 21) - def assert_rounds_range(self, context, scheme, lower, upper, salt="."*22): + def assert_rounds_range(self, context, scheme, lower, upper): "helper to check vary_rounds covers specified range" # NOTE: this runs enough times the min and max *should* be hit, # though there's a faint chance it will randomly fail. handler = context.policy.get_handler(scheme) + salt = handler.default_salt_chars[0:1] * handler.max_salt_size seen = set() for i in irange(300): h = context.genconfig(scheme, salt=salt) diff --git a/passlib/tests/test_handlers.py b/passlib/tests/test_handlers.py index 9da90d0..98a741f 100644 --- a/passlib/tests/test_handlers.py +++ b/passlib/tests/test_handlers.py @@ -110,16 +110,14 @@ class _BCryptTest(HandlerCase): kwds = dict(checksum='8CIhhFCj15KqqFvo/n.Jatx8dJ92f82', salt='VlsfIX9.apXuQBr6tego0.', - rounds=12, ident="2a", strict=True) + rounds=12, ident="2a") handler(**kwds) - kwds['ident'] = None - self.assertRaises(ValueError, handler, **kwds) - - del kwds['strict'] + kwds.update(ident=None) + self.assertRaises(TypeError, handler, **kwds) - kwds['ident'] = 'Q' + kwds.update(use_defaults=True, ident='Q') self.assertRaises(ValueError, handler, **kwds) #=============================================================== @@ -149,7 +147,7 @@ class _BCryptTest(HandlerCase): self.assertWarningMatches(wlog.pop(0), message_re="^encountered a bcrypt hash with incorrectly set padding bits.*", ) - self.assertFalse(wlog) + self.assertNoWarnings(wlog) def check_padding(hash): "check bcrypt hash doesn't have salt padding bits set" @@ -174,12 +172,14 @@ class _BCryptTest(HandlerCase): with catch_warnings(record=True) as wlog: warnings.simplefilter("always") - hash = bcrypt.genconfig(salt="."*21 + "A.") + hash = bcrypt.genconfig(salt="."*21 + "A.", relaxed=True) + self.assertWarningMatches(wlog.pop(0), message_re="salt too large") check_warning(wlog) self.assertEqual(hash, "$2a$12$" + "." * 22) - hash = bcrypt.genconfig(salt="."*23) - self.assertFalse(wlog) + hash = bcrypt.genconfig(salt="."*23, relaxed=True) + self.assertWarningMatches(wlog.pop(0), message_re="salt too large") + self.assertNoWarnings(wlog) self.assertEqual(hash, "$2a$12$" + "." * 22) #=============================================================== @@ -710,14 +710,13 @@ class NTHashTest(HandlerCase): def test_idents(self): handler = self.handler - kwds = dict(checksum='7f8fe03093cc84b267b109625f6bbf4b', ident="3", strict=True) + kwds = dict(checksum='7f8fe03093cc84b267b109625f6bbf4b', ident="3") handler(**kwds) - kwds['ident'] = None - self.assertRaises(ValueError, handler, **kwds) + kwds.update(ident=None) + self.assertRaises(TypeError, handler, **kwds) - del kwds['strict'] - kwds['ident'] = 'Q' + kwds.update(use_defaults=True, ident='Q') self.assertRaises(ValueError, handler, **kwds) #========================================================= @@ -917,14 +916,14 @@ class PHPassTest(HandlerCase): def test_idents(self): handler = self.handler - kwds = dict(checksum='eRo7ud9Fh4E2PdI0S3r.L0', salt='IQRaTwmf', rounds=9, ident="P", strict=True) + kwds = dict(checksum='eRo7ud9Fh4E2PdI0S3r.L0', salt='IQRaTwmf', + rounds=9, ident="P") handler(**kwds) - kwds['ident'] = None - self.assertRaises(ValueError, handler, **kwds) + kwds.update(ident=None) + self.assertRaises(TypeError, handler, **kwds) - del kwds['strict'] - kwds['ident'] = 'Q' + kwds.update(use_defaults=True, ident='Q') self.assertRaises(ValueError, handler, **kwds) #========================================================= @@ -1067,8 +1066,8 @@ class ScramTest(HandlerCase): def test_100_algs(self): "test parsing of 'algs' setting" - def parse(source): - return self.handler(algs=source).algs + def parse(algs, **kwds): + return self.handler(algs=algs, use_defaults=True, **kwds).algs # None -> default list self.assertEqual(parse(None), ["sha-1","sha-256","sha-512"]) @@ -1087,7 +1086,7 @@ class ScramTest(HandlerCase): self.assertRaises(ValueError, parse, ["sha-1","shaxxx-890"]) # alg & checksum mutually exclusive. - self.assertRaises(RuntimeError, self.handler, algs=['sha-1'], + self.assertRaises(RuntimeError, parse, ['sha-1'], checksum={"sha-1": b("\x00"*20)}) def test_101_extract_digest_info(self): diff --git a/passlib/tests/test_utils_handlers.py b/passlib/tests/test_utils_handlers.py index fe2667a..def6d3e 100644 --- a/passlib/tests/test_utils_handlers.py +++ b/passlib/tests/test_utils_handlers.py @@ -13,10 +13,10 @@ import warnings from passlib.hash import ldap_md5, sha256_crypt from passlib.registry import _unload_handler_name as unload_handler_name, \ register_crypt_handler, get_crypt_handler -from passlib.exc import MissingBackendError +from passlib.exc import MissingBackendError, PasslibHandlerWarning from passlib.utils import getrandstr, JYTHON, rng, to_unicode from passlib.utils.compat import b, bytes, bascii_to_str, str_to_uascii, \ - uascii_to_str, unicode + uascii_to_str, unicode, PY_MAX_25 import passlib.utils.handlers as uh from passlib.tests.utils import HandlerCase, TestCase, catch_warnings, \ dummy_handler_in_registry @@ -25,6 +25,21 @@ from passlib.utils.compat import u log = getLogger(__name__) #========================================================= +# utils +#========================================================= +def _makelang(alphabet, size): + "generate all strings of given size using alphabet" + def helper(size): + if size < 2: + for char in alphabet: + yield char + else: + for char in alphabet: + for tail in helper(size-1): + yield char+tail + return set(helper(size)) + +#========================================================= #test support classes - StaticHandler, GenericHandler, etc #========================================================= class SkeletonTest(TestCase): @@ -95,13 +110,13 @@ class SkeletonTest(TestCase): else: raise ValueError - #check fallback + # check fallback self.assertFalse(d1.identify(None)) self.assertFalse(d1.identify('')) self.assertTrue(d1.identify('a')) self.assertFalse(d1.identify('b')) - #check ident-based + # check ident-based d1.ident = u('!') self.assertFalse(d1.identify(None)) self.assertFalse(d1.identify('')) @@ -109,70 +124,108 @@ class SkeletonTest(TestCase): self.assertFalse(d1.identify('a')) def test_11_norm_checksum(self): - "test GenericHandler.norm_checksum()" + "test GenericHandler checksum handling" + # setup helpers class d1(uh.GenericHandler): name = 'd1' checksum_size = 4 checksum_chars = 'x' - self.assertRaises(ValueError, d1.norm_checksum, 'xxx') - self.assertEqual(d1.norm_checksum('xxxx'), 'xxxx') - self.assertRaises(ValueError, d1.norm_checksum, 'xxxxx') - self.assertRaises(ValueError, d1.norm_checksum, 'xxyx') + + def norm_checksum(*a, **k): + return d1(*a, **k).checksum + + # too small + self.assertRaises(ValueError, norm_checksum, 'xxx') + + # right size + self.assertEqual(norm_checksum('xxxx'), 'xxxx') + + # too large + self.assertRaises(ValueError, norm_checksum, 'xxxxx') + + # wrong chars + self.assertRaises(ValueError, norm_checksum, 'xxyx') def test_20_norm_salt(self): - "test GenericHandler+HasSalt: .norm_salt(), .generate_salt()" - class d1(uh.HasSalt, uh.GenericHandler): - name = 'd1' - setting_kwds = ('salt',) - min_salt_size = 1 - max_salt_size = 3 - default_salt_size = 2 - salt_chars = 'a' - - #check salt=None - self.assertEqual(d1.norm_salt(None), 'aa') - self.assertRaises(ValueError, d1.norm_salt, None, strict=True) - - #check small & large salts - with catch_warnings(): - warnings.filterwarnings("ignore", ".* salt string must be at (least|most) .*", UserWarning) - self.assertEqual(d1.norm_salt('aaaa'), 'aaa') - self.assertRaises(ValueError, d1.norm_salt, '') - self.assertRaises(ValueError, d1.norm_salt, 'aaaa', strict=True) - - #check generate salt (indirectly) - self.assertEqual(len(d1.norm_salt(None)), 2) - self.assertEqual(len(d1.norm_salt(None,salt_size=1)), 1) - self.assertEqual(len(d1.norm_salt(None,salt_size=3)), 3) - self.assertEqual(len(d1.norm_salt(None,salt_size=5)), 3) - self.assertRaises(ValueError, d1.norm_salt, None, salt_size=5, strict=True) - - def test_21_norm_salt(self): - "test GenericHandler+HasSalt: .norm_salt(), .generate_salt() - with no max_salt_size" + "test GenericHandler + HasSalt mixin" + # setup helpers class d1(uh.HasSalt, uh.GenericHandler): name = 'd1' setting_kwds = ('salt',) - min_salt_size = 1 - max_salt_size = None - default_salt_size = 2 - salt_chars = 'a' - - #check salt=None - self.assertEqual(d1.norm_salt(None), 'aa') - self.assertRaises(ValueError, d1.norm_salt, None, strict=True) - - #check small & large salts - self.assertRaises(ValueError, d1.norm_salt, '') - self.assertEqual(d1.norm_salt('aaaa', strict=True), 'aaaa') - - #check generate salt (indirectly) - self.assertEqual(len(d1.norm_salt(None)), 2) - self.assertEqual(len(d1.norm_salt(None,salt_size=1)), 1) - self.assertEqual(len(d1.norm_salt(None,salt_size=3)), 3) - self.assertEqual(len(d1.norm_salt(None,salt_size=5)), 5) + min_salt_size = 2 + max_salt_size = 4 + default_salt_size = 3 + salt_chars = 'ab' + + def norm_salt(**k): + return d1(**k).salt + + def gen_salt(sz, **k): + return d1(use_defaults=True, salt_size=sz, **k).salt + + salts2 = _makelang('ab', 2) + salts3 = _makelang('ab', 3) + salts4 = _makelang('ab', 4) + + # check salt=None + self.assertRaises(TypeError, norm_salt) + self.assertRaises(TypeError, norm_salt, salt=None) + self.assertIn(norm_salt(use_defaults=True), salts3) + + # check explicit salts + with catch_warnings(record=True) as wlog: + + # check too-small salts + self.assertRaises(ValueError, norm_salt, salt='') + self.assertRaises(ValueError, norm_salt, salt='a') + self.assertNoWarnings(wlog) + + # check correct salts + self.assertEqual(norm_salt(salt='ab'), 'ab') + self.assertEqual(norm_salt(salt='aba'), 'aba') + self.assertEqual(norm_salt(salt='abba'), 'abba') + self.assertNoWarnings(wlog) + + # check too-large salts + self.assertRaises(ValueError, norm_salt, salt='aaaabb') + self.assertNoWarnings(wlog) + + self.assertEqual(norm_salt(salt='aaaabb', relaxed=True), 'aaaa') + self.assertWarningMatches(wlog.pop(0), category=PasslibHandlerWarning) + self.assertNoWarnings(wlog) + + #check generated salts + with catch_warnings(record=True) as wlog: + + # check too-small salt size + self.assertRaises(ValueError, gen_salt, 0) + self.assertRaises(ValueError, gen_salt, 1) + self.assertNoWarnings(wlog) + + # check correct salt size + self.assertIn(gen_salt(2), salts2) + self.assertIn(gen_salt(3), salts3) + self.assertIn(gen_salt(4), salts4) + self.assertNoWarnings(wlog) + + # check too-large salt size + self.assertRaises(ValueError, gen_salt, 5) + self.assertNoWarnings(wlog) + + self.assertIn(gen_salt(5, relaxed=True), salts4) + self.assertWarningMatches(wlog.pop(0), category=PasslibHandlerWarning) + self.assertNoWarnings(wlog) + + # test with max_salt_size=None + del d1.max_salt_size + with catch_warnings(record=True) as wlog: + self.assertEqual(len(gen_salt(None)), 3) + self.assertEqual(len(gen_salt(5)), 5) + self.assertNoWarnings(wlog) def test_30_norm_rounds(self): - "test GenericHandler+HasRounds: .norm_rounds()" + "test GenericHandler + HasRounds mixin" + # setup helpers class d1(uh.HasRounds, uh.GenericHandler): name = 'd1' setting_kwds = ('rounds',) @@ -180,24 +233,44 @@ class SkeletonTest(TestCase): max_rounds = 3 default_rounds = 2 - #check rounds=None - self.assertEqual(d1.norm_rounds(None), 2) - self.assertRaises(ValueError, d1.norm_rounds, None, strict=True) + def norm_rounds(**k): + return d1(**k).rounds + + # check rounds=None + self.assertRaises(TypeError, norm_rounds) + self.assertRaises(TypeError, norm_rounds, rounds=None) + self.assertEqual(norm_rounds(use_defaults=True), 2) + + # check explicit rounds + with catch_warnings(record=True) as wlog: + # too small + self.assertRaises(ValueError, norm_rounds, rounds=0) + self.assertNoWarnings(wlog) + + self.assertEqual(norm_rounds(rounds=0, relaxed=True), 1) + self.assertWarningMatches(wlog.pop(0), category=PasslibHandlerWarning) + self.assertNoWarnings(wlog) + + # just right + self.assertEqual(norm_rounds(rounds=1), 1) + self.assertEqual(norm_rounds(rounds=2), 2) + self.assertEqual(norm_rounds(rounds=3), 3) + self.assertNoWarnings(wlog) - #check small & large rounds - with catch_warnings(): - warnings.filterwarnings("ignore", ".* does not allow (less|more) than \d rounds: .*", UserWarning) - self.assertEqual(d1.norm_rounds(0), 1) - self.assertEqual(d1.norm_rounds(4), 3) - self.assertRaises(ValueError, d1.norm_rounds, 0, strict=True) - self.assertRaises(ValueError, d1.norm_rounds, 4, strict=True) + # too large + self.assertRaises(ValueError, norm_rounds, rounds=4) + self.assertNoWarnings(wlog) - #check no default rounds + self.assertEqual(norm_rounds(rounds=4, relaxed=True), 3) + self.assertWarningMatches(wlog.pop(0), category=PasslibHandlerWarning) + self.assertNoWarnings(wlog) + + # check no default rounds d1.default_rounds = None - self.assertRaises(ValueError, d1.norm_rounds, None) + self.assertRaises(TypeError, norm_rounds, use_defaults=True) def test_40_backends(self): - "test GenericHandler+HasManyBackends" + "test GenericHandler + HasManyBackends mixin" class d1(uh.HasManyBackends, uh.GenericHandler): name = 'd1' setting_kwds = () @@ -249,33 +322,36 @@ class SkeletonTest(TestCase): self.assertRaises(ValueError, d1.set_backend, 'c') self.assertRaises(ValueError, d1.has_backend, 'c') - def test_50_bh_norm_ident(self): - "test GenericHandler+HasManyIdents: .norm_ident() & .identify()" + def test_50_norm_ident(self): + "test GenericHandler + HasManyIdents" + # setup helpers class d1(uh.HasManyIdents, uh.GenericHandler): name = 'd1' setting_kwds = ('ident',) + default_ident = u("!A") ident_values = [ u("!A"), u("!B") ] ident_aliases = { u("A"): u("!A")} - #check ident=None w/ no default - self.assertIs(d1.norm_ident(None), None) - self.assertRaises(ValueError, d1.norm_ident, None, strict=True) + def norm_ident(**k): + return d1(**k).ident + + # check ident=None + self.assertRaises(TypeError, norm_ident) + self.assertRaises(TypeError, norm_ident, ident=None) + self.assertEqual(norm_ident(use_defaults=True), u('!A')) - #check ident=None w/ default - d1.default_ident = u("!A") - self.assertEqual(d1.norm_ident(None), u('!A')) - self.assertRaises(ValueError, d1.norm_ident, None, strict=True) + # check valid idents + self.assertEqual(norm_ident(ident=u('!A')), u('!A')) + self.assertEqual(norm_ident(ident=u('!B')), u('!B')) + self.assertRaises(ValueError, norm_ident, ident=u('!C')) - #check explicit - self.assertEqual(d1.norm_ident(u('!A')), u('!A')) - self.assertEqual(d1.norm_ident(u('!B')), u('!B')) - self.assertRaises(ValueError, d1.norm_ident, u('!C')) + # check aliases + self.assertEqual(norm_ident(ident=u('A')), u('!A')) - #check aliases - self.assertEqual(d1.norm_ident(u('A')), u('!A')) - self.assertRaises(ValueError, d1.norm_ident, u('B')) + # check invalid idents + self.assertRaises(ValueError, norm_ident, ident=u('B')) - #check identify + # check identify is honoring ident system self.assertTrue(d1.identify(u("!Axxx"))) self.assertTrue(d1.identify(u("!Bxxx"))) self.assertFalse(d1.identify(u("!Cxxx"))) @@ -283,6 +359,10 @@ class SkeletonTest(TestCase): self.assertFalse(d1.identify(u(""))) self.assertFalse(d1.identify(None)) + # check default_ident missing is detected. + d1.default_ident = None + self.assertRaises(AssertionError, norm_ident, use_defaults=True) + #========================================================= #eoc #========================================================= @@ -344,7 +424,7 @@ class PrefixWrapperTest(TestCase): d2 = uh.PrefixWrapper("d2", "sha256_crypt", "{XXX}") self.assertIs(d2.setting_kwds, sha256_crypt.setting_kwds) - if PY_25_MAX: # lacks __dir__() support + if PY_MAX_25: # __dir__() support not added until py 2.6 self.assertFalse('max_rounds' in dir(d2)) else: self.assertTrue('max_rounds' in dir(d2)) @@ -444,7 +524,7 @@ class SaltedHash(uh.HasStubChecksum, uh.HasSalt, uh.GenericHandler): raise ValueError("not a salted-example hash") if isinstance(hash, bytes): hash = hash.decode("ascii") - return cls(salt=hash[5:-40], checksum=hash[-40:], strict=True) + return cls(salt=hash[5:-40], checksum=hash[-40:]) _stub_checksum = u('0') * 40 @@ -481,9 +561,6 @@ class UnsaltedHashTest(HandlerCase): # behavior, but that's a lot of effort for a non-critical # border case. so just skipping this test instead... self.assertRaises(TypeError, UnsaltedHash, salt='x') - self.assertRaises(ValueError, SaltedHash, checksum=SaltedHash._stub_checksum, salt=None, strict=True) - self.assertRaises(ValueError, SaltedHash, checksum=SaltedHash._stub_checksum, salt='xxx', strict=True) - self.assertRaises(TypeError, UnsaltedHash.genconfig, rounds=1) class SaltedHashTest(HandlerCase): @@ -495,6 +572,12 @@ class SaltedHashTest(HandlerCase): '@salt9f978a9bfe360d069b0c13f2afecd570447407fa7e48'), ] + def test_bad_kwds(self): + self.assertRaises(TypeError, SaltedHash, + checksum=SaltedHash._stub_checksum, salt=None) + self.assertRaises(ValueError, SaltedHash, + checksum=SaltedHash._stub_checksum, salt='xxx') + #========================================================= #EOF #========================================================= diff --git a/passlib/tests/utils.py b/passlib/tests/utils.py index 628c4ee..a89e707 100644 --- a/passlib/tests/utils.py +++ b/passlib/tests/utils.py @@ -10,6 +10,7 @@ import re import os import sys import tempfile +from passlib.exc import PasslibHandlerWarning from passlib.utils.compat import PY2, PY27, PY_MIN_32, PY3 try: @@ -870,8 +871,11 @@ class HandlerCase(TestCase): #make sure salt is truncated exactly where it should be. salt = cc * mx c1 = self.do_genconfig(salt=salt) - c2 = self.do_genconfig(salt=salt + cc) - self.assertEqual(c1,c2) + self.assertRaises(ValueError, self.do_genconfig, salt=salt + cc) + if _has_relaxed_setting(handler): + with catch_warnings(record=True): # issues passlibhandlerwarning + c2 = self.do_genconfig(salt=salt + cc, relaxed=True) + self.assertEqual(c1,c2) #if min_salt supports it, check smaller than mx is NOT truncated if handler.min_salt_size < mx: @@ -1112,6 +1116,12 @@ def _has_possible_crypt_support(handler): 'os_crypt' in handler.backends and \ not hasattr(handler, "orig_prefix") # ignore wrapper classes +def _has_relaxed_setting(handler): + # FIXME: I've been lazy, should probably just add 'relaxed' kwd + # to all handlers that derive from GenericHandler + return 'relaxed' in handler.setting_kwds or issubclass(handler, + uh.GenericHandler) + def create_backend_case(base, name, module="passlib.tests.test_handlers"): "create a test case for specific backend of a multi-backend handler" #get handler, figure out if backend should be tested diff --git a/passlib/utils/handlers.py b/passlib/utils/handlers.py index 4f4a54f..fcbfdb7 100644 --- a/passlib/utils/handlers.py +++ b/passlib/utils/handlers.py @@ -20,11 +20,10 @@ from passlib.utils import is_crypt_handler from passlib.utils import classproperty, consteq, getrandstr, getrandbytes,\ BASE64_CHARS, HASH64_CHARS, rng, to_native_str from passlib.utils.compat import b, bjoin_ints, bytes, irange, u, \ - uascii_to_str, unicode + uascii_to_str, ujoin, unicode #pkg #local __all__ = [ - #framework for implementing handlers 'StaticHandler', 'GenericHandler', @@ -270,21 +269,31 @@ class GenericHandler(object): by :meth:`from_string()`). defaults to ``None``. - :param strict: - If ``True``, this flag signals that :meth:`norm_checksum` - (as well as the other :samp:`norm_{xxx}` methods provided by the mixins) - should throw a :exc:`ValueError` if any errors are found - in any of the provided parameters. + :param use_defaults: + If ``False`` (the default), a :exc:`TypeError` should be thrown + if any settings required by the handler were not explicitly provided. - If ``False`` (the default), the :exc:`ValueError` should only - be throw if the error is not recoverable (eg: clipping salt string to max size). + If ``True``, the handler should attempt to provide a default for any + missing values. This means generate missing salts, fill in default + cost parameters, etc. This is typically only set to ``True`` when the constructor - is called by :meth:`from_string`, in order to perform validation - on the hash string it's parsing; whereas :meth:`encrypt` - does not set this flag, allowing user-provided values + is called by :meth:`encrypt`, allowing user-provided values to be handled in a more permissive manner. + :param relaxed: + If ``False`` (the default), a :exc:`ValueError` should be thrown + if any settings are out of bounds or otherwise invalid. + + If ``True``, they should be corrected if possible, and a warning + issue. If not possible, only then should an error be raised. + (e.g. under ``relaxed=True``, rounds values will be clamped + to min/max rounds). + + This is mainly used when parsing the config strings of certain + hashes, whose specifications implementations to be tolerant + of incorrect values in salt strings. + Class Attributes ================ @@ -315,7 +324,8 @@ class GenericHandler(object): =================== .. attribute:: checksum - The checksum string as provided by the constructor (after passing through :meth:`norm_checksum`). + The checksum string as provided by the constructor (after passing it + through :meth:`_norm_checksum`). Required Class Methods ====================== @@ -330,7 +340,7 @@ class GenericHandler(object): The following methods provide generally useful default behaviors, though they may be overridden if the hash subclass needs to: - .. automethod:: norm_checksum + .. automethod:: _norm_checksum .. automethod:: genconfig .. automethod:: genhash @@ -346,42 +356,55 @@ class GenericHandler(object): ident = None #identifier prefix if known - checksum_size = None #if specified, norm_checksum will require this length - checksum_chars = None #if specified, norm_checksum() will validate this + checksum_size = None #if specified, _norm_checksum will require this length + checksum_chars = None #if specified, _norm_checksum() will validate this #===================================================== #instance attrs #===================================================== - checksum = None - strict = False #: whether norm_xxx() functions should use strict checking. + checksum = None # stores checksum +# relaxed = False # when norm_xxx() funcs should be strict about inputs +# use_defaults = False # whether norm_xxx() funcs should fill in defaults. #===================================================== #init #===================================================== - def __init__(self, checksum=None, strict=False, **kwds): - self.strict = strict - self.checksum = self.norm_checksum(checksum, strict=strict) + def __init__(self, checksum=None, use_defaults=False, relaxed=False, + **kwds): + self.use_defaults = use_defaults + self.relaxed = relaxed super(GenericHandler, self).__init__(**kwds) + self.checksum = self._norm_checksum(checksum) - #XXX: support a subclass-specified _norm_checksum method - # to normalize for the purposes of verify()? - # currently the code cost seems smaller to just have classes override verify. - - @classmethod - def norm_checksum(cls, checksum, strict=False): - "validates checksum keyword against class requirements, returns normalized version of checksum" + def _norm_checksum(self, checksum): + """validates checksum keyword against class requirements, + returns normalized version of checksum. + """ + # NOTE: this code assumes checksum should be a unicode string. + # For classes where the checksum is raw bytes, the HasRawChecksum + # mixin overrides this method with a more appropriate one. if checksum is None: - if strict: - raise ValueError("checksum not specified") return None + + # normalize to unicode if isinstance(checksum, bytes): checksum = checksum.decode('ascii') - cc = cls.checksum_size + + # check size + cc = self.checksum_size if cc and len(checksum) != cc: - raise ValueError("%s checksum must be %d characters" % (cls.name, cc)) - cs = cls.checksum_chars - if cs and any(c not in cs for c in checksum): - raise ValueError("invalid characters in %s checksum" % (cls.name,)) + raise ValueError("checksum wrong size (%s checksum must be " + "exactly %d characters" % (self.name, cc)) + + # check charset + cs = self.checksum_chars + if cs: + bad = set(checksum) + bad.difference_update(cs) + if bad: + raise ValueError("invalid characters in %s checksum: %r" % + (self.name, ujoin(sorted(bad)))) + return checksum #===================================================== @@ -456,7 +479,7 @@ class GenericHandler(object): #========================================================= @classmethod def genconfig(cls, **settings): - return cls(**settings).to_string() + return cls(use_defaults=True, **settings).to_string() @classmethod def genhash(cls, secret, config): @@ -473,7 +496,7 @@ class GenericHandler(object): #========================================================= @classmethod def encrypt(cls, secret, **settings): - self = cls(**settings) + self = cls(use_defaults=True, **settings) self.checksum = self.calc_checksum(secret) return self.to_string() @@ -507,17 +530,15 @@ class HasRawChecksum(GenericHandler): document this class's usage """ - checksum_chars = None - - @classmethod - def norm_checksum(cls, checksum, strict=False): + def _norm_checksum(self, checksum): if checksum is None: return None if isinstance(checksum, unicode): raise TypeError("checksum must be specified as bytes") - cc = cls.checksum_size + cc = self.checksum_size if cc and len(checksum) != cc: - raise ValueError("%s checksum must be %d characters" % (cls.name, cc)) + raise ValueError("checksum wrong size (%s checksum must be " + "exactly %d characters" % (self.name, cc)) return checksum class HasStubChecksum(GenericHandler): @@ -605,29 +626,29 @@ class HasManyIdents(GenericHandler): #========================================================= #init #========================================================= - def __init__(self, ident=None, strict=False, **kwds): - self.ident = self.norm_ident(ident, strict=strict) - super(HasManyIdents, self).__init__(strict=strict, **kwds) - - @classmethod - def norm_ident(cls, ident, strict=False): - #fill in default identifier - if not ident: - if strict: - raise ValueError("no ident specified") - return cls.default_ident - - #handle unicode + def __init__(self, ident=None, **kwds): + super(HasManyIdents, self).__init__(**kwds) + self.ident = self._norm_ident(ident) + + def _norm_ident(self, ident): + # fill in default identifier + if ident is None: + if not self.use_defaults: + raise TypeError("no ident specified") + ident = self.default_ident + assert ident is not None, "class must define default_ident" + + # handle unicode if isinstance(ident, bytes): ident = ident.decode('ascii') - #check if identifier is valid - iv = cls.ident_values + # check if identifier is valid + iv = self.ident_values if ident in iv: return ident - #check if it's an alias - ia = cls.ident_aliases + # resolve aliases, and recheck against ident_values + ia = self.ident_aliases if ia: try: value = ia[ident] @@ -637,7 +658,7 @@ class HasManyIdents(GenericHandler): if value in iv: return value - #failure! + # failure! raise ValueError("invalid ident: %r" % (ident,)) #========================================================= @@ -727,37 +748,29 @@ class HasSalt(GenericHandler): .. automethod:: norm_salt .. automethod:: generate_salt """ - #TODO: split out "HasRawSalt" mixin for classes where salt should be provided as raw bytes. - # also might need a "HasRawChecksum" to accompany it. #XXX: allow providing raw salt to this class, and encoding it? #========================================================= #class attrs #========================================================= - #NOTE: min/max/default_salt_chars is deprecated, use min/max/default_salt_size instead - #: required - minimum size of salt (error if too small) min_salt_size = None - - #: required - maximum size of salt (truncated if too large) max_salt_size = None + salt_chars = None @classproperty def default_salt_size(cls): "default salt chars (defaults to max_salt_size if not specified by subclass)" return cls.max_salt_size - #: optional - set of characters allowed in salt string. - salt_chars = None - @classproperty def default_salt_chars(cls): "required - set of characters used to generate *new* salt strings (defaults to salt_chars)" return cls.salt_chars - #: helper for HasRawSalt, shouldn't be used publically + # private helpers for HasRawSalt, shouldn't be used by subclasses _salt_is_bytes = False - _salt_unit = "char" + _salt_unit = "chars" #========================================================= #instance attrs @@ -767,90 +780,95 @@ class HasSalt(GenericHandler): #========================================================= #init #========================================================= - def __init__(self, salt=None, salt_size=None, strict=False, **kwds): - self.salt = self.norm_salt(salt, salt_size=salt_size, strict=strict) - super(HasSalt, self).__init__(strict=strict, **kwds) - - @classmethod - def generate_salt(cls, salt_size=None, strict=False): - """helper method for norm_salt(); generates a new random salt string. - - :param salt_size: optional salt size, falls back to :attr:`default_salt_size`. - :param strict: if too-large salt should throw error, or merely be trimmed. - """ - if salt_size is None: - salt_size = cls.default_salt_size - else: - mn = cls.min_salt_size - if mn and salt_size < mn: - raise ValueError("%s salt string must be at least %d characters" % (cls.name, mn)) - mx = cls.max_salt_size - if mx and salt_size > mx: - if strict: - raise ValueError("%s salt string must be at most %d characters" % (cls.name, mx)) - salt_size = mx - if cls._salt_is_bytes: - if cls.salt_chars != ALL_BYTE_VALUES: - raise NotImplementedError("raw salts w/ only certain bytes not supported") - return getrandbytes(rng, salt_size) - else: - return getrandstr(rng, cls.default_salt_chars, salt_size) + def __init__(self, salt=None, salt_size=None, **kwds): + super(HasSalt, self).__init__(**kwds) + self.salt = self._norm_salt(salt, salt_size=salt_size) - @classmethod - def norm_salt(cls, salt, salt_size=None, strict=False): + def _norm_salt(self, salt, salt_size=None): """helper to normalize & validate user-provided salt string + If no salt provided, a random salt is generated + using :attr:`default_salt_size` and :attr:`default_salt_chars`. + :arg salt: salt string or ``None`` - :param strict: enable strict checking (see below); disabled by default + :param salt_size: optionally specified size of autogenerated salt + + :raises TypeError: + If salt not provided and ``use_defaults=False``. :raises ValueError: - * if ``strict=True`` and no salt is provided - * if ``strict=True`` and salt contains greater than :attr:`max_salt_size` characters * if salt contains chars that aren't in :attr:`salt_chars`. * if salt contains less than :attr:`min_salt_size` characters. - - if no salt provided and ``strict=False``, a random salt is generated - using :attr:`default_salt_size` and :attr:`default_salt_chars`. - if the salt is longer than :attr:`max_salt_size` and ``strict=False``, - the salt string is clipped to :attr:`max_salt_size`. + * if ``relaxed=False`` and salt has more than :attr:`max_salt_size` + characters (if ``relaxed=True``, the salt is truncated + and a warning is issued instead). :returns: normalized or generated salt """ - #generate new salt if none provided + # generate new salt if none provided if salt is None: - if strict: - raise ValueError("no salt specified") - #XXX: should we run generated salts through norm_salt? probably. - return cls.generate_salt(salt_size=salt_size, strict=strict) - - #validate input charset - if cls._salt_is_bytes: - if isinstance(salt, unicode): + if not self.use_defaults: + raise TypeError("no salt specified") + if salt_size is None: + salt_size = self.default_salt_size + salt = self._generate_salt(salt_size) + + # check type + if self._salt_is_bytes: + if not isinstance(salt, bytes): raise TypeError("salt must be specified as bytes") else: - if isinstance(salt, bytes): - salt = salt.decode("ascii") - sc = cls.salt_chars + if not isinstance(salt, unicode): + if isinstance(salt, bytes): + salt = salt.decode("ascii") + else: + raise TypeError("salt must be specified as unicode") + + # check charset + sc = self.salt_chars if sc is not None: - for c in salt: - if c not in sc: - raise ValueError("invalid character in %s salt: %r" % (cls.name, c)) - - #check min size - mn = cls.min_salt_size + bad = set(salt) + bad.difference_update(sc) + if bad: + raise ValueError("invalid characters in %s salt: %r" % + (self.name, ujoin(sorted(bad)))) + + # check min size + mn = self.min_salt_size if mn and len(salt) < mn: - raise ValueError("%s salt string must be at least %d %ss" % (cls.name, mn, cls._salt_unit)) - - #check max size - mx = cls.max_salt_size - if mx is not None and len(salt) > mx: - if strict: - raise ValueError("%s salt string must be at most %d %ss" % (cls.name, mx, cls._salt_unit)) - salt = salt[:mx] + msg = "salt too small (%s requires %s %d %s)" % (self.name, + "exactly" if mn == self.max_salt_size else ">=", mn, + self._salt_unit) + raise ValueError(msg) + + # check max size + mx = self.max_salt_size + if mx and len(salt) > mx: + msg = "salt too large (%s requires %s %d %s)" % (self.name, + "exactly" if mx == mn else "<=", mx, self._salt_unit) + if self.relaxed: + warn(msg, PasslibHandlerWarning) + salt = self._truncate_salt(salt, mx) + else: + raise ValueError(msg) return salt + + @staticmethod + def _truncate_salt(salt, mx): + # NOTE: some hashes (e.g. bcrypt) has structure within their + # salt string. this provides a method to overide to perform + # the truncation properly + return salt[:mx] + + def _generate_salt(self, salt_size): + """helper method for _norm_salt(); generates a new random salt string. + :arg salt_size: salt size to generate + """ + return getrandstr(rng, self.default_salt_chars, salt_size) + #========================================================= #eoc #========================================================= @@ -871,7 +889,11 @@ class HasRawSalt(HasSalt): # using private _salt_is_bytes flag. # this arrangement may be changed in the future. _salt_is_bytes = True - _salt_unit = "byte" + _salt_unit = "bytes" + + def _generate_salt(self, salt_size): + assert self.salt_chars in [None, ALL_BYTE_VALUES] + return getrandbytes(rng, salt_size) class HasRounds(GenericHandler): """mixin for validating rounds parameter @@ -946,10 +968,9 @@ class HasRounds(GenericHandler): #class attrs #========================================================= min_rounds = 0 - max_rounds = None #required by ExtendedHandler.norm_rounds() - default_rounds = None #if not specified, ExtendedHandler.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 + max_rounds = None + defaults_rounds = None + rounds_cost = "linear" # default to the common case #========================================================= #instance attrs @@ -959,12 +980,11 @@ class HasRounds(GenericHandler): #========================================================= #init #========================================================= - def __init__(self, rounds=None, strict=False, **kwds): - self.rounds = self.norm_rounds(rounds, strict=strict) - super(HasRounds, self).__init__(strict=strict, **kwds) + def __init__(self, rounds=None, **kwds): + super(HasRounds, self).__init__(**kwds) + self.rounds = self._norm_rounds(rounds) - @classmethod - def norm_rounds(cls, rounds, strict=False): + def _norm_rounds(self, rounds): """helper routine for normalizing rounds :arg rounds: rounds integer or ``None`` @@ -983,35 +1003,40 @@ class HasRounds(GenericHandler): :returns: normalized rounds value """ - #provide default if rounds not explicitly set + # fill in default if rounds is None: - if strict: - raise ValueError("no rounds specified") - rounds = cls.default_rounds + if not self.use_defaults: + raise TypeError("no rounds specified") + rounds = self.default_rounds if rounds is None: - raise ValueError("%s rounds value must be specified explicitly" % (cls.name,)) + raise TypeError("%s rounds value must be specified explicitly" + % (self.name,)) - #if class requests, always throw error instead of clipping - if cls._strict_rounds_bounds: - strict = True + # check type + if not isinstance(rounds, int): + raise TypeError("rounds must be an integer") - mn = cls.min_rounds + # check bounds + mn = self.min_rounds if rounds < mn: - if strict: - raise ValueError("%s rounds must be >= %d" % (cls.name, mn)) - warn("%s does not allow less than %d rounds: %d" % - (cls.name, mn, rounds), PasslibHandlerWarning) - rounds = mn + msg = "rounds too low (%s requires >= %d rounds)" % (self.name, mn) + if self.relaxed: + warn(msg, PasslibHandlerWarning) + rounds = mn + else: + raise ValueError(msg) - mx = cls.max_rounds + mx = self.max_rounds if mx and rounds > mx: - if strict: - raise ValueError("%s rounds must be <= %d" % (cls.name, mx)) - warn("%s does not allow more than %d rounds: %d" % - (cls.name, mx, rounds), PasslibHandlerWarning) - rounds = mx + msg = "rounds too high (%s requires <= %d rounds)" % (self.name, mx) + if self.relaxed: + warn(msg, PasslibHandlerWarning) + rounds = mx + else: + raise ValueError(msg) return rounds + #========================================================= #eoc #========================================================= @@ -1273,6 +1298,7 @@ class PrefixWrapper(object): return value _ident_values = False + @property def ident_values(self): value = self._ident_values |
