summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2012-02-08 22:38:25 -0500
committerEli Collins <elic@assurancetechnologies.com>2012-02-08 22:38:25 -0500
commit86a2dc3ed68fcdf7853f5e219541a19b5fcacfff (patch)
tree8319e8704f04a23faab1eedfad383d50ff6d3671
parent4c4615329b64287dabd729e3078ab03cb2bb7442 (diff)
downloadpasslib-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.py3
-rw-r--r--passlib/handlers/bcrypt.py29
-rw-r--r--passlib/handlers/des_crypt.py12
-rw-r--r--passlib/handlers/django.py2
-rw-r--r--passlib/handlers/fshp.py46
-rw-r--r--passlib/handlers/ldap_digests.py2
-rw-r--r--passlib/handlers/md5_crypt.py2
-rw-r--r--passlib/handlers/nthash.py2
-rw-r--r--passlib/handlers/oracle.py2
-rw-r--r--passlib/handlers/pbkdf2.py6
-rw-r--r--passlib/handlers/phpass.py2
-rw-r--r--passlib/handlers/scram.py18
-rw-r--r--passlib/handlers/sha1_crypt.py1
-rw-r--r--passlib/handlers/sha2_crypt.py10
-rw-r--r--passlib/handlers/sun_md5_crypt.py2
-rw-r--r--passlib/tests/test_context.py3
-rw-r--r--passlib/tests/test_handlers.py45
-rw-r--r--passlib/tests/test_utils_handlers.py267
-rw-r--r--passlib/tests/utils.py14
-rw-r--r--passlib/utils/handlers.py358
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