Coverage for src / taipanstack / security / password.py: 100%

36 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-23 14:54 +0000

1""" 

2Password hashing and verification utilities. 

3 

4This module provides functions for secure password management using Argon2, 

5with fallback verification support for PBKDF2-HMAC-SHA256. 

6""" 

7 

8import hashlib 

9import secrets 

10 

11import argon2 

12from argon2.exceptions import VerifyMismatchError 

13from pydantic import SecretStr 

14 

15# Global Argon2 PasswordHasher instance 

16_ph = argon2.PasswordHasher() 

17 

18# Legacy PBKDF2 Constants 

19LEGACY_ITERATIONS = 600_000 

20LEGACY_SALT_SIZE = 16 

21LEGACY_HASH_ALGORITHM = "sha256" 

22LEGACY_FORMAT = "pbkdf2_sha256" 

23 

24 

25def hash_password(password: str | SecretStr) -> str: 

26 """ 

27 Hash a password using Argon2id. 

28 

29 Args: 

30 password: The plaintext password to hash. 

31 

32 Returns: 

33 The hashed password in Argon2 format. 

34 

35 """ 

36 if isinstance(password, SecretStr): 

37 password_str = password.get_secret_value() 

38 else: 

39 password_str = password 

40 

41 return _ph.hash(password_str) 

42 

43 

44def verify_password(password: str | SecretStr, password_hash: str) -> bool: 

45 """ 

46 Verify a password against an Argon2 or legacy PBKDF2-HMAC-SHA256 hash. 

47 

48 Args: 

49 password: The plaintext password to verify. 

50 password_hash: The stored password hash. 

51 

52 Returns: 

53 True if the password matches the hash, False otherwise. 

54 

55 """ 

56 if isinstance(password, SecretStr): 

57 password_str = password.get_secret_value() 

58 else: 

59 password_str = password 

60 

61 if password_hash.startswith(LEGACY_FORMAT + "$"): 

62 # Legacy PBKDF2 verification 

63 try: 

64 parts = password_hash.split("$") 

65 if len(parts) != 4: # noqa: PLR2004 

66 return False 

67 

68 _algorithm, iterations_str, salt_hex, hash_hex = parts 

69 

70 iterations = int(iterations_str) 

71 salt = bytes.fromhex(salt_hex) 

72 stored_hash = bytes.fromhex(hash_hex) 

73 

74 new_hash = hashlib.pbkdf2_hmac( 

75 LEGACY_HASH_ALGORITHM, 

76 password_str.encode("utf-8"), 

77 salt, 

78 iterations, 

79 ) 

80 

81 return secrets.compare_digest(new_hash, stored_hash) 

82 except (ValueError, TypeError): 

83 return False 

84 

85 # Argon2 verification 

86 try: 

87 return _ph.verify(password_hash, password_str) 

88 except ( 

89 VerifyMismatchError, 

90 ValueError, 

91 TypeError, 

92 argon2.exceptions.InvalidHashError, 

93 ): 

94 return False