test_handlers_django.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. """passlib.tests.test_handlers_django - tests for passlib hash algorithms"""
  2. #=============================================================================
  3. # imports
  4. #=============================================================================
  5. from __future__ import with_statement
  6. # core
  7. import logging; log = logging.getLogger(__name__)
  8. import re
  9. import warnings
  10. # site
  11. # pkg
  12. from passlib import hash
  13. from passlib.utils import repeat_string
  14. from passlib.utils.compat import u
  15. from passlib.tests.utils import TestCase, HandlerCase, skipUnless, SkipTest
  16. from passlib.tests.test_handlers import UPASS_USD, UPASS_TABLE
  17. from passlib.tests.test_ext_django import DJANGO_VERSION, MIN_DJANGO_VERSION, \
  18. check_django_hasher_has_backend
  19. # module
  20. #=============================================================================
  21. # django
  22. #=============================================================================
  23. # standard string django uses
  24. UPASS_LETMEIN = u('l\xe8tmein')
  25. def vstr(version):
  26. return ".".join(str(e) for e in version)
  27. class _DjangoHelper(TestCase):
  28. """
  29. mixin for HandlerCase subclasses that are testing a hasher
  30. which is also present in django.
  31. """
  32. __unittest_skip = True
  33. #: minimum django version where hash alg is present / that we support testing against
  34. min_django_version = MIN_DJANGO_VERSION
  35. #: max django version where hash alg is present
  36. #: TODO: for a bunch of the tests below, this is just max version where
  37. #: settings.PASSWORD_HASHERS includes it by default -- could add helper to patch
  38. #: desired django hasher back in for duration of test.
  39. #: XXX: change this to "disabled_in_django_version" instead?
  40. max_django_version = None
  41. def _require_django_support(self):
  42. # make sure min django version
  43. if DJANGO_VERSION < self.min_django_version:
  44. raise self.skipTest("Django >= %s not installed" % vstr(self.min_django_version))
  45. if self.max_django_version and DJANGO_VERSION > self.max_django_version:
  46. raise self.skipTest("Django <= %s not installed" % vstr(self.max_django_version))
  47. # make sure django has a backend for specified hasher
  48. name = self.handler.django_name
  49. if not check_django_hasher_has_backend(name):
  50. raise self.skipTest('django hasher %r not available' % name)
  51. return True
  52. extra_fuzz_verifiers = HandlerCase.fuzz_verifiers + (
  53. "fuzz_verifier_django",
  54. )
  55. def fuzz_verifier_django(self):
  56. try:
  57. self._require_django_support()
  58. except SkipTest:
  59. return None
  60. from django.contrib.auth.hashers import check_password
  61. def verify_django(secret, hash):
  62. """django/check_password"""
  63. if self.handler.name == "django_bcrypt" and hash.startswith("bcrypt$$2y$"):
  64. hash = hash.replace("$$2y$", "$$2a$")
  65. if isinstance(secret, bytes):
  66. secret = secret.decode("utf-8")
  67. return check_password(secret, hash)
  68. return verify_django
  69. def test_90_django_reference(self):
  70. """run known correct hashes through Django's check_password()"""
  71. self._require_django_support()
  72. # XXX: esp. when it's no longer supported by django,
  73. # should verify it's *NOT* recognized
  74. from django.contrib.auth.hashers import check_password
  75. assert self.known_correct_hashes
  76. for secret, hash in self.iter_known_hashes():
  77. self.assertTrue(check_password(secret, hash),
  78. "secret=%r hash=%r failed to verify" %
  79. (secret, hash))
  80. self.assertFalse(check_password('x' + secret, hash),
  81. "mangled secret=%r hash=%r incorrect verified" %
  82. (secret, hash))
  83. def test_91_django_generation(self):
  84. """test against output of Django's make_password()"""
  85. self._require_django_support()
  86. # XXX: esp. when it's no longer supported by django,
  87. # should verify it's *NOT* recognized
  88. from passlib.utils import tick
  89. from django.contrib.auth.hashers import make_password
  90. name = self.handler.django_name # set for all the django_* handlers
  91. end = tick() + self.max_fuzz_time/2
  92. generator = self.FuzzHashGenerator(self, self.getRandom())
  93. while tick() < end:
  94. secret, other = generator.random_password_pair()
  95. if not secret: # django rejects empty passwords.
  96. continue
  97. if isinstance(secret, bytes):
  98. secret = secret.decode("utf-8")
  99. hash = make_password(secret, hasher=name)
  100. self.assertTrue(self.do_identify(hash))
  101. self.assertTrue(self.do_verify(secret, hash))
  102. self.assertFalse(self.do_verify(other, hash))
  103. class django_disabled_test(HandlerCase):
  104. """test django_disabled"""
  105. handler = hash.django_disabled
  106. disabled_contains_salt = True
  107. known_correct_hashes = [
  108. # *everything* should hash to "!", and nothing should verify
  109. ("password", "!"),
  110. ("", "!"),
  111. (UPASS_TABLE, "!"),
  112. ]
  113. known_alternate_hashes = [
  114. # django 1.6 appends random alpnum string
  115. ("!9wa845vn7098ythaehasldkfj", "password", "!"),
  116. ]
  117. class django_des_crypt_test(HandlerCase, _DjangoHelper):
  118. """test django_des_crypt"""
  119. handler = hash.django_des_crypt
  120. max_django_version = (1,9)
  121. known_correct_hashes = [
  122. # ensures only first two digits of salt count.
  123. ("password", 'crypt$c2$c2M87q...WWcU'),
  124. ("password", 'crypt$c2e86$c2M87q...WWcU'),
  125. ("passwordignoreme", 'crypt$c2.AZ$c2M87q...WWcU'),
  126. # ensures utf-8 used for unicode
  127. (UPASS_USD, 'crypt$c2e86$c2hN1Bxd6ZiWs'),
  128. (UPASS_TABLE, 'crypt$0.aQs$0.wB.TT0Czvlo'),
  129. (u("hell\u00D6"), "crypt$sa$saykDgk3BPZ9E"),
  130. # prevent regression of issue 22
  131. ("foo", 'crypt$MNVY.9ajgdvDQ$MNVY.9ajgdvDQ'),
  132. ]
  133. known_alternate_hashes = [
  134. # ensure django 1.4 empty salt field is accepted;
  135. # but that salt field is re-filled (for django 1.0 compatibility)
  136. ('crypt$$c2M87q...WWcU', "password", 'crypt$c2$c2M87q...WWcU'),
  137. ]
  138. known_unidentified_hashes = [
  139. 'sha1$aa$bb',
  140. ]
  141. known_malformed_hashes = [
  142. # checksum too short
  143. 'crypt$c2$c2M87q',
  144. # salt must be >2
  145. 'crypt$f$c2M87q...WWcU',
  146. # make sure first 2 chars of salt & chk field agree.
  147. 'crypt$ffe86$c2M87q...WWcU',
  148. ]
  149. class django_salted_md5_test(HandlerCase, _DjangoHelper):
  150. """test django_salted_md5"""
  151. handler = hash.django_salted_md5
  152. max_django_version = (1,9)
  153. known_correct_hashes = [
  154. # test extra large salt
  155. ("password", 'md5$123abcdef$c8272612932975ee80e8a35995708e80'),
  156. # test django 1.4 alphanumeric salt
  157. ("test", 'md5$3OpqnFAHW5CT$54b29300675271049a1ebae07b395e20'),
  158. # ensures utf-8 used for unicode
  159. (UPASS_USD, 'md5$c2e86$92105508419a81a6babfaecf876a2fa0'),
  160. (UPASS_TABLE, 'md5$d9eb8$01495b32852bffb27cf5d4394fe7a54c'),
  161. ]
  162. known_unidentified_hashes = [
  163. 'sha1$aa$bb',
  164. ]
  165. known_malformed_hashes = [
  166. # checksum too short
  167. 'md5$aa$bb',
  168. ]
  169. class FuzzHashGenerator(HandlerCase.FuzzHashGenerator):
  170. def random_salt_size(self):
  171. # workaround for django14 regression --
  172. # 1.4 won't accept hashes with empty salt strings, unlike 1.3 and earlier.
  173. # looks to be fixed in a future release -- https://code.djangoproject.com/ticket/18144
  174. # for now, we avoid salt_size==0 under 1.4
  175. handler = self.handler
  176. default = handler.default_salt_size
  177. assert handler.min_salt_size == 0
  178. lower = 1
  179. upper = handler.max_salt_size or default*4
  180. return self.randintgauss(lower, upper, default, default*.5)
  181. class django_salted_sha1_test(HandlerCase, _DjangoHelper):
  182. """test django_salted_sha1"""
  183. handler = hash.django_salted_sha1
  184. max_django_version = (1,9)
  185. known_correct_hashes = [
  186. # test extra large salt
  187. ("password",'sha1$123abcdef$e4a1877b0e35c47329e7ed7e58014276168a37ba'),
  188. # test django 1.4 alphanumeric salt
  189. ("test", 'sha1$bcwHF9Hy8lxS$6b4cfa0651b43161c6f1471ce9523acf1f751ba3'),
  190. # ensures utf-8 used for unicode
  191. (UPASS_USD, 'sha1$c2e86$0f75c5d7fbd100d587c127ef0b693cde611b4ada'),
  192. (UPASS_TABLE, 'sha1$6d853$ef13a4d8fb57aed0cb573fe9c82e28dc7fd372d4'),
  193. # generic password
  194. ("MyPassword", 'sha1$54123$893cf12e134c3c215f3a76bd50d13f92404a54d3'),
  195. ]
  196. known_unidentified_hashes = [
  197. 'md5$aa$bb',
  198. ]
  199. known_malformed_hashes = [
  200. # checksum too short
  201. 'sha1$c2e86$0f75',
  202. ]
  203. # reuse custom random_salt_size() helper...
  204. FuzzHashGenerator = django_salted_md5_test.FuzzHashGenerator
  205. class django_pbkdf2_sha256_test(HandlerCase, _DjangoHelper):
  206. """test django_pbkdf2_sha256"""
  207. handler = hash.django_pbkdf2_sha256
  208. known_correct_hashes = [
  209. #
  210. # custom - generated via django 1.4 hasher
  211. #
  212. ('not a password',
  213. 'pbkdf2_sha256$10000$kjVJaVz6qsnJ$5yPHw3rwJGECpUf70daLGhOrQ5+AMxIJdz1c3bqK1Rs='),
  214. (UPASS_TABLE,
  215. 'pbkdf2_sha256$10000$bEwAfNrH1TlQ$OgYUblFNUX1B8GfMqaCYUK/iHyO0pa7STTDdaEJBuY0='),
  216. ]
  217. class django_pbkdf2_sha1_test(HandlerCase, _DjangoHelper):
  218. """test django_pbkdf2_sha1"""
  219. handler = hash.django_pbkdf2_sha1
  220. known_correct_hashes = [
  221. #
  222. # custom - generated via django 1.4 hashers
  223. #
  224. ('not a password',
  225. 'pbkdf2_sha1$10000$wz5B6WkasRoF$atJmJ1o+XfJxKq1+Nu1f1i57Z5I='),
  226. (UPASS_TABLE,
  227. 'pbkdf2_sha1$10000$KZKWwvqb8BfL$rw5pWsxJEU4JrZAQhHTCO+u0f5Y='),
  228. ]
  229. @skipUnless(hash.bcrypt.has_backend(), "no bcrypt backends available")
  230. class django_bcrypt_test(HandlerCase, _DjangoHelper):
  231. """test django_bcrypt"""
  232. handler = hash.django_bcrypt
  233. # XXX: not sure when this wasn't in default list anymore. somewhere in [2.0 - 2.2]
  234. max_django_version = (2, 0)
  235. fuzz_salts_need_bcrypt_repair = True
  236. known_correct_hashes = [
  237. #
  238. # just copied and adapted a few test vectors from bcrypt (above),
  239. # since django_bcrypt is just a wrapper for the real bcrypt class.
  240. #
  241. ('', 'bcrypt$$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.'),
  242. ('abcdefghijklmnopqrstuvwxyz',
  243. 'bcrypt$$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq'),
  244. (UPASS_TABLE,
  245. 'bcrypt$$2a$05$Z17AXnnlpzddNUvnC6cZNOSwMA/8oNiKnHTHTwLlBijfucQQlHjaG'),
  246. ]
  247. # NOTE: the following have been cloned from _bcrypt_test()
  248. def populate_settings(self, kwds):
  249. # speed up test w/ lower rounds
  250. kwds.setdefault("rounds", 4)
  251. super(django_bcrypt_test, self).populate_settings(kwds)
  252. class FuzzHashGenerator(HandlerCase.FuzzHashGenerator):
  253. def random_rounds(self):
  254. # decrease default rounds for fuzz testing to speed up volume.
  255. return self.randintgauss(5, 8, 6, 1)
  256. def random_ident(self):
  257. # omit multi-ident tests, only $2a$ counts for this class
  258. # XXX: enable this to check 2a / 2b?
  259. return None
  260. @skipUnless(hash.bcrypt.has_backend(), "no bcrypt backends available")
  261. class django_bcrypt_sha256_test(HandlerCase, _DjangoHelper):
  262. """test django_bcrypt_sha256"""
  263. handler = hash.django_bcrypt_sha256
  264. forbidden_characters = None
  265. fuzz_salts_need_bcrypt_repair = True
  266. known_correct_hashes = [
  267. #
  268. # custom - generated via django 1.6 hasher
  269. #
  270. ('',
  271. 'bcrypt_sha256$$2a$06$/3OeRpbOf8/l6nPPRdZPp.nRiyYqPobEZGdNRBWihQhiFDh1ws1tu'),
  272. (UPASS_LETMEIN,
  273. 'bcrypt_sha256$$2a$08$NDjSAIcas.EcoxCRiArvT.MkNiPYVhrsrnJsRkLueZOoV1bsQqlmC'),
  274. (UPASS_TABLE,
  275. 'bcrypt_sha256$$2a$06$kCXUnRFQptGg491siDKNTu8RxjBGSjALHRuvhPYNFsa4Ea5d9M48u'),
  276. # test >72 chars is hashed correctly -- under bcrypt these hash the same.
  277. (repeat_string("abc123",72),
  278. 'bcrypt_sha256$$2a$06$Tg/oYyZTyAf.Nb3qSgN61OySmyXA8FoY4PjGizjE1QSDfuL5MXNni'),
  279. (repeat_string("abc123",72)+"qwr",
  280. 'bcrypt_sha256$$2a$06$Tg/oYyZTyAf.Nb3qSgN61Ocy0BEz1RK6xslSNi8PlaLX2pe7x/KQG'),
  281. (repeat_string("abc123",72)+"xyz",
  282. 'bcrypt_sha256$$2a$06$Tg/oYyZTyAf.Nb3qSgN61OvY2zoRVUa2Pugv2ExVOUT2YmhvxUFUa'),
  283. ]
  284. known_malformed_hashers = [
  285. # data in django salt field
  286. 'bcrypt_sha256$xyz$2a$06$/3OeRpbOf8/l6nPPRdZPp.nRiyYqPobEZGdNRBWihQhiFDh1ws1tu',
  287. ]
  288. # NOTE: the following have been cloned from _bcrypt_test()
  289. def populate_settings(self, kwds):
  290. # speed up test w/ lower rounds
  291. kwds.setdefault("rounds", 4)
  292. super(django_bcrypt_sha256_test, self).populate_settings(kwds)
  293. class FuzzHashGenerator(HandlerCase.FuzzHashGenerator):
  294. def random_rounds(self):
  295. # decrease default rounds for fuzz testing to speed up volume.
  296. return self.randintgauss(5, 8, 6, 1)
  297. def random_ident(self):
  298. # omit multi-ident tests, only $2a$ counts for this class
  299. # XXX: enable this to check 2a / 2b?
  300. return None
  301. from passlib.tests.test_handlers_argon2 import _base_argon2_test
  302. @skipUnless(hash.argon2.has_backend(), "no argon2 backends available")
  303. class django_argon2_test(HandlerCase, _DjangoHelper):
  304. """test django_bcrypt"""
  305. handler = hash.django_argon2
  306. # NOTE: most of this adapted from _base_argon2_test & argon2pure test
  307. known_correct_hashes = [
  308. # sample test
  309. ("password", 'argon2$argon2i$v=19$m=256,t=1,p=1$c29tZXNhbHQ$AJFIsNZTMKTAewB4+ETN1A'),
  310. # sample w/ all parameters different
  311. ("password", 'argon2$argon2i$v=19$m=380,t=2,p=2$c29tZXNhbHQ$SrssP8n7m/12VWPM8dvNrw'),
  312. # generated from django 1.10.3
  313. (UPASS_LETMEIN, 'argon2$argon2i$v=19$m=512,t=2,p=2$V25jN1l4UUJZWkR1$MxpA1BD2Gh7+D79gaAw6sQ'),
  314. ]
  315. def setUpWarnings(self):
  316. super(django_argon2_test, self).setUpWarnings()
  317. warnings.filterwarnings("ignore", ".*Using argon2pure backend.*")
  318. def do_stub_encrypt(self, handler=None, **settings):
  319. # overriding default since no way to get stub config from argon2._calc_hash()
  320. # (otherwise test_21b_max_rounds blocks trying to do max rounds)
  321. handler = (handler or self.handler).using(**settings)
  322. self = handler.wrapped(use_defaults=True)
  323. self.checksum = self._stub_checksum
  324. assert self.checksum
  325. return handler._wrap_hash(self.to_string())
  326. def test_03_legacy_hash_workflow(self):
  327. # override base method
  328. raise self.skipTest("legacy 1.6 workflow not supported")
  329. class FuzzHashGenerator(_base_argon2_test.FuzzHashGenerator):
  330. def random_type(self):
  331. # override default since django only uses type I (see note in class)
  332. return "I"
  333. def random_rounds(self):
  334. # decrease default rounds for fuzz testing to speed up volume.
  335. return self.randintgauss(1, 3, 2, 1)
  336. #=============================================================================
  337. # eof
  338. #=============================================================================