test_hashers.py 32 KB

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