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
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-23 14:54 +0000
1"""
2Password hashing and verification utilities.
4This module provides functions for secure password management using Argon2,
5with fallback verification support for PBKDF2-HMAC-SHA256.
6"""
8import hashlib
9import secrets
11import argon2
12from argon2.exceptions import VerifyMismatchError
13from pydantic import SecretStr
15# Global Argon2 PasswordHasher instance
16_ph = argon2.PasswordHasher()
18# Legacy PBKDF2 Constants
19LEGACY_ITERATIONS = 600_000
20LEGACY_SALT_SIZE = 16
21LEGACY_HASH_ALGORITHM = "sha256"
22LEGACY_FORMAT = "pbkdf2_sha256"
25def hash_password(password: str | SecretStr) -> str:
26 """
27 Hash a password using Argon2id.
29 Args:
30 password: The plaintext password to hash.
32 Returns:
33 The hashed password in Argon2 format.
35 """
36 if isinstance(password, SecretStr):
37 password_str = password.get_secret_value()
38 else:
39 password_str = password
41 return _ph.hash(password_str)
44def verify_password(password: str | SecretStr, password_hash: str) -> bool:
45 """
46 Verify a password against an Argon2 or legacy PBKDF2-HMAC-SHA256 hash.
48 Args:
49 password: The plaintext password to verify.
50 password_hash: The stored password hash.
52 Returns:
53 True if the password matches the hash, False otherwise.
55 """
56 if isinstance(password, SecretStr):
57 password_str = password.get_secret_value()
58 else:
59 password_str = password
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
68 _algorithm, iterations_str, salt_hex, hash_hex = parts
70 iterations = int(iterations_str)
71 salt = bytes.fromhex(salt_hex)
72 stored_hash = bytes.fromhex(hash_hex)
74 new_hash = hashlib.pbkdf2_hmac(
75 LEGACY_HASH_ALGORITHM,
76 password_str.encode("utf-8"),
77 salt,
78 iterations,
79 )
81 return secrets.compare_digest(new_hash, stored_hash)
82 except (ValueError, TypeError):
83 return False
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