test_hashers.py 28 KB

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