summaryrefslogtreecommitdiff
path: root/passlib
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2011-09-12 15:50:22 -0400
committerEli Collins <elic@assurancetechnologies.com>2011-09-12 15:50:22 -0400
commit3e73bef1a6d8e586fb1eceee21bec46fe642cf44 (patch)
tree25409059bac4d269ae825243346d078d08a506ea /passlib
parent00df5078b8760575321b91fdc878d0c9a04d6c12 (diff)
downloadpasslib-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.py75
-rw-r--r--passlib/tests/test_drivers.py72
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',
]