test_hashers.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  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, PBKDF2PasswordHasher, PBKDF2SHA1PasswordHasher,
  6. check_password, get_hasher, identify_hasher, is_password_usable,
  7. make_password,
  8. )
  9. from django.test import SimpleTestCase
  10. from django.test.utils import override_settings
  11. from django.utils.encoding import force_bytes
  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_pbkdf2(self):
  45. encoded = make_password('lètmein', 'seasalt', 'pbkdf2_sha256')
  46. self.assertEqual(encoded, 'pbkdf2_sha256$100000$seasalt$BNZ6eyaNc8qFTJPjrAq99hSYb73EgAdytAtdBg2Sdcc=')
  47. self.assertTrue(is_password_usable(encoded))
  48. self.assertTrue(check_password('lètmein', encoded))
  49. self.assertFalse(check_password('lètmeinz', encoded))
  50. self.assertEqual(identify_hasher(encoded).algorithm, "pbkdf2_sha256")
  51. # Blank passwords
  52. blank_encoded = make_password('', 'seasalt', 'pbkdf2_sha256')
  53. self.assertTrue(blank_encoded.startswith('pbkdf2_sha256$'))
  54. self.assertTrue(is_password_usable(blank_encoded))
  55. self.assertTrue(check_password('', blank_encoded))
  56. self.assertFalse(check_password(' ', blank_encoded))
  57. @override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher'])
  58. def test_sha1(self):
  59. encoded = make_password('lètmein', 'seasalt', 'sha1')
  60. self.assertEqual(encoded, 'sha1$seasalt$cff36ea83f5706ce9aa7454e63e431fc726b2dc8')
  61. self.assertTrue(is_password_usable(encoded))
  62. self.assertTrue(check_password('lètmein', encoded))
  63. self.assertFalse(check_password('lètmeinz', encoded))
  64. self.assertEqual(identify_hasher(encoded).algorithm, "sha1")
  65. # Blank passwords
  66. blank_encoded = make_password('', 'seasalt', 'sha1')
  67. self.assertTrue(blank_encoded.startswith('sha1$'))
  68. self.assertTrue(is_password_usable(blank_encoded))
  69. self.assertTrue(check_password('', blank_encoded))
  70. self.assertFalse(check_password(' ', blank_encoded))
  71. @override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.MD5PasswordHasher'])
  72. def test_md5(self):
  73. encoded = make_password('lètmein', 'seasalt', 'md5')
  74. self.assertEqual(encoded, 'md5$seasalt$3f86d0d3d465b7b458c231bf3555c0e3')
  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, "md5")
  79. # Blank passwords
  80. blank_encoded = make_password('', 'seasalt', 'md5')
  81. self.assertTrue(blank_encoded.startswith('md5$'))
  82. self.assertTrue(is_password_usable(blank_encoded))
  83. self.assertTrue(check_password('', blank_encoded))
  84. self.assertFalse(check_password(' ', blank_encoded))
  85. @override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.UnsaltedMD5PasswordHasher'])
  86. def test_unsalted_md5(self):
  87. encoded = make_password('lètmein', '', 'unsalted_md5')
  88. self.assertEqual(encoded, '88a434c88cca4e900f7874cd98123f43')
  89. self.assertTrue(is_password_usable(encoded))
  90. self.assertTrue(check_password('lètmein', encoded))
  91. self.assertFalse(check_password('lètmeinz', encoded))
  92. self.assertEqual(identify_hasher(encoded).algorithm, "unsalted_md5")
  93. # Alternate unsalted syntax
  94. alt_encoded = "md5$$%s" % encoded
  95. self.assertTrue(is_password_usable(alt_encoded))
  96. self.assertTrue(check_password('lètmein', alt_encoded))
  97. self.assertFalse(check_password('lètmeinz', alt_encoded))
  98. # Blank passwords
  99. blank_encoded = make_password('', '', 'unsalted_md5')
  100. self.assertTrue(is_password_usable(blank_encoded))
  101. self.assertTrue(check_password('', blank_encoded))
  102. self.assertFalse(check_password(' ', blank_encoded))
  103. @override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher'])
  104. def test_unsalted_sha1(self):
  105. encoded = make_password('lètmein', '', 'unsalted_sha1')
  106. self.assertEqual(encoded, 'sha1$$6d138ca3ae545631b3abd71a4f076ce759c5700b')
  107. self.assertTrue(is_password_usable(encoded))
  108. self.assertTrue(check_password('lètmein', encoded))
  109. self.assertFalse(check_password('lètmeinz', encoded))
  110. self.assertEqual(identify_hasher(encoded).algorithm, "unsalted_sha1")
  111. # Raw SHA1 isn't acceptable
  112. alt_encoded = encoded[6:]
  113. self.assertFalse(check_password('lètmein', alt_encoded))
  114. # Blank passwords
  115. blank_encoded = make_password('', '', 'unsalted_sha1')
  116. self.assertTrue(blank_encoded.startswith('sha1$'))
  117. self.assertTrue(is_password_usable(blank_encoded))
  118. self.assertTrue(check_password('', blank_encoded))
  119. self.assertFalse(check_password(' ', blank_encoded))
  120. @skipUnless(crypt, "no crypt module to generate password.")
  121. @override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.CryptPasswordHasher'])
  122. def test_crypt(self):
  123. encoded = make_password('lètmei', 'ab', 'crypt')
  124. self.assertEqual(encoded, 'crypt$$ab1Hv2Lg7ltQo')
  125. self.assertTrue(is_password_usable(encoded))
  126. self.assertTrue(check_password('lètmei', encoded))
  127. self.assertFalse(check_password('lètmeiz', encoded))
  128. self.assertEqual(identify_hasher(encoded).algorithm, "crypt")
  129. # Blank passwords
  130. blank_encoded = make_password('', 'ab', 'crypt')
  131. self.assertTrue(blank_encoded.startswith('crypt$'))
  132. self.assertTrue(is_password_usable(blank_encoded))
  133. self.assertTrue(check_password('', blank_encoded))
  134. self.assertFalse(check_password(' ', blank_encoded))
  135. @skipUnless(bcrypt, "bcrypt not installed")
  136. def test_bcrypt_sha256(self):
  137. encoded = make_password('lètmein', hasher='bcrypt_sha256')
  138. self.assertTrue(is_password_usable(encoded))
  139. self.assertTrue(encoded.startswith('bcrypt_sha256$'))
  140. self.assertTrue(check_password('lètmein', encoded))
  141. self.assertFalse(check_password('lètmeinz', encoded))
  142. self.assertEqual(identify_hasher(encoded).algorithm, "bcrypt_sha256")
  143. # password truncation no longer works
  144. password = (
  145. 'VSK0UYV6FFQVZ0KG88DYN9WADAADZO1CTSIVDJUNZSUML6IBX7LN7ZS3R5'
  146. 'JGB3RGZ7VI7G7DJQ9NI8BQFSRPTG6UWTTVESA5ZPUN'
  147. )
  148. encoded = make_password(password, hasher='bcrypt_sha256')
  149. self.assertTrue(check_password(password, encoded))
  150. self.assertFalse(check_password(password[:72], encoded))
  151. # Blank passwords
  152. blank_encoded = make_password('', hasher='bcrypt_sha256')
  153. self.assertTrue(blank_encoded.startswith('bcrypt_sha256$'))
  154. self.assertTrue(is_password_usable(blank_encoded))
  155. self.assertTrue(check_password('', blank_encoded))
  156. self.assertFalse(check_password(' ', blank_encoded))
  157. @skipUnless(bcrypt, "bcrypt not installed")
  158. def test_bcrypt(self):
  159. encoded = make_password('lètmein', hasher='bcrypt')
  160. self.assertTrue(is_password_usable(encoded))
  161. self.assertTrue(encoded.startswith('bcrypt$'))
  162. self.assertTrue(check_password('lètmein', encoded))
  163. self.assertFalse(check_password('lètmeinz', encoded))
  164. self.assertEqual(identify_hasher(encoded).algorithm, "bcrypt")
  165. # Blank passwords
  166. blank_encoded = make_password('', hasher='bcrypt')
  167. self.assertTrue(blank_encoded.startswith('bcrypt$'))
  168. self.assertTrue(is_password_usable(blank_encoded))
  169. self.assertTrue(check_password('', blank_encoded))
  170. self.assertFalse(check_password(' ', blank_encoded))
  171. @skipUnless(bcrypt, "bcrypt not installed")
  172. def test_bcrypt_upgrade(self):
  173. hasher = get_hasher('bcrypt')
  174. self.assertEqual('bcrypt', hasher.algorithm)
  175. self.assertNotEqual(hasher.rounds, 4)
  176. old_rounds = hasher.rounds
  177. try:
  178. # Generate a password with 4 rounds.
  179. hasher.rounds = 4
  180. encoded = make_password('letmein', hasher='bcrypt')
  181. rounds = hasher.safe_summary(encoded)['work factor']
  182. self.assertEqual(rounds, '04')
  183. state = {'upgraded': False}
  184. def setter(password):
  185. state['upgraded'] = True
  186. # No upgrade is triggered.
  187. self.assertTrue(check_password('letmein', encoded, setter, 'bcrypt'))
  188. self.assertFalse(state['upgraded'])
  189. # Revert to the old rounds count and ...
  190. hasher.rounds = old_rounds
  191. # ... check if the password would get updated to the new count.
  192. self.assertTrue(check_password('letmein', encoded, setter, 'bcrypt'))
  193. self.assertTrue(state['upgraded'])
  194. finally:
  195. hasher.rounds = old_rounds
  196. @skipUnless(bcrypt, "bcrypt not installed")
  197. def test_bcrypt_harden_runtime(self):
  198. hasher = get_hasher('bcrypt')
  199. self.assertEqual('bcrypt', hasher.algorithm)
  200. with mock.patch.object(hasher, 'rounds', 4):
  201. encoded = make_password('letmein', hasher='bcrypt')
  202. with mock.patch.object(hasher, 'rounds', 6), \
  203. mock.patch.object(hasher, 'encode', side_effect=hasher.encode):
  204. hasher.harden_runtime('wrong_password', encoded)
  205. # Increasing rounds from 4 to 6 means an increase of 4 in workload,
  206. # therefore hardening should run 3 times to make the timing the
  207. # same (the original encode() call already ran once).
  208. self.assertEqual(hasher.encode.call_count, 3)
  209. # Get the original salt (includes the original workload factor)
  210. algorithm, data = encoded.split('$', 1)
  211. expected_call = (('wrong_password', force_bytes(data[:29])),)
  212. self.assertEqual(hasher.encode.call_args_list, [expected_call] * 3)
  213. def test_unusable(self):
  214. encoded = make_password(None)
  215. self.assertEqual(len(encoded), len(UNUSABLE_PASSWORD_PREFIX) + UNUSABLE_PASSWORD_SUFFIX_LENGTH)
  216. self.assertFalse(is_password_usable(encoded))
  217. self.assertFalse(check_password(None, encoded))
  218. self.assertFalse(check_password(encoded, encoded))
  219. self.assertFalse(check_password(UNUSABLE_PASSWORD_PREFIX, encoded))
  220. self.assertFalse(check_password('', encoded))
  221. self.assertFalse(check_password('lètmein', encoded))
  222. self.assertFalse(check_password('lètmeinz', encoded))
  223. with self.assertRaisesMessage(ValueError, 'Unknown password hashing algorith'):
  224. identify_hasher(encoded)
  225. # Assert that the unusable passwords actually contain a random part.
  226. # This might fail one day due to a hash collision.
  227. self.assertNotEqual(encoded, make_password(None), "Random password collision?")
  228. def test_unspecified_password(self):
  229. """
  230. Makes sure specifying no plain password with a valid encoded password
  231. returns `False`.
  232. """
  233. self.assertFalse(check_password(None, make_password('lètmein')))
  234. def test_bad_algorithm(self):
  235. msg = (
  236. "Unknown password hashing algorithm '%s'. Did you specify it in "
  237. "the PASSWORD_HASHERS setting?"
  238. )
  239. with self.assertRaisesMessage(ValueError, msg % 'lolcat'):
  240. make_password('lètmein', hasher='lolcat')
  241. with self.assertRaisesMessage(ValueError, msg % 'lolcat'):
  242. identify_hasher('lolcat$salt$hash')
  243. def test_bad_encoded(self):
  244. self.assertFalse(is_password_usable('lètmein_badencoded'))
  245. self.assertFalse(is_password_usable(''))
  246. def test_low_level_pbkdf2(self):
  247. hasher = PBKDF2PasswordHasher()
  248. encoded = hasher.encode('lètmein', 'seasalt2')
  249. self.assertEqual(encoded, 'pbkdf2_sha256$100000$seasalt2$Tl4GMr+Yt1zzO1sbKoUaDBdds5NkR3RxaDWuQsliFrI=')
  250. self.assertTrue(hasher.verify('lètmein', encoded))
  251. def test_low_level_pbkdf2_sha1(self):
  252. hasher = PBKDF2SHA1PasswordHasher()
  253. encoded = hasher.encode('lètmein', 'seasalt2')
  254. self.assertEqual(encoded, 'pbkdf2_sha1$100000$seasalt2$dK/dL+ySBZ5zoR0+Zk3SB/VsH0U=')
  255. self.assertTrue(hasher.verify('lètmein', encoded))
  256. @override_settings(
  257. PASSWORD_HASHERS=[
  258. 'django.contrib.auth.hashers.PBKDF2PasswordHasher',
  259. 'django.contrib.auth.hashers.SHA1PasswordHasher',
  260. 'django.contrib.auth.hashers.MD5PasswordHasher',
  261. ],
  262. )
  263. def test_upgrade(self):
  264. self.assertEqual('pbkdf2_sha256', get_hasher('default').algorithm)
  265. for algo in ('sha1', 'md5'):
  266. encoded = make_password('lètmein', hasher=algo)
  267. state = {'upgraded': False}
  268. def setter(password):
  269. state['upgraded'] = True
  270. self.assertTrue(check_password('lètmein', encoded, setter))
  271. self.assertTrue(state['upgraded'])
  272. def test_no_upgrade(self):
  273. encoded = make_password('lètmein')
  274. state = {'upgraded': False}
  275. def setter():
  276. state['upgraded'] = True
  277. self.assertFalse(check_password('WRONG', encoded, setter))
  278. self.assertFalse(state['upgraded'])
  279. @override_settings(
  280. PASSWORD_HASHERS=[
  281. 'django.contrib.auth.hashers.PBKDF2PasswordHasher',
  282. 'django.contrib.auth.hashers.SHA1PasswordHasher',
  283. 'django.contrib.auth.hashers.MD5PasswordHasher',
  284. ],
  285. )
  286. def test_no_upgrade_on_incorrect_pass(self):
  287. self.assertEqual('pbkdf2_sha256', get_hasher('default').algorithm)
  288. for algo in ('sha1', 'md5'):
  289. encoded = make_password('lètmein', hasher=algo)
  290. state = {'upgraded': False}
  291. def setter():
  292. state['upgraded'] = True
  293. self.assertFalse(check_password('WRONG', encoded, setter))
  294. self.assertFalse(state['upgraded'])
  295. def test_pbkdf2_upgrade(self):
  296. hasher = get_hasher('default')
  297. self.assertEqual('pbkdf2_sha256', hasher.algorithm)
  298. self.assertNotEqual(hasher.iterations, 1)
  299. old_iterations = hasher.iterations
  300. try:
  301. # Generate a password with 1 iteration.
  302. hasher.iterations = 1
  303. encoded = make_password('letmein')
  304. algo, iterations, salt, hash = encoded.split('$', 3)
  305. self.assertEqual(iterations, '1')
  306. state = {'upgraded': False}
  307. def setter(password):
  308. state['upgraded'] = True
  309. # No upgrade is triggered
  310. self.assertTrue(check_password('letmein', encoded, setter))
  311. self.assertFalse(state['upgraded'])
  312. # Revert to the old iteration count and ...
  313. hasher.iterations = old_iterations
  314. # ... check if the password would get updated to the new iteration count.
  315. self.assertTrue(check_password('letmein', encoded, setter))
  316. self.assertTrue(state['upgraded'])
  317. finally:
  318. hasher.iterations = old_iterations
  319. def test_pbkdf2_harden_runtime(self):
  320. hasher = get_hasher('default')
  321. self.assertEqual('pbkdf2_sha256', hasher.algorithm)
  322. with mock.patch.object(hasher, 'iterations', 1):
  323. encoded = make_password('letmein')
  324. with mock.patch.object(hasher, 'iterations', 6), \
  325. mock.patch.object(hasher, 'encode', side_effect=hasher.encode):
  326. hasher.harden_runtime('wrong_password', encoded)
  327. # Encode should get called once ...
  328. self.assertEqual(hasher.encode.call_count, 1)
  329. # ... with the original salt and 5 iterations.
  330. algorithm, iterations, salt, hash = encoded.split('$', 3)
  331. expected_call = (('wrong_password', salt, 5),)
  332. self.assertEqual(hasher.encode.call_args, expected_call)
  333. def test_pbkdf2_upgrade_new_hasher(self):
  334. hasher = get_hasher('default')
  335. self.assertEqual('pbkdf2_sha256', hasher.algorithm)
  336. self.assertNotEqual(hasher.iterations, 1)
  337. state = {'upgraded': False}
  338. def setter(password):
  339. state['upgraded'] = True
  340. with self.settings(PASSWORD_HASHERS=[
  341. 'auth_tests.test_hashers.PBKDF2SingleIterationHasher']):
  342. encoded = make_password('letmein')
  343. algo, iterations, salt, hash = encoded.split('$', 3)
  344. self.assertEqual(iterations, '1')
  345. # No upgrade is triggered
  346. self.assertTrue(check_password('letmein', encoded, setter))
  347. self.assertFalse(state['upgraded'])
  348. # Revert to the old iteration count and check if the password would get
  349. # updated to the new iteration count.
  350. with self.settings(PASSWORD_HASHERS=[
  351. 'django.contrib.auth.hashers.PBKDF2PasswordHasher',
  352. 'auth_tests.test_hashers.PBKDF2SingleIterationHasher']):
  353. self.assertTrue(check_password('letmein', encoded, setter))
  354. self.assertTrue(state['upgraded'])
  355. def test_check_password_calls_harden_runtime(self):
  356. hasher = get_hasher('default')
  357. encoded = make_password('letmein')
  358. with mock.patch.object(hasher, 'harden_runtime'), \
  359. mock.patch.object(hasher, 'must_update', return_value=True):
  360. # Correct password supplied, no hardening needed
  361. check_password('letmein', encoded)
  362. self.assertEqual(hasher.harden_runtime.call_count, 0)
  363. # Wrong password supplied, hardening needed
  364. check_password('wrong_password', encoded)
  365. self.assertEqual(hasher.harden_runtime.call_count, 1)
  366. def test_load_library_no_algorithm(self):
  367. msg = "Hasher 'BasePasswordHasher' doesn't specify a library attribute"
  368. with self.assertRaisesMessage(ValueError, msg):
  369. BasePasswordHasher()._load_library()
  370. def test_load_library_importerror(self):
  371. PlainHasher = type('PlainHasher', (BasePasswordHasher,), {'algorithm': 'plain', 'library': 'plain'})
  372. msg = "Couldn't load 'PlainHasher' algorithm library: No module named 'plain'"
  373. with self.assertRaisesMessage(ValueError, msg):
  374. PlainHasher()._load_library()
  375. @skipUnless(argon2, "argon2-cffi not installed")
  376. @override_settings(PASSWORD_HASHERS=PASSWORD_HASHERS)
  377. class TestUtilsHashPassArgon2(SimpleTestCase):
  378. def test_argon2(self):
  379. encoded = make_password('lètmein', hasher='argon2')
  380. self.assertTrue(is_password_usable(encoded))
  381. self.assertTrue(encoded.startswith('argon2$'))
  382. self.assertTrue(check_password('lètmein', encoded))
  383. self.assertFalse(check_password('lètmeinz', encoded))
  384. self.assertEqual(identify_hasher(encoded).algorithm, 'argon2')
  385. # Blank passwords
  386. blank_encoded = make_password('', hasher='argon2')
  387. self.assertTrue(blank_encoded.startswith('argon2$'))
  388. self.assertTrue(is_password_usable(blank_encoded))
  389. self.assertTrue(check_password('', blank_encoded))
  390. self.assertFalse(check_password(' ', blank_encoded))
  391. # Old hashes without version attribute
  392. encoded = (
  393. 'argon2$argon2i$m=8,t=1,p=1$c29tZXNhbHQ$gwQOXSNhxiOxPOA0+PY10P9QFO'
  394. '4NAYysnqRt1GSQLE55m+2GYDt9FEjPMHhP2Cuf0nOEXXMocVrsJAtNSsKyfg'
  395. )
  396. self.assertTrue(check_password('secret', encoded))
  397. self.assertFalse(check_password('wrong', encoded))
  398. def test_argon2_upgrade(self):
  399. self._test_argon2_upgrade('time_cost', 'time cost', 1)
  400. self._test_argon2_upgrade('memory_cost', 'memory cost', 16)
  401. self._test_argon2_upgrade('parallelism', 'parallelism', 1)
  402. def test_argon2_version_upgrade(self):
  403. hasher = get_hasher('argon2')
  404. state = {'upgraded': False}
  405. encoded = (
  406. 'argon2$argon2i$m=8,t=1,p=1$c29tZXNhbHQ$gwQOXSNhxiOxPOA0+PY10P9QFO'
  407. '4NAYysnqRt1GSQLE55m+2GYDt9FEjPMHhP2Cuf0nOEXXMocVrsJAtNSsKyfg'
  408. )
  409. def setter(password):
  410. state['upgraded'] = True
  411. old_m = hasher.memory_cost
  412. old_t = hasher.time_cost
  413. old_p = hasher.parallelism
  414. try:
  415. hasher.memory_cost = 8
  416. hasher.time_cost = 1
  417. hasher.parallelism = 1
  418. self.assertTrue(check_password('secret', encoded, setter, 'argon2'))
  419. self.assertTrue(state['upgraded'])
  420. finally:
  421. hasher.memory_cost = old_m
  422. hasher.time_cost = old_t
  423. hasher.parallelism = old_p
  424. def _test_argon2_upgrade(self, attr, summary_key, new_value):
  425. hasher = get_hasher('argon2')
  426. self.assertEqual('argon2', hasher.algorithm)
  427. self.assertNotEqual(getattr(hasher, attr), new_value)
  428. old_value = getattr(hasher, attr)
  429. try:
  430. # Generate hash with attr set to 1
  431. setattr(hasher, attr, new_value)
  432. encoded = make_password('letmein', hasher='argon2')
  433. attr_value = hasher.safe_summary(encoded)[summary_key]
  434. self.assertEqual(attr_value, new_value)
  435. state = {'upgraded': False}
  436. def setter(password):
  437. state['upgraded'] = True
  438. # No upgrade is triggered.
  439. self.assertTrue(check_password('letmein', encoded, setter, 'argon2'))
  440. self.assertFalse(state['upgraded'])
  441. # Revert to the old rounds count and ...
  442. setattr(hasher, attr, old_value)
  443. # ... check if the password would get updated to the new count.
  444. self.assertTrue(check_password('letmein', encoded, setter, 'argon2'))
  445. self.assertTrue(state['upgraded'])
  446. finally:
  447. setattr(hasher, attr, old_value)