"""passlib.tests.test_handlers_django - tests for passlib hash algorithms""" #============================================================================= # imports #============================================================================= from __future__ import with_statement # core import hashlib import logging; log = logging.getLogger(__name__) import os import warnings # site # pkg from passlib import hash from passlib.utils import repeat_string from passlib.utils.compat import irange, PY3, u, get_method_function from passlib.tests.utils import TestCase, HandlerCase, skipUnless, \ TEST_MODE, b, catch_warnings, UserHandlerMixin, randintgauss, EncodingHandlerMixin from passlib.tests.test_handlers import UPASS_WAV, UPASS_USD, UPASS_TABLE # module #============================================================================= # django #============================================================================= 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 DJANGO_VERSION if DJANGO_VERSION < (1,0): return None if self.requires14 and DJANGO_VERSION < (1,4): return None from django.contrib.auth.models import check_password def verify_django(secret, hash): "django/check_password" if DJANGO_VERSION >= (1,4) and not secret: return "skip" if self.handler.name == "django_bcrypt" and hash.startswith("bcrypt$$2y$"): hash = hash.replace("$$2y$", "$$2a$") if DJANGO_VERSION >= (1,5) and self.django_has_encoding_glitch and isinstance(secret, bytes): # e.g. unsalted_md5 on 1.5 and higher try to combine # salt + password before encoding to bytes, leading to ascii error. # this works around that issue. secret = secret.decode("utf-8") return check_password(secret, hash) return verify_django def test_90_django_reference(self): "run known correct hashes through Django's check_password()" 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: 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(): 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)) django_has_encoding_glitch = False def test_91_django_generation(self): "test against output of Django's make_password()" from passlib.tests.test_ext_django import DJANGO_VERSION if DJANGO_VERSION < (1,4): 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 if DJANGO_VERSION >= (1,5) and self.django_has_encoding_glitch and isinstance(secret, bytes): # e.g. unsalted_md5 on 1.5 and higher try to combine # salt + password before encoding to bytes, leading to ascii error. # this works around that issue. secret = secret.decode("utf-8") 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" handler = hash.django_disabled is_disabled_handler = True known_correct_hashes = [ # *everything* should hash to "!", and nothing should verify ("password", "!"), ("", "!"), (UPASS_TABLE, "!"), ] known_alternate_hashes = [ # django 1.6 appends random alpnum string ("!9wa845vn7098ythaehasldkfj", "password", "!"), ] class django_des_crypt_test(HandlerCase, _DjangoHelper): "test django_des_crypt" handler = hash.django_des_crypt secret_size = 8 known_correct_hashes = [ # 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_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', ] known_malformed_hashes = [ # checksum too short 'crypt$c2$c2M87q', # salt must be >2 'crypt$f$c2M87q...WWcU', # make sure first 2 chars of salt & chk field agree. 'crypt$ffe86$c2M87q...WWcU', ] class django_salted_md5_test(HandlerCase, _DjangoHelper): "test django_salted_md5" handler = hash.django_salted_md5 django_has_encoding_glitch = True known_correct_hashes = [ # 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'), ] known_unidentified_hashes = [ 'sha1$aa$bb', ] known_malformed_hashes = [ # checksum too short 'md5$aa$bb', ] def fuzz_setting_salt_size(self): # workaround for django14 regression -- # 1.4 won't accept hashes with empty salt strings, unlike 1.3 and earlier. # looks to be fixed in a future release -- https://code.djangoproject.com/ticket/18144 # for now, we avoid salt_size==0 under 1.4 handler = self.handler from passlib.tests.test_ext_django import has_django14 default = handler.default_salt_size assert handler.min_salt_size == 0 lower = 1 if has_django14 else 0 upper = handler.max_salt_size or default*4 return randintgauss(lower, upper, default, default*.5) class django_salted_sha1_test(HandlerCase, _DjangoHelper): "test django_salted_sha1" handler = hash.django_salted_sha1 django_has_encoding_glitch = True known_correct_hashes = [ # 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'), # generic password ("MyPassword", 'sha1$54123$893cf12e134c3c215f3a76bd50d13f92404a54d3'), ] known_unidentified_hashes = [ 'md5$aa$bb', ] known_malformed_hashes = [ # checksum too short 'sha1$c2e86$0f75', ] fuzz_setting_salt_size = get_method_function(django_salted_md5_test.fuzz_setting_salt_size) 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 populate_settings(self, kwds): # speed up test w/ lower rounds kwds.setdefault("rounds", 4) super(django_bcrypt_test, self).populate_settings(kwds) def fuzz_setting_rounds(self): # decrease default rounds for fuzz testing to speed up volume. return randintgauss(5, 8, 6, 1) def fuzz_setting_ident(self): # omit multi-ident tests, only $2a$ counts for this class return None django_bcrypt_test = skipUnless(hash.bcrypt.has_backend(), "no bcrypt backends available")(django_bcrypt_test) #============================================================================= # eof #=============================================================================