import os import stat import tempfile import unittest from pathlib import Path from unittest import mock from auth import ITERATIONS, hash_password, load_pepper, needs_rehash, verify_password PEPPER = b"p" * 32 # Independently computed: HMAC-SHA256(PEPPER, password) -> PBKDF2-SHA256, 1000 iterations, 16 zero-byte salt. 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"}): with 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("auth", level="WARNING"): first = load_pepper(pepper_file) with self.assertLogs("auth", 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)