diff options
| author | Eli Collins <elic@assurancetechnologies.com> | 2011-09-12 15:50:22 -0400 |
|---|---|---|
| committer | Eli Collins <elic@assurancetechnologies.com> | 2011-09-12 15:50:22 -0400 |
| commit | 3e73bef1a6d8e586fb1eceee21bec46fe642cf44 (patch) | |
| tree | 25409059bac4d269ae825243346d078d08a506ea /passlib | |
| parent | 00df5078b8760575321b91fdc878d0c9a04d6c12 (diff) | |
| download | passlib-3e73bef1a6d8e586fb1eceee21bec46fe642cf44.tar.gz | |
bugfix: django_des_crypt now accepts all H64_CHARS in salt [issue 22]
* also added more django-related unittests
* django_des_crypt now uses des_crypt handler instead of raw_des_crypt function
* django_des_crypt now detects salt char mismatches in hash
Diffstat (limited to 'passlib')
| -rw-r--r-- | passlib/handlers/django.py | 75 | ||||
| -rw-r--r-- | passlib/tests/test_drivers.py | 72 |
2 files changed, 114 insertions, 33 deletions
diff --git a/passlib/handlers/django.py b/passlib/handlers/django.py index f717859..823463b 100644 --- a/passlib/handlers/django.py +++ b/passlib/handlers/django.py @@ -20,6 +20,17 @@ __all__ = [ ] #========================================================= +# lazy imports +#========================================================= +des_crypt = None + +def _import_des_crypt(): + global des_crypt + if des_crypt is None: + from passlib.hash import des_crypt + return des_crypt + +#========================================================= #salted hashes #========================================================= class DjangoSaltedHash(uh.HasSalt, uh.GenericHandler): @@ -112,6 +123,7 @@ class django_salted_md5(DjangoSaltedHash): #========================================================= #other #========================================================= + class django_des_crypt(DjangoSaltedHash): """This class implements Django's :class:`des_crypt` wrapper, and follows the :ref:`password-hash-api`. @@ -123,38 +135,55 @@ class django_des_crypt(DjangoSaltedHash): Optional salt string. 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:: - + Django only supports this on Unix systems, but it is available cross-platform under Passlib. - """ + """ name = "django_des_crypt" ident = "crypt$" - checksum_chars = uh.H64_CHARS - checksum_size = 13 #NOTE: includes dup copy of salt chars - _stub_checksum = u'.' * 13 - - #NOTE: django generates des_crypt hashes w/ 5 char salt, - # but last 3 are just ignored by crypt() - - #XXX: we *could* check if OS des_crypt support present, - # but not really worth bother. - - _raw_crypt = None #lazy imported + checksum_chars = salt_chars = uh.H64_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. + + def __init__(self, **kwds): + super(django_des_crypt, self).__init__(**kwds) + + # make sure salt embedded in checksum is a match, + # else hash can *never* validate + salt = self.salt + chk = self.checksum + if salt and chk and salt[:2] != chk[:2]: + raise ValueError("invalid django_des_crypt hash: " + "first two digits of salt and checksum must match") + + _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 def calc_checksum(self, secret): - if isinstance(secret, unicode): - secret = secret.encode("utf-8") - #lazy import raw_crypt from des_crypt only if needed, - #since most django deploys won't need this. - raw_crypt = self._raw_crypt - if raw_crypt is None: - from passlib.handlers.des_crypt import raw_crypt - self._raw_crypt = raw_crypt + # NOTE: we lazily import des_crypt, + # since most django deploys won't use django_des_crypt + global des_crypt + if des_crypt is None: + _import_des_crypt() salt = self.salt[:2] - return salt + raw_crypt(secret, salt.encode("ascii")).decode("ascii") + return salt + des_crypt(salt=salt).calc_checksum(secret) class django_disabled(uh.StaticHandler): """This class provides disabled password behavior for Django, and follows the :ref:`password-hash-api`. diff --git a/passlib/tests/test_drivers.py b/passlib/tests/test_drivers.py index 8c043e8..828f898 100644 --- a/passlib/tests/test_drivers.py +++ b/passlib/tests/test_drivers.py @@ -19,9 +19,10 @@ from passlib.tests.utils import TestCase, HandlerCase, create_backend_case, \ #some #========================================================= -#some common passwords which used as test cases... +#some common unicode passwords which used as test cases... UPASS_WAV = u'\u0399\u03c9\u03b1\u03bd\u03bd\u03b7\u03c2' UPASS_USD = u"\u20AC\u00A5$" +UPASS_TABLE = u"t\u00e1\u0411\u2113\u0259" #========================================================= #apr md5 crypt @@ -210,6 +211,22 @@ Builtin_DesCryptTest = create_backend_case(_DesCryptTest, "builtin") #========================================================= #django #========================================================= +class _DjangoHelper(object): + + def test_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") + try: + from django.conf import settings + except ImportError: + return self.skipTest("Django not installed") + settings.configure() + from django.contrib.auth.models import check_password + for secret, hash in self.known_correct_hashes: + self.assertTrue(check_password(secret, hash)) + self.assertFalse(check_password('x' + secret, hash)) + class DjangoDisabledTest(HandlerCase): "test django_disabled" @@ -234,27 +251,54 @@ class DjangoDisabledTest(HandlerCase): self.assertEqual(result, "!") self.assertTrue(not self.do_verify(secret, result)) -class DjangoDesCryptTest(HandlerCase): +class DjangoDesCryptTest(HandlerCase, _DjangoHelper): "test django_des_crypt" handler = hash.django_des_crypt secret_chars = 8 known_correct_hashes = [ - ("password", 'crypt$c2e86$c2M87q...WWcU'), + #ensures only first two digits of salt count. + ("password", 'crypt$c2$c2M87q...WWcU'), + ("password", 'crypt$c2e86$c2M87q...WWcU'), + ("passwordignoreme", 'crypt$c2.AZ$c2M87q...WWcU'), + + #ensures utf-8 used for unicode (UPASS_USD, 'crypt$c2e86$c2hN1Bxd6ZiWs'), + (UPASS_TABLE, 'crypt$0.aQs$0.wB.TT0Czvlo'), + (u"hell\u00D6", "crypt$sa$saykDgk3BPZ9E"), + + #prevent regression of issue 22 + ("foo", 'crypt$MNVY.9ajgdvDQ$MNVY.9ajgdvDQ'), ] known_unidentified_hashes = [ 'sha1$aa$bb', ] -class DjangoSaltedMd5Test(HandlerCase): + known_malformed_hashes = [ + # checksum too short + '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 + 'crypt$ffe86$c2M87q...WWcU', + ] + +class DjangoSaltedMd5Test(HandlerCase, _DjangoHelper): "test django_salted_md5" handler = hash.django_salted_md5 known_correct_hashes = [ - ("password", 'md5$c2e86$a6e6e41fd0113c98d8b82422466dcf55'), - (UPASS_USD, 'md5$c2e86$92105508419a81a6babfaecf876a2fa0'), + #test extra large salt + ("password", 'md5$123abcdef$c8272612932975ee80e8a35995708e80'), + + #test unicode uses utf-8 + (UPASS_USD, 'md5$c2e86$92105508419a81a6babfaecf876a2fa0'), + (UPASS_TABLE, 'md5$d9eb8$01495b32852bffb27cf5d4394fe7a54c'), ] known_unidentified_hashes = [ @@ -262,17 +306,24 @@ class DjangoSaltedMd5Test(HandlerCase): ] known_malformed_hashes = [ + # checksum too short 'md5$aa$bb', ] -class DjangoSaltedSha1Test(HandlerCase): +class DjangoSaltedSha1Test(HandlerCase, _DjangoHelper): "test django_salted_sha1" handler = hash.django_salted_sha1 known_correct_hashes = [ - ("password", 'sha1$c2e86$b2949ae168955e92599656edd7a32916e0f5fc51'), - (UPASS_USD, 'sha1$c2e86$0f75c5d7fbd100d587c127ef0b693cde611b4ada'), - ("MyPassword", 'sha1$54123$893cf12e134c3c215f3a76bd50d13f92404a54d3'), + #test extra large salt + ("password",'sha1$123abcdef$e4a1877b0e35c47329e7ed7e58014276168a37ba'), + + #test unicode uses utf-8 + (UPASS_USD, 'sha1$c2e86$0f75c5d7fbd100d587c127ef0b693cde611b4ada'), + (UPASS_TABLE, 'sha1$6d853$ef13a4d8fb57aed0cb573fe9c82e28dc7fd372d4'), + + #generic password + ("MyPassword", 'sha1$54123$893cf12e134c3c215f3a76bd50d13f92404a54d3'), ] known_unidentified_hashes = [ @@ -280,6 +331,7 @@ class DjangoSaltedSha1Test(HandlerCase): ] known_malformed_hashes = [ + # checksum too short 'sha1$c2e86$0f75', ] |
