import os import stat import tempfile import unittest from pathlib import Path from unittest import mock from passwords import ( ITERATIONS, hash_password, load_pepper, needs_rehash, verify_password, ) PEPPER = b"p" * 32 KNOWN_ANSWER = "pbkdf2_sha256$1000$AAAAAAAAAAAAAAAAAAAAAA==$9ZLLEusnEPU3Km8h+vnd4ue9fw7rHdm4QwuBX5ozynE=" class PasswordHashTests(unittest.TestCase): def test_known_answer(self): stored = hash_password( "correct horse battery staple", PEPPER, salt=bytes(16), iterations=1000 ) self.assertEqual(stored, KNOWN_ANSWER) def test_round_trip_uses_current_iterations_and_random_salt(self): first = hash_password("correct horse battery staple", PEPPER) second = hash_password("correct horse battery staple", PEPPER) self.assertTrue(first.startswith(f"pbkdf2_sha256${ITERATIONS}$")) self.assertNotEqual(first, second) self.assertTrue(verify_password("correct horse battery staple", first, PEPPER)) def test_wrong_password_fails(self): self.assertFalse(verify_password("wrong password!!", KNOWN_ANSWER, PEPPER)) def test_wrong_pepper_fails(self): self.assertFalse( verify_password("correct horse battery staple", KNOWN_ANSWER, b"q" * 32) ) def test_needs_rehash_below_current_iterations(self): self.assertTrue(needs_rehash(KNOWN_ANSWER)) self.assertFalse(needs_rehash(hash_password("x" * 12, PEPPER))) class PepperTests(unittest.TestCase): def test_env_pepper_must_be_long_enough(self): with ( mock.patch.dict(os.environ, {"PASSWORD_PEPPER": "short"}), self.assertRaisesRegex(RuntimeError, "at least 32 characters"), ): load_pepper(Path("/nonexistent")) def test_env_pepper_is_used_when_set(self): with mock.patch.dict(os.environ, {"PASSWORD_PEPPER": "e" * 40}): self.assertEqual(load_pepper(Path("/nonexistent")), b"e" * 40) def test_file_fallback_creates_private_stable_pepper(self): with tempfile.TemporaryDirectory() as tmp, mock.patch.dict(os.environ): os.environ.pop("PASSWORD_PEPPER", None) pepper_file = Path(tmp) / ".pepper" with self.assertLogs("passwords", level="WARNING"): first = load_pepper(pepper_file) with self.assertLogs("passwords", level="WARNING"): second = load_pepper(pepper_file) self.assertEqual(first, second) self.assertEqual(len(first), 64) self.assertEqual(stat.S_IMODE(pepper_file.stat().st_mode), 0o600)