test_hashers.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871
  1. from unittest import mock, skipUnless
  2. from django.conf.global_settings import PASSWORD_HASHERS
  3. from django.contrib.auth.hashers import (
  4. UNUSABLE_PASSWORD_PREFIX,
  5. UNUSABLE_PASSWORD_SUFFIX_LENGTH,
  6. BasePasswordHasher,
  7. BCryptPasswordHasher,
  8. BCryptSHA256PasswordHasher,
  9. MD5PasswordHasher,
  10. PBKDF2PasswordHasher,
  11. PBKDF2SHA1PasswordHasher,
  12. ScryptPasswordHasher,
  13. check_password,
  14. get_hasher,
  15. identify_hasher,
  16. is_password_usable,
  17. make_password,
  18. )
  19. from django.test import SimpleTestCase, ignore_warnings
  20. from django.test.utils import override_settings
  21. from django.utils.deprecation import RemovedInDjango50Warning, RemovedInDjango51Warning
  22. # RemovedInDjango50Warning.
  23. try:
  24. import crypt
  25. except ImportError:
  26. crypt = None
  27. else:
  28. # On some platforms (e.g. OpenBSD), crypt.crypt() always return None.
  29. if crypt.crypt("") is None:
  30. crypt = None
  31. try:
  32. import bcrypt
  33. except ImportError:
  34. bcrypt = None
  35. try:
  36. import argon2
  37. except ImportError:
  38. argon2 = None
  39. class PBKDF2SingleIterationHasher(PBKDF2PasswordHasher):
  40. iterations = 1
  41. @override_settings(PASSWORD_HASHERS=PASSWORD_HASHERS)
  42. class TestUtilsHashPass(SimpleTestCase):
  43. def test_simple(self):
  44. encoded = make_password("lètmein")
  45. self.assertTrue(encoded.startswith("pbkdf2_sha256$"))
  46. self.assertTrue(is_password_usable(encoded))
  47. self.assertTrue(check_password("lètmein", encoded))
  48. self.assertFalse(check_password("lètmeinz", encoded))
  49. # Blank passwords
  50. blank_encoded = make_password("")
  51. self.assertTrue(blank_encoded.startswith("pbkdf2_sha256$"))
  52. self.assertTrue(is_password_usable(blank_encoded))
  53. self.assertTrue(check_password("", blank_encoded))
  54. self.assertFalse(check_password(" ", blank_encoded))
  55. def test_bytes(self):
  56. encoded = make_password(b"bytes_password")
  57. self.assertTrue(encoded.startswith("pbkdf2_sha256$"))
  58. self.assertIs(is_password_usable(encoded), True)
  59. self.assertIs(check_password(b"bytes_password", encoded), True)
  60. def test_invalid_password(self):
  61. msg = "Password must be a string or bytes, got int."
  62. with self.assertRaisesMessage(TypeError, msg):
  63. make_password(1)
  64. def test_pbkdf2(self):
  65. encoded = make_password("lètmein", "seasalt", "pbkdf2_sha256")
  66. self.assertEqual(
  67. encoded,
  68. "pbkdf2_sha256$480000$seasalt$G4ja8YRtfnNyEx4Ii2pbFMp/l8s4nnbMdJ+Fob/qNK8=",
  69. )
  70. self.assertTrue(is_password_usable(encoded))
  71. self.assertTrue(check_password("lètmein", encoded))
  72. self.assertFalse(check_password("lètmeinz", encoded))
  73. self.assertEqual(identify_hasher(encoded).algorithm, "pbkdf2_sha256")
  74. # Blank passwords
  75. blank_encoded = make_password("", "seasalt", "pbkdf2_sha256")
  76. self.assertTrue(blank_encoded.startswith("pbkdf2_sha256$"))
  77. self.assertTrue(is_password_usable(blank_encoded))
  78. self.assertTrue(check_password("", blank_encoded))
  79. self.assertFalse(check_password(" ", blank_encoded))
  80. # Salt entropy check.
  81. hasher = get_hasher("pbkdf2_sha256")
  82. encoded_weak_salt = make_password("lètmein", "iodizedsalt", "pbkdf2_sha256")
  83. encoded_strong_salt = make_password("lètmein", hasher.salt(), "pbkdf2_sha256")
  84. self.assertIs(hasher.must_update(encoded_weak_salt), True)
  85. self.assertIs(hasher.must_update(encoded_strong_salt), False)
  86. @ignore_warnings(category=RemovedInDjango51Warning)
  87. @override_settings(
  88. PASSWORD_HASHERS=["django.contrib.auth.hashers.SHA1PasswordHasher"]
  89. )
  90. def test_sha1(self):
  91. encoded = make_password("lètmein", "seasalt", "sha1")
  92. self.assertEqual(
  93. encoded, "sha1$seasalt$cff36ea83f5706ce9aa7454e63e431fc726b2dc8"
  94. )
  95. self.assertTrue(is_password_usable(encoded))
  96. self.assertTrue(check_password("lètmein", encoded))
  97. self.assertFalse(check_password("lètmeinz", encoded))
  98. self.assertEqual(identify_hasher(encoded).algorithm, "sha1")
  99. # Blank passwords
  100. blank_encoded = make_password("", "seasalt", "sha1")
  101. self.assertTrue(blank_encoded.startswith("sha1$"))
  102. self.assertTrue(is_password_usable(blank_encoded))
  103. self.assertTrue(check_password("", blank_encoded))
  104. self.assertFalse(check_password(" ", blank_encoded))
  105. # Salt entropy check.
  106. hasher = get_hasher("sha1")
  107. encoded_weak_salt = make_password("lètmein", "iodizedsalt", "sha1")
  108. encoded_strong_salt = make_password("lètmein", hasher.salt(), "sha1")
  109. self.assertIs(hasher.must_update(encoded_weak_salt), True)
  110. self.assertIs(hasher.must_update(encoded_strong_salt), False)
  111. @override_settings(
  112. PASSWORD_HASHERS=["django.contrib.auth.hashers.SHA1PasswordHasher"]
  113. )
  114. def test_sha1_deprecation_warning(self):
  115. msg = "django.contrib.auth.hashers.SHA1PasswordHasher is deprecated."
  116. with self.assertRaisesMessage(RemovedInDjango51Warning, msg):
  117. get_hasher("sha1")
  118. @override_settings(
  119. PASSWORD_HASHERS=["django.contrib.auth.hashers.MD5PasswordHasher"]
  120. )
  121. def test_md5(self):
  122. encoded = make_password("lètmein", "seasalt", "md5")
  123. self.assertEqual(encoded, "md5$seasalt$3f86d0d3d465b7b458c231bf3555c0e3")
  124. self.assertTrue(is_password_usable(encoded))
  125. self.assertTrue(check_password("lètmein", encoded))
  126. self.assertFalse(check_password("lètmeinz", encoded))
  127. self.assertEqual(identify_hasher(encoded).algorithm, "md5")
  128. # Blank passwords
  129. blank_encoded = make_password("", "seasalt", "md5")
  130. self.assertTrue(blank_encoded.startswith("md5$"))
  131. self.assertTrue(is_password_usable(blank_encoded))
  132. self.assertTrue(check_password("", blank_encoded))
  133. self.assertFalse(check_password(" ", blank_encoded))
  134. # Salt entropy check.
  135. hasher = get_hasher("md5")
  136. encoded_weak_salt = make_password("lètmein", "iodizedsalt", "md5")
  137. encoded_strong_salt = make_password("lètmein", hasher.salt(), "md5")
  138. self.assertIs(hasher.must_update(encoded_weak_salt), True)
  139. self.assertIs(hasher.must_update(encoded_strong_salt), False)
  140. @ignore_warnings(category=RemovedInDjango51Warning)
  141. @override_settings(
  142. PASSWORD_HASHERS=["django.contrib.auth.hashers.UnsaltedMD5PasswordHasher"]
  143. )
  144. def test_unsalted_md5(self):
  145. encoded = make_password("lètmein", "", "unsalted_md5")
  146. self.assertEqual(encoded, "88a434c88cca4e900f7874cd98123f43")
  147. self.assertTrue(is_password_usable(encoded))
  148. self.assertTrue(check_password("lètmein", encoded))
  149. self.assertFalse(check_password("lètmeinz", encoded))
  150. self.assertEqual(identify_hasher(encoded).algorithm, "unsalted_md5")
  151. # Alternate unsalted syntax
  152. alt_encoded = "md5$$%s" % encoded
  153. self.assertTrue(is_password_usable(alt_encoded))
  154. self.assertTrue(check_password("lètmein", alt_encoded))
  155. self.assertFalse(check_password("lètmeinz", alt_encoded))
  156. # Blank passwords
  157. blank_encoded = make_password("", "", "unsalted_md5")
  158. self.assertTrue(is_password_usable(blank_encoded))
  159. self.assertTrue(check_password("", blank_encoded))
  160. self.assertFalse(check_password(" ", blank_encoded))
  161. @ignore_warnings(category=RemovedInDjango51Warning)
  162. @override_settings(
  163. PASSWORD_HASHERS=["django.contrib.auth.hashers.UnsaltedMD5PasswordHasher"]
  164. )
  165. def test_unsalted_md5_encode_invalid_salt(self):
  166. hasher = get_hasher("unsalted_md5")
  167. msg = "salt must be empty."
  168. with self.assertRaisesMessage(ValueError, msg):
  169. hasher.encode("password", salt="salt")
  170. @override_settings(
  171. PASSWORD_HASHERS=["django.contrib.auth.hashers.UnsaltedMD5PasswordHasher"]
  172. )
  173. def test_unsalted_md5_deprecation_warning(self):
  174. msg = "django.contrib.auth.hashers.UnsaltedMD5PasswordHasher is deprecated."
  175. with self.assertRaisesMessage(RemovedInDjango51Warning, msg):
  176. get_hasher("unsalted_md5")
  177. @ignore_warnings(category=RemovedInDjango51Warning)
  178. @override_settings(
  179. PASSWORD_HASHERS=["django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher"]
  180. )
  181. def test_unsalted_sha1(self):
  182. encoded = make_password("lètmein", "", "unsalted_sha1")
  183. self.assertEqual(encoded, "sha1$$6d138ca3ae545631b3abd71a4f076ce759c5700b")
  184. self.assertTrue(is_password_usable(encoded))
  185. self.assertTrue(check_password("lètmein", encoded))
  186. self.assertFalse(check_password("lètmeinz", encoded))
  187. self.assertEqual(identify_hasher(encoded).algorithm, "unsalted_sha1")
  188. # Raw SHA1 isn't acceptable
  189. alt_encoded = encoded[6:]
  190. self.assertFalse(check_password("lètmein", alt_encoded))
  191. # Blank passwords
  192. blank_encoded = make_password("", "", "unsalted_sha1")
  193. self.assertTrue(blank_encoded.startswith("sha1$"))
  194. self.assertTrue(is_password_usable(blank_encoded))
  195. self.assertTrue(check_password("", blank_encoded))
  196. self.assertFalse(check_password(" ", blank_encoded))
  197. @ignore_warnings(category=RemovedInDjango51Warning)
  198. @override_settings(
  199. PASSWORD_HASHERS=["django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher"]
  200. )
  201. def test_unsalted_sha1_encode_invalid_salt(self):
  202. hasher = get_hasher("unsalted_sha1")
  203. msg = "salt must be empty."
  204. with self.assertRaisesMessage(ValueError, msg):
  205. hasher.encode("password", salt="salt")
  206. @override_settings(
  207. PASSWORD_HASHERS=["django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher"]
  208. )
  209. def test_unsalted_sha1_deprecation_warning(self):
  210. msg = "django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher is deprecated."
  211. with self.assertRaisesMessage(RemovedInDjango51Warning, msg):
  212. get_hasher("unsalted_sha1")
  213. @ignore_warnings(category=RemovedInDjango50Warning)
  214. @skipUnless(crypt, "no crypt module to generate password.")
  215. @override_settings(
  216. PASSWORD_HASHERS=["django.contrib.auth.hashers.CryptPasswordHasher"]
  217. )
  218. def test_crypt(self):
  219. encoded = make_password("lètmei", "ab", "crypt")
  220. self.assertEqual(encoded, "crypt$$ab1Hv2Lg7ltQo")
  221. self.assertTrue(is_password_usable(encoded))
  222. self.assertTrue(check_password("lètmei", encoded))
  223. self.assertFalse(check_password("lètmeiz", encoded))
  224. self.assertEqual(identify_hasher(encoded).algorithm, "crypt")
  225. # Blank passwords
  226. blank_encoded = make_password("", "ab", "crypt")
  227. self.assertTrue(blank_encoded.startswith("crypt$"))
  228. self.assertTrue(is_password_usable(blank_encoded))
  229. self.assertTrue(check_password("", blank_encoded))
  230. self.assertFalse(check_password(" ", blank_encoded))
  231. @ignore_warnings(category=RemovedInDjango50Warning)
  232. @skipUnless(crypt, "no crypt module to generate password.")
  233. @override_settings(
  234. PASSWORD_HASHERS=["django.contrib.auth.hashers.CryptPasswordHasher"]
  235. )
  236. def test_crypt_encode_invalid_salt(self):
  237. hasher = get_hasher("crypt")
  238. msg = "salt must be of length 2."
  239. with self.assertRaisesMessage(ValueError, msg):
  240. hasher.encode("password", salt="a")
  241. @ignore_warnings(category=RemovedInDjango50Warning)
  242. @skipUnless(crypt, "no crypt module to generate password.")
  243. @override_settings(
  244. PASSWORD_HASHERS=["django.contrib.auth.hashers.CryptPasswordHasher"]
  245. )
  246. def test_crypt_encode_invalid_hash(self):
  247. hasher = get_hasher("crypt")
  248. msg = "hash must be provided."
  249. with mock.patch("crypt.crypt", return_value=None):
  250. with self.assertRaisesMessage(TypeError, msg):
  251. hasher.encode("password", salt="ab")
  252. @skipUnless(crypt, "no crypt module to generate password.")
  253. @override_settings(
  254. PASSWORD_HASHERS=["django.contrib.auth.hashers.CryptPasswordHasher"]
  255. )
  256. def test_crypt_deprecation_warning(self):
  257. msg = "django.contrib.auth.hashers.CryptPasswordHasher is deprecated."
  258. with self.assertRaisesMessage(RemovedInDjango50Warning, msg):
  259. get_hasher("crypt")
  260. @skipUnless(bcrypt, "bcrypt not installed")
  261. def test_bcrypt_sha256(self):
  262. encoded = make_password("lètmein", hasher="bcrypt_sha256")
  263. self.assertTrue(is_password_usable(encoded))
  264. self.assertTrue(encoded.startswith("bcrypt_sha256$"))
  265. self.assertTrue(check_password("lètmein", encoded))
  266. self.assertFalse(check_password("lètmeinz", encoded))
  267. self.assertEqual(identify_hasher(encoded).algorithm, "bcrypt_sha256")
  268. # password truncation no longer works
  269. password = (
  270. "VSK0UYV6FFQVZ0KG88DYN9WADAADZO1CTSIVDJUNZSUML6IBX7LN7ZS3R5"
  271. "JGB3RGZ7VI7G7DJQ9NI8BQFSRPTG6UWTTVESA5ZPUN"
  272. )
  273. encoded = make_password(password, hasher="bcrypt_sha256")
  274. self.assertTrue(check_password(password, encoded))
  275. self.assertFalse(check_password(password[:72], encoded))
  276. # Blank passwords
  277. blank_encoded = make_password("", hasher="bcrypt_sha256")
  278. self.assertTrue(blank_encoded.startswith("bcrypt_sha256$"))
  279. self.assertTrue(is_password_usable(blank_encoded))
  280. self.assertTrue(check_password("", blank_encoded))
  281. self.assertFalse(check_password(" ", blank_encoded))
  282. @skipUnless(bcrypt, "bcrypt not installed")
  283. @override_settings(
  284. PASSWORD_HASHERS=["django.contrib.auth.hashers.BCryptPasswordHasher"]
  285. )
  286. def test_bcrypt(self):
  287. encoded = make_password("lètmein", hasher="bcrypt")
  288. self.assertTrue(is_password_usable(encoded))
  289. self.assertTrue(encoded.startswith("bcrypt$"))
  290. self.assertTrue(check_password("lètmein", encoded))
  291. self.assertFalse(check_password("lètmeinz", encoded))
  292. self.assertEqual(identify_hasher(encoded).algorithm, "bcrypt")
  293. # Blank passwords
  294. blank_encoded = make_password("", hasher="bcrypt")
  295. self.assertTrue(blank_encoded.startswith("bcrypt$"))
  296. self.assertTrue(is_password_usable(blank_encoded))
  297. self.assertTrue(check_password("", blank_encoded))
  298. self.assertFalse(check_password(" ", blank_encoded))
  299. @skipUnless(bcrypt, "bcrypt not installed")
  300. @override_settings(
  301. PASSWORD_HASHERS=["django.contrib.auth.hashers.BCryptPasswordHasher"]
  302. )
  303. def test_bcrypt_upgrade(self):
  304. hasher = get_hasher("bcrypt")
  305. self.assertEqual("bcrypt", hasher.algorithm)
  306. self.assertNotEqual(hasher.rounds, 4)
  307. old_rounds = hasher.rounds
  308. try:
  309. # Generate a password with 4 rounds.
  310. hasher.rounds = 4
  311. encoded = make_password("letmein", hasher="bcrypt")
  312. rounds = hasher.safe_summary(encoded)["work factor"]
  313. self.assertEqual(rounds, 4)
  314. state = {"upgraded": False}
  315. def setter(password):
  316. state["upgraded"] = True
  317. # No upgrade is triggered.
  318. self.assertTrue(check_password("letmein", encoded, setter, "bcrypt"))
  319. self.assertFalse(state["upgraded"])
  320. # Revert to the old rounds count and ...
  321. hasher.rounds = old_rounds
  322. # ... check if the password would get updated to the new count.
  323. self.assertTrue(check_password("letmein", encoded, setter, "bcrypt"))
  324. self.assertTrue(state["upgraded"])
  325. finally:
  326. hasher.rounds = old_rounds
  327. @skipUnless(bcrypt, "bcrypt not installed")
  328. @override_settings(
  329. PASSWORD_HASHERS=["django.contrib.auth.hashers.BCryptPasswordHasher"]
  330. )
  331. def test_bcrypt_harden_runtime(self):
  332. hasher = get_hasher("bcrypt")
  333. self.assertEqual("bcrypt", hasher.algorithm)
  334. with mock.patch.object(hasher, "rounds", 4):
  335. encoded = make_password("letmein", hasher="bcrypt")
  336. with mock.patch.object(hasher, "rounds", 6), mock.patch.object(
  337. hasher, "encode", side_effect=hasher.encode
  338. ):
  339. hasher.harden_runtime("wrong_password", encoded)
  340. # Increasing rounds from 4 to 6 means an increase of 4 in workload,
  341. # therefore hardening should run 3 times to make the timing the
  342. # same (the original encode() call already ran once).
  343. self.assertEqual(hasher.encode.call_count, 3)
  344. # Get the original salt (includes the original workload factor)
  345. algorithm, data = encoded.split("$", 1)
  346. expected_call = (("wrong_password", data[:29].encode()),)
  347. self.assertEqual(hasher.encode.call_args_list, [expected_call] * 3)
  348. def test_unusable(self):
  349. encoded = make_password(None)
  350. self.assertEqual(
  351. len(encoded),
  352. len(UNUSABLE_PASSWORD_PREFIX) + UNUSABLE_PASSWORD_SUFFIX_LENGTH,
  353. )
  354. self.assertFalse(is_password_usable(encoded))
  355. self.assertFalse(check_password(None, encoded))
  356. self.assertFalse(check_password(encoded, encoded))
  357. self.assertFalse(check_password(UNUSABLE_PASSWORD_PREFIX, encoded))
  358. self.assertFalse(check_password("", encoded))
  359. self.assertFalse(check_password("lètmein", encoded))
  360. self.assertFalse(check_password("lètmeinz", encoded))
  361. with self.assertRaisesMessage(ValueError, "Unknown password hashing algorith"):
  362. identify_hasher(encoded)
  363. # Assert that the unusable passwords actually contain a random part.
  364. # This might fail one day due to a hash collision.
  365. self.assertNotEqual(encoded, make_password(None), "Random password collision?")
  366. def test_unspecified_password(self):
  367. """
  368. Makes sure specifying no plain password with a valid encoded password
  369. returns `False`.
  370. """
  371. self.assertFalse(check_password(None, make_password("lètmein")))
  372. def test_bad_algorithm(self):
  373. msg = (
  374. "Unknown password hashing algorithm '%s'. Did you specify it in "
  375. "the PASSWORD_HASHERS setting?"
  376. )
  377. with self.assertRaisesMessage(ValueError, msg % "lolcat"):
  378. make_password("lètmein", hasher="lolcat")
  379. with self.assertRaisesMessage(ValueError, msg % "lolcat"):
  380. identify_hasher("lolcat$salt$hash")
  381. def test_is_password_usable(self):
  382. passwords = ("lètmein_badencoded", "", None)
  383. for password in passwords:
  384. with self.subTest(password=password):
  385. self.assertIs(is_password_usable(password), True)
  386. def test_low_level_pbkdf2(self):
  387. hasher = PBKDF2PasswordHasher()
  388. encoded = hasher.encode("lètmein", "seasalt2")
  389. self.assertEqual(
  390. encoded,
  391. "pbkdf2_sha256$480000$seasalt2$WlORJKPl5w3Lubr7rYLOwSQCEOm4Or/NCA"
  392. "aECnB1PE0=",
  393. )
  394. self.assertTrue(hasher.verify("lètmein", encoded))
  395. def test_low_level_pbkdf2_sha1(self):
  396. hasher = PBKDF2SHA1PasswordHasher()
  397. encoded = hasher.encode("lètmein", "seasalt2")
  398. self.assertEqual(
  399. encoded, "pbkdf2_sha1$480000$seasalt2$qyT+EkK5g82hk2r+fRecFeoe28E="
  400. )
  401. self.assertTrue(hasher.verify("lètmein", encoded))
  402. @skipUnless(bcrypt, "bcrypt not installed")
  403. def test_bcrypt_salt_check(self):
  404. hasher = BCryptPasswordHasher()
  405. encoded = hasher.encode("lètmein", hasher.salt())
  406. self.assertIs(hasher.must_update(encoded), False)
  407. @skipUnless(bcrypt, "bcrypt not installed")
  408. def test_bcryptsha256_salt_check(self):
  409. hasher = BCryptSHA256PasswordHasher()
  410. encoded = hasher.encode("lètmein", hasher.salt())
  411. self.assertIs(hasher.must_update(encoded), False)
  412. @override_settings(
  413. PASSWORD_HASHERS=[
  414. "django.contrib.auth.hashers.PBKDF2PasswordHasher",
  415. "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
  416. "django.contrib.auth.hashers.MD5PasswordHasher",
  417. ],
  418. )
  419. def test_upgrade(self):
  420. self.assertEqual("pbkdf2_sha256", get_hasher("default").algorithm)
  421. for algo in ("pbkdf2_sha1", "md5"):
  422. with self.subTest(algo=algo):
  423. encoded = make_password("lètmein", hasher=algo)
  424. state = {"upgraded": False}
  425. def setter(password):
  426. state["upgraded"] = True
  427. self.assertTrue(check_password("lètmein", encoded, setter))
  428. self.assertTrue(state["upgraded"])
  429. def test_no_upgrade(self):
  430. encoded = make_password("lètmein")
  431. state = {"upgraded": False}
  432. def setter():
  433. state["upgraded"] = True
  434. self.assertFalse(check_password("WRONG", encoded, setter))
  435. self.assertFalse(state["upgraded"])
  436. @override_settings(
  437. PASSWORD_HASHERS=[
  438. "django.contrib.auth.hashers.PBKDF2PasswordHasher",
  439. "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
  440. "django.contrib.auth.hashers.MD5PasswordHasher",
  441. ],
  442. )
  443. def test_no_upgrade_on_incorrect_pass(self):
  444. self.assertEqual("pbkdf2_sha256", get_hasher("default").algorithm)
  445. for algo in ("pbkdf2_sha1", "md5"):
  446. with self.subTest(algo=algo):
  447. encoded = make_password("lètmein", hasher=algo)
  448. state = {"upgraded": False}
  449. def setter():
  450. state["upgraded"] = True
  451. self.assertFalse(check_password("WRONG", encoded, setter))
  452. self.assertFalse(state["upgraded"])
  453. def test_pbkdf2_upgrade(self):
  454. hasher = get_hasher("default")
  455. self.assertEqual("pbkdf2_sha256", hasher.algorithm)
  456. self.assertNotEqual(hasher.iterations, 1)
  457. old_iterations = hasher.iterations
  458. try:
  459. # Generate a password with 1 iteration.
  460. hasher.iterations = 1
  461. encoded = make_password("letmein")
  462. algo, iterations, salt, hash = encoded.split("$", 3)
  463. self.assertEqual(iterations, "1")
  464. state = {"upgraded": False}
  465. def setter(password):
  466. state["upgraded"] = True
  467. # No upgrade is triggered
  468. self.assertTrue(check_password("letmein", encoded, setter))
  469. self.assertFalse(state["upgraded"])
  470. # Revert to the old iteration count and ...
  471. hasher.iterations = old_iterations
  472. # ... check if the password would get updated to the new iteration count.
  473. self.assertTrue(check_password("letmein", encoded, setter))
  474. self.assertTrue(state["upgraded"])
  475. finally:
  476. hasher.iterations = old_iterations
  477. def test_pbkdf2_harden_runtime(self):
  478. hasher = get_hasher("default")
  479. self.assertEqual("pbkdf2_sha256", hasher.algorithm)
  480. with mock.patch.object(hasher, "iterations", 1):
  481. encoded = make_password("letmein")
  482. with mock.patch.object(hasher, "iterations", 6), mock.patch.object(
  483. hasher, "encode", side_effect=hasher.encode
  484. ):
  485. hasher.harden_runtime("wrong_password", encoded)
  486. # Encode should get called once ...
  487. self.assertEqual(hasher.encode.call_count, 1)
  488. # ... with the original salt and 5 iterations.
  489. algorithm, iterations, salt, hash = encoded.split("$", 3)
  490. expected_call = (("wrong_password", salt, 5),)
  491. self.assertEqual(hasher.encode.call_args, expected_call)
  492. def test_pbkdf2_upgrade_new_hasher(self):
  493. hasher = get_hasher("default")
  494. self.assertEqual("pbkdf2_sha256", hasher.algorithm)
  495. self.assertNotEqual(hasher.iterations, 1)
  496. state = {"upgraded": False}
  497. def setter(password):
  498. state["upgraded"] = True
  499. with self.settings(
  500. PASSWORD_HASHERS=["auth_tests.test_hashers.PBKDF2SingleIterationHasher"]
  501. ):
  502. encoded = make_password("letmein")
  503. algo, iterations, salt, hash = encoded.split("$", 3)
  504. self.assertEqual(iterations, "1")
  505. # No upgrade is triggered
  506. self.assertTrue(check_password("letmein", encoded, setter))
  507. self.assertFalse(state["upgraded"])
  508. # Revert to the old iteration count and check if the password would get
  509. # updated to the new iteration count.
  510. with self.settings(
  511. PASSWORD_HASHERS=[
  512. "django.contrib.auth.hashers.PBKDF2PasswordHasher",
  513. "auth_tests.test_hashers.PBKDF2SingleIterationHasher",
  514. ]
  515. ):
  516. self.assertTrue(check_password("letmein", encoded, setter))
  517. self.assertTrue(state["upgraded"])
  518. def test_check_password_calls_harden_runtime(self):
  519. hasher = get_hasher("default")
  520. encoded = make_password("letmein")
  521. with mock.patch.object(hasher, "harden_runtime"), mock.patch.object(
  522. hasher, "must_update", return_value=True
  523. ):
  524. # Correct password supplied, no hardening needed
  525. check_password("letmein", encoded)
  526. self.assertEqual(hasher.harden_runtime.call_count, 0)
  527. # Wrong password supplied, hardening needed
  528. check_password("wrong_password", encoded)
  529. self.assertEqual(hasher.harden_runtime.call_count, 1)
  530. def test_encode_invalid_salt(self):
  531. hasher_classes = [
  532. MD5PasswordHasher,
  533. PBKDF2PasswordHasher,
  534. PBKDF2SHA1PasswordHasher,
  535. ScryptPasswordHasher,
  536. ]
  537. msg = "salt must be provided and cannot contain $."
  538. for hasher_class in hasher_classes:
  539. hasher = hasher_class()
  540. for salt in [None, "", "sea$salt"]:
  541. with self.subTest(hasher_class.__name__, salt=salt):
  542. with self.assertRaisesMessage(ValueError, msg):
  543. hasher.encode("password", salt)
  544. def test_encode_password_required(self):
  545. hasher_classes = [
  546. MD5PasswordHasher,
  547. PBKDF2PasswordHasher,
  548. PBKDF2SHA1PasswordHasher,
  549. ScryptPasswordHasher,
  550. ]
  551. msg = "password must be provided."
  552. for hasher_class in hasher_classes:
  553. hasher = hasher_class()
  554. with self.subTest(hasher_class.__name__):
  555. with self.assertRaisesMessage(TypeError, msg):
  556. hasher.encode(None, "seasalt")
  557. class BasePasswordHasherTests(SimpleTestCase):
  558. not_implemented_msg = "subclasses of BasePasswordHasher must provide %s() method"
  559. def setUp(self):
  560. self.hasher = BasePasswordHasher()
  561. def test_load_library_no_algorithm(self):
  562. msg = "Hasher 'BasePasswordHasher' doesn't specify a library attribute"
  563. with self.assertRaisesMessage(ValueError, msg):
  564. self.hasher._load_library()
  565. def test_load_library_importerror(self):
  566. PlainHasher = type(
  567. "PlainHasher",
  568. (BasePasswordHasher,),
  569. {"algorithm": "plain", "library": "plain"},
  570. )
  571. msg = "Couldn't load 'PlainHasher' algorithm library: No module named 'plain'"
  572. with self.assertRaisesMessage(ValueError, msg):
  573. PlainHasher()._load_library()
  574. def test_attributes(self):
  575. self.assertIsNone(self.hasher.algorithm)
  576. self.assertIsNone(self.hasher.library)
  577. def test_encode(self):
  578. msg = self.not_implemented_msg % "an encode"
  579. with self.assertRaisesMessage(NotImplementedError, msg):
  580. self.hasher.encode("password", "salt")
  581. def test_decode(self):
  582. msg = self.not_implemented_msg % "a decode"
  583. with self.assertRaisesMessage(NotImplementedError, msg):
  584. self.hasher.decode("encoded")
  585. def test_harden_runtime(self):
  586. msg = (
  587. "subclasses of BasePasswordHasher should provide a harden_runtime() method"
  588. )
  589. with self.assertWarnsMessage(Warning, msg):
  590. self.hasher.harden_runtime("password", "encoded")
  591. def test_must_update(self):
  592. self.assertIs(self.hasher.must_update("encoded"), False)
  593. def test_safe_summary(self):
  594. msg = self.not_implemented_msg % "a safe_summary"
  595. with self.assertRaisesMessage(NotImplementedError, msg):
  596. self.hasher.safe_summary("encoded")
  597. def test_verify(self):
  598. msg = self.not_implemented_msg % "a verify"
  599. with self.assertRaisesMessage(NotImplementedError, msg):
  600. self.hasher.verify("password", "encoded")
  601. @skipUnless(argon2, "argon2-cffi not installed")
  602. @override_settings(PASSWORD_HASHERS=PASSWORD_HASHERS)
  603. class TestUtilsHashPassArgon2(SimpleTestCase):
  604. def test_argon2(self):
  605. encoded = make_password("lètmein", hasher="argon2")
  606. self.assertTrue(is_password_usable(encoded))
  607. self.assertTrue(encoded.startswith("argon2$argon2id$"))
  608. self.assertTrue(check_password("lètmein", encoded))
  609. self.assertFalse(check_password("lètmeinz", encoded))
  610. self.assertEqual(identify_hasher(encoded).algorithm, "argon2")
  611. # Blank passwords
  612. blank_encoded = make_password("", hasher="argon2")
  613. self.assertTrue(blank_encoded.startswith("argon2$argon2id$"))
  614. self.assertTrue(is_password_usable(blank_encoded))
  615. self.assertTrue(check_password("", blank_encoded))
  616. self.assertFalse(check_password(" ", blank_encoded))
  617. # Old hashes without version attribute
  618. encoded = (
  619. "argon2$argon2i$m=8,t=1,p=1$c29tZXNhbHQ$gwQOXSNhxiOxPOA0+PY10P9QFO"
  620. "4NAYysnqRt1GSQLE55m+2GYDt9FEjPMHhP2Cuf0nOEXXMocVrsJAtNSsKyfg"
  621. )
  622. self.assertTrue(check_password("secret", encoded))
  623. self.assertFalse(check_password("wrong", encoded))
  624. # Old hashes with version attribute.
  625. encoded = "argon2$argon2i$v=19$m=8,t=1,p=1$c2FsdHNhbHQ$YC9+jJCrQhs5R6db7LlN8Q"
  626. self.assertIs(check_password("secret", encoded), True)
  627. self.assertIs(check_password("wrong", encoded), False)
  628. # Salt entropy check.
  629. hasher = get_hasher("argon2")
  630. encoded_weak_salt = make_password("lètmein", "iodizedsalt", "argon2")
  631. encoded_strong_salt = make_password("lètmein", hasher.salt(), "argon2")
  632. self.assertIs(hasher.must_update(encoded_weak_salt), True)
  633. self.assertIs(hasher.must_update(encoded_strong_salt), False)
  634. def test_argon2_decode(self):
  635. salt = "abcdefghijk"
  636. encoded = make_password("lètmein", salt=salt, hasher="argon2")
  637. hasher = get_hasher("argon2")
  638. decoded = hasher.decode(encoded)
  639. self.assertEqual(decoded["memory_cost"], hasher.memory_cost)
  640. self.assertEqual(decoded["parallelism"], hasher.parallelism)
  641. self.assertEqual(decoded["salt"], salt)
  642. self.assertEqual(decoded["time_cost"], hasher.time_cost)
  643. def test_argon2_upgrade(self):
  644. self._test_argon2_upgrade("time_cost", "time cost", 1)
  645. self._test_argon2_upgrade("memory_cost", "memory cost", 64)
  646. self._test_argon2_upgrade("parallelism", "parallelism", 1)
  647. def test_argon2_version_upgrade(self):
  648. hasher = get_hasher("argon2")
  649. state = {"upgraded": False}
  650. encoded = (
  651. "argon2$argon2id$v=19$m=102400,t=2,p=8$Y041dExhNkljRUUy$TMa6A8fPJh"
  652. "CAUXRhJXCXdw"
  653. )
  654. def setter(password):
  655. state["upgraded"] = True
  656. old_m = hasher.memory_cost
  657. old_t = hasher.time_cost
  658. old_p = hasher.parallelism
  659. try:
  660. hasher.memory_cost = 8
  661. hasher.time_cost = 1
  662. hasher.parallelism = 1
  663. self.assertTrue(check_password("secret", encoded, setter, "argon2"))
  664. self.assertTrue(state["upgraded"])
  665. finally:
  666. hasher.memory_cost = old_m
  667. hasher.time_cost = old_t
  668. hasher.parallelism = old_p
  669. def _test_argon2_upgrade(self, attr, summary_key, new_value):
  670. hasher = get_hasher("argon2")
  671. self.assertEqual("argon2", hasher.algorithm)
  672. self.assertNotEqual(getattr(hasher, attr), new_value)
  673. old_value = getattr(hasher, attr)
  674. try:
  675. # Generate hash with attr set to 1
  676. setattr(hasher, attr, new_value)
  677. encoded = make_password("letmein", hasher="argon2")
  678. attr_value = hasher.safe_summary(encoded)[summary_key]
  679. self.assertEqual(attr_value, new_value)
  680. state = {"upgraded": False}
  681. def setter(password):
  682. state["upgraded"] = True
  683. # No upgrade is triggered.
  684. self.assertTrue(check_password("letmein", encoded, setter, "argon2"))
  685. self.assertFalse(state["upgraded"])
  686. # Revert to the old rounds count and ...
  687. setattr(hasher, attr, old_value)
  688. # ... check if the password would get updated to the new count.
  689. self.assertTrue(check_password("letmein", encoded, setter, "argon2"))
  690. self.assertTrue(state["upgraded"])
  691. finally:
  692. setattr(hasher, attr, old_value)
  693. @override_settings(PASSWORD_HASHERS=PASSWORD_HASHERS)
  694. class TestUtilsHashPassScrypt(SimpleTestCase):
  695. def test_scrypt(self):
  696. encoded = make_password("lètmein", "seasalt", "scrypt")
  697. self.assertEqual(
  698. encoded,
  699. "scrypt$16384$seasalt$8$1$Qj3+9PPyRjSJIebHnG81TMjsqtaIGxNQG/aEB/NY"
  700. "afTJ7tibgfYz71m0ldQESkXFRkdVCBhhY8mx7rQwite/Pw==",
  701. )
  702. self.assertIs(is_password_usable(encoded), True)
  703. self.assertIs(check_password("lètmein", encoded), True)
  704. self.assertIs(check_password("lètmeinz", encoded), False)
  705. self.assertEqual(identify_hasher(encoded).algorithm, "scrypt")
  706. # Blank passwords.
  707. blank_encoded = make_password("", "seasalt", "scrypt")
  708. self.assertIs(blank_encoded.startswith("scrypt$"), True)
  709. self.assertIs(is_password_usable(blank_encoded), True)
  710. self.assertIs(check_password("", blank_encoded), True)
  711. self.assertIs(check_password(" ", blank_encoded), False)
  712. def test_scrypt_decode(self):
  713. encoded = make_password("lètmein", "seasalt", "scrypt")
  714. hasher = get_hasher("scrypt")
  715. decoded = hasher.decode(encoded)
  716. tests = [
  717. ("block_size", hasher.block_size),
  718. ("parallelism", hasher.parallelism),
  719. ("salt", "seasalt"),
  720. ("work_factor", hasher.work_factor),
  721. ]
  722. for key, excepted in tests:
  723. with self.subTest(key=key):
  724. self.assertEqual(decoded[key], excepted)
  725. def _test_scrypt_upgrade(self, attr, summary_key, new_value):
  726. hasher = get_hasher("scrypt")
  727. self.assertEqual(hasher.algorithm, "scrypt")
  728. self.assertNotEqual(getattr(hasher, attr), new_value)
  729. old_value = getattr(hasher, attr)
  730. try:
  731. # Generate hash with attr set to the new value.
  732. setattr(hasher, attr, new_value)
  733. encoded = make_password("lètmein", "seasalt", "scrypt")
  734. attr_value = hasher.safe_summary(encoded)[summary_key]
  735. self.assertEqual(attr_value, new_value)
  736. state = {"upgraded": False}
  737. def setter(password):
  738. state["upgraded"] = True
  739. # No update is triggered.
  740. self.assertIs(check_password("lètmein", encoded, setter, "scrypt"), True)
  741. self.assertIs(state["upgraded"], False)
  742. # Revert to the old value.
  743. setattr(hasher, attr, old_value)
  744. # Password is updated.
  745. self.assertIs(check_password("lètmein", encoded, setter, "scrypt"), True)
  746. self.assertIs(state["upgraded"], True)
  747. finally:
  748. setattr(hasher, attr, old_value)
  749. def test_scrypt_upgrade(self):
  750. tests = [
  751. ("work_factor", "work factor", 2**11),
  752. ("block_size", "block size", 10),
  753. ("parallelism", "parallelism", 2),
  754. ]
  755. for attr, summary_key, new_value in tests:
  756. with self.subTest(attr=attr):
  757. self._test_scrypt_upgrade(attr, summary_key, new_value)