diff options
author | Eli Collins <elic@assurancetechnologies.com> | 2012-04-27 02:30:21 -0400 |
---|---|---|
committer | Eli Collins <elic@assurancetechnologies.com> | 2012-04-27 02:30:21 -0400 |
commit | 7a0d65a5a6d61a976daf311fec63171df49ecb37 (patch) | |
tree | a7ef0ea2fdb19e41edd2279a73269435ce8db740 /passlib | |
parent | a01c5e6e1d1a1d770d431702882de49faa586075 (diff) | |
download | passlib-7a0d65a5a6d61a976daf311fec63171df49ecb37.tar.gz |
added support for the new Django 1.4 hash formats
- updated salt handling of the existing django hashes, in a way which should be backwards compatible w/ django 1.0
- UTs now test Django hasher output against passlib handlers (reverse was already being done)
- refactor of fuzz testing to reuse some of the methods.
Diffstat (limited to 'passlib')
-rw-r--r-- | passlib/apps.py | 21 | ||||
-rw-r--r-- | passlib/handlers/django.py | 232 | ||||
-rw-r--r-- | passlib/tests/test_handlers.py | 149 | ||||
-rw-r--r-- | passlib/tests/utils.py | 33 |
4 files changed, 347 insertions, 88 deletions
diff --git a/passlib/apps.py b/passlib/apps.py index 1b108c6..76b8e23 100644 --- a/passlib/apps.py +++ b/passlib/apps.py @@ -89,19 +89,26 @@ custom_app_context = LazyCryptContext( #========================================================= #django #========================================================= - -# XXX: should this be integrated with passlib.ext.django, -# so that it's policy changes to reflect what the extension has set? -# in that case we might need a default_django_context as well. -django_context = LazyCryptContext( - schemes=[ +_django10_schemes = [ "django_salted_sha1", "django_salted_md5", "django_des_crypt", "hex_md5", "django_disabled", - ], +] + +django10_context = LazyCryptContext( + schemes=_django10_schemes, default="django_salted_sha1", deprecated=["hex_md5"], ) +django14_context = LazyCryptContext( + schemes=["django_pbkdf2_sha256", "django_pbkdf2_sha1", "django_bcrypt"] \ + + _django10_schemes, + deprecated=_django10_schemes, +) + +# this will always point to latest version +django_context = django14_context + #========================================================= #ldap #========================================================= diff --git a/passlib/handlers/django.py b/passlib/handlers/django.py index c79a00d..c61aae7 100644 --- a/passlib/handlers/django.py +++ b/passlib/handlers/django.py @@ -3,26 +3,31 @@ #imports #========================================================= #core +from base64 import b64encode from hashlib import md5, sha1 import re import logging; log = logging.getLogger(__name__) from warnings import warn #site #libs -from passlib.utils import to_unicode +from passlib.utils import to_unicode, classproperty from passlib.utils.compat import b, bytes, str_to_uascii, uascii_to_str, unicode, u +from passlib.utils.pbkdf2 import pbkdf2 import passlib.utils.handlers as uh #pkg #local __all__ = [ "django_salted_sha1", "django_salted_md5", + "django_bcrypt", + "django_pbkdf2_sha1", + "django_pbkdf2_sha256", "django_des_crypt", "django_disabled", ] #========================================================= -# lazy imports +# lazy imports & constants #========================================================= des_crypt = None @@ -32,35 +37,56 @@ def _import_des_crypt(): from passlib.hash import des_crypt return des_crypt +# django 1.4's salt charset +SALT_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' + #========================================================= -#salted hashes +# salted hashes #========================================================= class DjangoSaltedHash(uh.HasSalt, uh.GenericHandler): """base class providing common code for django hashes""" - #must be specified by subclass - along w/ calc_checksum + # name, ident, checksum_size must be set by subclass. + # ident must include "$" suffix. setting_kwds = ("salt", "salt_size") - ident = None #must have "$" suffix - #common to most subclasses min_salt_size = 0 - default_salt_size = 5 + # NOTE: django 1.0-1.3 would accept empty salt strings. + # django 1.4 won't, but this appears to be regression + # (https://code.djangoproject.com/ticket/18144) + # so presumably it will be fixed in a later release. + default_salt_size = 12 max_salt_size = None - salt_chars = checksum_chars = uh.LOWER_HEX_CHARS + salt_chars = SALT_CHARS + + checksum_chars = uh.LOWER_HEX_CHARS + + @classproperty + def _stub_checksum(cls): + return cls.checksum_chars[0] * cls.checksum_size @classmethod def from_string(cls, hash): - hash = to_unicode(hash, "ascii", "hash") - ident = cls.ident - assert ident.endswith(u("$")) - if not hash.startswith(ident): - raise uh.exc.InvalidHashError(cls) - _, salt, chk = hash.split(u("$")) - return cls(salt=salt, checksum=chk or None) + salt, chk = uh.parse_mc2(hash, cls.ident, handler=cls) + return cls(salt=salt, checksum=chk) def to_string(self): - chk = self.checksum or self._stub_checksum - hash = u("%s%s$%s") % (self.ident, self.salt, chk) - return uascii_to_str(hash) + return uh.render_mc2(self.ident, self.salt, + self.checksum or self._stub_checksum) + +class DjangoVariableHash(uh.HasRounds, DjangoSaltedHash): + """base class providing common code for django hashes w/ variable rounds""" + setting_kwds = DjangoSaltedHash.setting_kwds + ("rounds",) + + min_rounds = 1 + + @classmethod + def from_string(cls, hash): + rounds, salt, chk = uh.parse_mc3(hash, cls.ident, handler=cls) + return cls(rounds=rounds, salt=salt, checksum=chk) + + def to_string(self): + return uh.render_mc3(self.ident, self.rounds, self.salt, + self.checksum or self._stub_checksum) class django_salted_sha1(DjangoSaltedHash): """This class implements Django's Salted SHA1 hash, and follows the :ref:`password-hash-api`. @@ -81,7 +107,6 @@ class django_salted_sha1(DjangoSaltedHash): name = "django_salted_sha1" ident = u("sha1$") checksum_size = 40 - _stub_checksum = u('0') * 40 def _calc_checksum(self, secret): if isinstance(secret, unicode): @@ -107,18 +132,105 @@ class django_salted_md5(DjangoSaltedHash): name = "django_salted_md5" ident = u("md5$") checksum_size = 32 - _stub_checksum = u('0') * 32 def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") return str_to_uascii(md5(self.salt.encode("ascii") + secret).hexdigest()) +django_bcrypt = uh.PrefixWrapper("django_bcrypt", "bcrypt", + prefix=u('bcrypt$'), ident=u("bcrypt$"), + # NOTE: this docstring is duplicated in the docs, since sphinx + # seems to be having trouble reading it via autodata:: + doc="""This class implements Django 1.4's BCrypt wrapper, and follows the :ref:`password-hash-api`. + + This is identical to :class:`!bcrypt` itself, but with + the Django-specific prefix ``"bcrypt$"`` prepended. + + See :doc:`/lib/passlib.hash.bcrypt` for more details, + the usage and behavior is identical. + + This should be compatible with the hashes generated by + Django 1.4's :class:`!BCryptPasswordHasher` class. + + .. versionadded:: 1.6 + """) + +class django_pbkdf2_sha256(DjangoVariableHash): + """This class implements Django's PBKDF2-HMAC-SHA256 hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and a variable number of rounds. + + The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords: + + :param salt: + Optional salt string. + If not specified, a 12 character one will be autogenerated (this is recommended). + If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``. + + :param salt_size: + Optional number of characters to use when autogenerating new salts. + Defaults to 12, but can be any positive value. + + :param rounds: + Optional number of rounds to use. + Defaults to 10000, but must be within ``range(1,1<<32)``. + + This should be compatible with the hashes generated by + Django 1.4's :class:`!PBKDF2PasswordHasher` class. + + .. versionadded:: 1.6 + """ + name = "django_pbkdf2_sha256" + ident = u('pbkdf2_sha256$') + min_salt_size = 1 + max_rounds = 0xffffffff # setting at 32-bit limit for now + checksum_chars = uh.PADDED_BASE64_CHARS + checksum_size = 44 # 32 bytes -> base64 + default_rounds = 10000 # NOTE: using django default here + _prf = "hmac-sha256" + + def _calc_checksum(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + hash = pbkdf2(secret, self.salt.encode("ascii"), self.rounds, + keylen=-1, prf=self._prf) + return b64encode(hash).rstrip().decode("ascii") + +class django_pbkdf2_sha1(django_pbkdf2_sha256): + """This class implements Django's PBKDF2-HMAC-SHA1 hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and a variable number of rounds. + + The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords: + + :param salt: + Optional salt string. + If not specified, a 12 character one will be autogenerated (this is recommended). + If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``. + + :param salt_size: + Optional number of characters to use when autogenerating new salts. + Defaults to 12, but can be any positive value. + + :param rounds: + Optional number of rounds to use. + Defaults to 10000, but must be within ``range(1,1<<32)``. + + This should be compatible with the hashes generated by + Django 1.4's :class:`!PBKDF2SHA1PasswordHasher` class. + + .. versionadded:: 1.6 + """ + name = "django_pbkdf2_sha1" + ident = u('pbkdf2_sha1$') + checksum_size = 28 # 20 bytes -> base64 + _prf = "hmac-sha1" + #========================================================= #other #========================================================= - -class django_des_crypt(DjangoSaltedHash): +class django_des_crypt(uh.HasSalt, uh.GenericHandler): """This class implements Django's :class:`des_crypt` wrapper, and follows the :ref:`password-hash-api`. It supports a fixed-length salt. @@ -130,50 +242,49 @@ class django_des_crypt(DjangoSaltedHash): If not specified, one will be autogenerated (this is recommended). If specified, it must be 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``. - .. note:: + This should be compatible with the hashes generated by + Django 1.4's :class:`!CryptPasswordHasher` class. + Note that Django only supports this hash on Unix systems + (though :class:`!django_des_crypt` is available cross-platform + under Passlib). - Django only supports this on Unix systems, - but it is available cross-platform under Passlib. + .. versionchanged:: 1.6 + This class will now accept hashes with empty salt strings, + since Django 1.4 generates them this way. """ - name = "django_des_crypt" + setting_kwds = ("salt", "salt_size") ident = u("crypt$") checksum_chars = salt_chars = uh.HASH64_CHARS - checksum_size = 13 - min_salt_size = 2 - - # NOTE: checksum is full des_crypt hash, - # including salt as first two digits. - # these should always match first two digits - # of django_des_crypt's salt... - # and all remaining chars of salt are ignored. + checksum_size = 11 + min_salt_size = default_salt_size = 2 + _stub_checksum = u('.')*11 - def __init__(self, **kwds): - super(django_des_crypt, self).__init__(**kwds) + @classmethod + def from_string(cls, hash): + salt, chk = uh.parse_mc2(hash, cls.ident, handler=cls) + if chk: + # chk should be full des_crypt hash + if not salt: + # django 1.4 always uses empty salt field, + # so extract salt from des_crypt hash <chk> + salt = chk[:2] + elif salt[:2] != chk[:2]: + # django 1.0 stored 5 chars in salt field, and duplicated + # the first two chars in <chk>. we keep the full salt, + # but make sure the first two chars match as sanity check. + raise uh.exc.MalformedHashError(cls, + "first two digits of salt and checksum must match") + # in all cases, strip salt chars from <chk> + chk = chk[2:] + return cls(salt=salt, checksum=chk) - # make sure salt embedded in checksum is a match, - # else hash can *never* validate + def to_string(self): + # NOTE: always filling in salt field, so that we're compatible + # with django 1.0 (which requires it) salt = self.salt - chk = self.checksum - if salt and chk: - if salt[:2] != chk[:2]: - raise uh.exc.MalformedHashError(self, - "first two digits of salt and checksum must match") - # repeat stub checksum detection since salt isn't set - # when _norm_checksum() is called. - if chk == self._stub_checksum: - self.checksum = None - - _base_stub_checksum = u('.') * 13 - - @property - def _stub_checksum(self): - "generate stub checksum dynamically, so it matches always matches salt" - stub = self._base_stub_checksum - if self.salt: - return self.salt[:2] + stub[2:] - else: - return stub + chk = salt[:2] + (self.checksum or self._stub_checksum) + return uh.render_mc2(self.ident, salt, chk) def _calc_checksum(self, secret): # NOTE: we lazily import des_crypt, @@ -181,8 +292,7 @@ class django_des_crypt(DjangoSaltedHash): global des_crypt if des_crypt is None: _import_des_crypt() - salt = self.salt[:2] - return salt + des_crypt(salt=salt)._calc_checksum(secret) + return des_crypt(salt=self.salt[:2])._calc_checksum(secret) class django_disabled(uh.StaticHandler): """This class provides disabled password behavior for Django, and follows the :ref:`password-hash-api`. @@ -212,5 +322,5 @@ class django_disabled(uh.StaticHandler): return False #========================================================= -#eof +# eof #========================================================= diff --git a/passlib/tests/test_handlers.py b/passlib/tests/test_handlers.py index 4e7c040..81a67fd 100644 --- a/passlib/tests/test_handlers.py +++ b/passlib/tests/test_handlers.py @@ -678,27 +678,64 @@ class _DjangoHelper(object): # NOTE: not testing against Django < 1.0 since it doesn't support # most of these hash formats. + # flag if hash wasn't added until Django 1.4 + requires14 = False + def fuzz_verifier_django(self): - from passlib.tests.test_ext_django import has_django1 + from passlib.tests.test_ext_django import has_django1, has_django14 if not has_django1: return None + if self.requires14 and not has_django14: + return None from django.contrib.auth.models import check_password def verify_django(secret, hash): - "django check_password()" + "django/check_password" + if has_django14 and not secret: + return "skip" + if self.handler.name == "django_bcrypt" and hash.startswith("bcrypt$$2y$"): + hash = hash.replace("$$2y$", "$$2a$") return check_password(secret, hash) return verify_django def test_90_django_reference(self): "run known correct hashes through Django's check_password()" - if not self.known_correct_hashes: - return self.skipTest("no known correct hashes specified") - from passlib.tests.test_ext_django import has_django1 + from passlib.tests.test_ext_django import has_django1, has_django14 + if self.requires14 and not has_django14: + raise self.skipTest("Django >= 1.4 not installed") if not has_django1: - return self.skipTest("Django >= 1.0 not installed") + raise self.skipTest("Django >= 1.0 not installed") from django.contrib.auth.models import check_password + assert self.known_correct_hashes for secret, hash in self.iter_known_hashes(): - self.assertTrue(check_password(secret, hash)) - self.assertFalse(check_password('x' + secret, hash)) + if has_django14 and not secret: + # django 1.4 rejects empty passwords + self.assertFalse(check_password(secret, hash), + "empty string should not have verified") + continue + self.assertTrue(check_password(secret, hash), + "secret=%r hash=%r failed to verify" % + (secret, hash)) + self.assertFalse(check_password('x' + secret, hash), + "mangled secret=%r hash=%r incorrect verified" % + (secret, hash)) + + def test_91_django_generation(self): + "test against output of Django's make_password()" + from passlib.tests.test_ext_django import has_django14 + if not has_django14: + raise self.skipTest("Django >= 1.4 not installed") + from passlib.utils import tick + from django.contrib.auth.hashers import make_password + name = self.handler.django_name # set for all the django_* handlers + end = tick() + self.max_fuzz_time/2 + while tick() < end: + secret, other = self.get_fuzz_password_pair() + if not secret: # django 1.4 rejects empty passwords. + continue + hash = make_password(secret, hasher=name) + self.assertTrue(self.do_identify(hash)) + self.assertTrue(self.do_verify(secret, hash)) + self.assertFalse(self.do_verify(other, hash)) class django_disabled_test(HandlerCase): "test django_disabled" @@ -732,6 +769,12 @@ class django_des_crypt_test(HandlerCase, _DjangoHelper): ("foo", 'crypt$MNVY.9ajgdvDQ$MNVY.9ajgdvDQ'), ] + known_alternate_hashes = [ + # ensure django 1.4 empty salt field is accepted; + # but that salt field is re-filled (for django 1.0 compatibility) + ('crypt$$c2M87q...WWcU', "password", 'crypt$c2$c2M87q...WWcU'), + ] + known_unidentified_hashes = [ 'sha1$aa$bb', ] @@ -741,11 +784,9 @@ class django_des_crypt_test(HandlerCase, _DjangoHelper): 'crypt$c2$c2M87q', # salt must be >2 - 'crypt$$c2M87q...WWcU', 'crypt$f$c2M87q...WWcU', - # this format duplicates salt inside checksum, - # reject any where the two copies don't match + # make sure first 2 chars of salt & chk field agree. 'crypt$ffe86$c2M87q...WWcU', ] @@ -757,6 +798,9 @@ class django_salted_md5_test(HandlerCase, _DjangoHelper): # test extra large salt ("password", 'md5$123abcdef$c8272612932975ee80e8a35995708e80'), + # test django 1.4 alphanumeric salt + ("test", 'md5$3OpqnFAHW5CT$54b29300675271049a1ebae07b395e20'), + # ensures utf-8 used for unicode (UPASS_USD, 'md5$c2e86$92105508419a81a6babfaecf876a2fa0'), (UPASS_TABLE, 'md5$d9eb8$01495b32852bffb27cf5d4394fe7a54c'), @@ -779,6 +823,9 @@ class django_salted_sha1_test(HandlerCase, _DjangoHelper): # test extra large salt ("password",'sha1$123abcdef$e4a1877b0e35c47329e7ed7e58014276168a37ba'), + # test django 1.4 alphanumeric salt + ("test", 'sha1$bcwHF9Hy8lxS$6b4cfa0651b43161c6f1471ce9523acf1f751ba3'), + # ensures utf-8 used for unicode (UPASS_USD, 'sha1$c2e86$0f75c5d7fbd100d587c127ef0b693cde611b4ada'), (UPASS_TABLE, 'sha1$6d853$ef13a4d8fb57aed0cb573fe9c82e28dc7fd372d4'), @@ -796,6 +843,82 @@ class django_salted_sha1_test(HandlerCase, _DjangoHelper): 'sha1$c2e86$0f75', ] +class django_pbkdf2_sha256_test(HandlerCase, _DjangoHelper): + "test django_pbkdf2_sha256" + handler = hash.django_pbkdf2_sha256 + requires14 = True + + known_correct_hashes = [ + # + # custom - generated via django 1.4 hasher + # + ('not a password', + 'pbkdf2_sha256$10000$kjVJaVz6qsnJ$5yPHw3rwJGECpUf70daLGhOrQ5+AMxIJdz1c3bqK1Rs='), + (UPASS_TABLE, + 'pbkdf2_sha256$10000$bEwAfNrH1TlQ$OgYUblFNUX1B8GfMqaCYUK/iHyO0pa7STTDdaEJBuY0='), + ] + +class django_pbkdf2_sha1_test(HandlerCase, _DjangoHelper): + "test django_pbkdf2_sha1" + handler = hash.django_pbkdf2_sha1 + requires14 = True + + known_correct_hashes = [ + # + # custom - generated via django 1.4 hashers + # + ('not a password', + 'pbkdf2_sha1$10000$wz5B6WkasRoF$atJmJ1o+XfJxKq1+Nu1f1i57Z5I='), + (UPASS_TABLE, + 'pbkdf2_sha1$10000$KZKWwvqb8BfL$rw5pWsxJEU4JrZAQhHTCO+u0f5Y='), + ] + +class django_bcrypt_test(HandlerCase, _DjangoHelper): + "test django_bcrypt" + handler = hash.django_bcrypt + secret_size = 72 + requires14 = True + + known_correct_hashes = [ + # + # just copied and adapted a few test vectors from bcrypt (above), + # since django_bcrypt is just a wrapper for the real bcrypt class. + # + ('', 'bcrypt$$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.'), + ('abcdefghijklmnopqrstuvwxyz', + 'bcrypt$$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq'), + (UPASS_TABLE, + 'bcrypt$$2a$05$Z17AXnnlpzddNUvnC6cZNOSwMA/8oNiKnHTHTwLlBijfucQQlHjaG'), + ] + + # NOTE: the following have been cloned from _bcrypt_test() + + def do_genconfig(self, **kwds): + # override default to speed up tests + kwds.setdefault("rounds", 5) + + # correct unused bits in provided salts, to silence some warnings. + if 'salt' in kwds: + from passlib.utils import bcrypt64 + kwds['salt'] = bcrypt64.repair_unused(kwds['salt']) + return self.handler.genconfig(**kwds) + + def do_encrypt(self, secret, **kwds): + # override default to speed up tests + kwds.setdefault("rounds", 5) + return self.handler.encrypt(secret, **kwds) + + def get_fuzz_rounds(self): + # decrease default rounds for fuzz testing to speed up volume. + return randintgauss(5, 8, 6, 1) + + def get_fuzz_ident(self): + ident = super(django_bcrypt_test,self).get_fuzz_ident() + if ident == u("$2x$"): + # just recognized, not currently supported. + return None + return ident + #========================================================= #fshp #========================================================= @@ -888,7 +1011,7 @@ class hex_md4_test(HandlerCase): (UPASS_TABLE, '876078368c47817ce5f9115f3a42cf74'), ] -class hex_md5_test(HandlerCase): +class hex_md5_test(HandlerCase, _DjangoHelper): handler = hash.hex_md5 known_correct_hashes = [ ("password", '5f4dcc3b5aa765d61d8327deb882cf99'), @@ -2534,5 +2657,5 @@ class unix_fallback_test(HandlerCase): self.assertFalse(h.verify('password',c)) #========================================================= -#EOF +# eof #========================================================= diff --git a/passlib/tests/utils.py b/passlib/tests/utils.py index aec23e8..35a7b50 100644 --- a/passlib/tests/utils.py +++ b/passlib/tests/utils.py @@ -979,6 +979,7 @@ class HandlerCase(TestCase): @property def salt_bits(self): "calculate number of salt bits in hash" + # XXX: replace this with bitsize() method? handler = self.handler assert has_salt_info(handler), "need explicit bit-size for " + handler.name from math import log @@ -1604,6 +1605,10 @@ class HandlerCase(TestCase): fuzz_password_encoding = "utf-8" fuzz_settings = ["rounds", "salt_size", "ident"] + @property + def max_fuzz_time(self): + return float(os.environ.get("PASSLIB_TESTS_FUZZ_TIME") or 1) + def test_77_fuzz_input(self): """test random passwords and options""" if self.is_disabled_handler: @@ -1613,7 +1618,7 @@ class HandlerCase(TestCase): from passlib.utils import tick handler = self.handler disabled = self.is_disabled_handler - max_time = float(os.environ.get("PASSLIB_TESTS_FUZZ_TIME") or 1) + max_time = self.max_fuzz_time verifiers = self.get_fuzz_verifiers() def vname(v): return (v.__doc__ or v.__name__).splitlines()[0] @@ -1623,11 +1628,7 @@ class HandlerCase(TestCase): count = 0 while tick() <= stop: # generate random password & options - secret = self.get_fuzz_password() - other = self.mangle_fuzz_password(secret) - if rng.randint(0,1): - secret = secret.encode(self.fuzz_password_encoding) - other = other.encode(self.fuzz_password_encoding) + secret, other = self.get_fuzz_password_pair() kwds = self.get_fuzz_settings() ctx = dict((k,kwds[k]) for k in handler.context_kwds if k in kwds) @@ -1726,15 +1727,31 @@ class HandlerCase(TestCase): def get_fuzz_password(self): "generate random passwords (for fuzz testing)" + # occasionally try an empty password if rng.random() < .0001: return u('') - return getrandstr(rng, self.fuzz_password_alphabet, rng.randint(5,99)) + # otherwise alternate between large and small passwords. + if rng.random() < .5: + size = randintgauss(1, 50, 15, 15) + else: + size = randintgauss(50, 99, 70, 20) + return getrandstr(rng, self.fuzz_password_alphabet, size) def mangle_fuzz_password(self, secret): "mangle fuzz-testing password so it doesn't match" secret = secret.strip()[1:] return secret or self.get_fuzz_password() + def get_fuzz_password_pair(self): + "generate random password, and non-matching alternate password" + secret = self.get_fuzz_password() + other = self.mangle_fuzz_password(secret) + if rng.randint(0,1): + secret = secret.encode(self.fuzz_password_encoding) + if rng.randint(0,1): + other = other.encode(self.fuzz_password_encoding) + return secret, other + def get_fuzz_settings(self): "generate random settings (for fuzz testing)" kwds = {} @@ -1771,6 +1788,8 @@ class HandlerCase(TestCase): handler = self.handler if 'ident' in handler.setting_kwds and hasattr(handler, "ident_values"): if rng.random() < .5: + # resolve wrappers before reading values + handler = getattr(handler, "wrapped", handler) return rng.choice(handler.ident_values) #========================================================= |