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