summaryrefslogtreecommitdiff
path: root/passlib
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2012-04-27 02:30:21 -0400
committerEli Collins <elic@assurancetechnologies.com>2012-04-27 02:30:21 -0400
commit7a0d65a5a6d61a976daf311fec63171df49ecb37 (patch)
treea7ef0ea2fdb19e41edd2279a73269435ce8db740 /passlib
parenta01c5e6e1d1a1d770d431702882de49faa586075 (diff)
downloadpasslib-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.py21
-rw-r--r--passlib/handlers/django.py232
-rw-r--r--passlib/tests/test_handlers.py149
-rw-r--r--passlib/tests/utils.py33
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)
#=========================================================