-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Expand file tree
/
Copy pathauth_password.py
More file actions
113 lines (90 loc) · 3.99 KB
/
auth_password.py
File metadata and controls
113 lines (90 loc) · 3.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
"""Utilities for dashboard password hashing and verification."""
import hashlib
import hmac
import re
import secrets
_PBKDF2_ITERATIONS = 200_000
_PBKDF2_SALT_BYTES = 16
_PBKDF2_ALGORITHM = "pbkdf2_sha256"
_PBKDF2_FORMAT = f"{_PBKDF2_ALGORITHM}$"
_LEGACY_MD5_LENGTH = 32
_DASHBOARD_PASSWORD_MIN_LENGTH = 12
DEFAULT_DASHBOARD_PASSWORD = "astrbot"
def hash_dashboard_password(raw_password: str) -> str:
"""Return a salted hash for dashboard password using PBKDF2-HMAC-SHA256."""
if not isinstance(raw_password, str) or raw_password == "":
raise ValueError("Password cannot be empty")
salt = secrets.token_hex(_PBKDF2_SALT_BYTES)
digest = hashlib.pbkdf2_hmac(
"sha256",
raw_password.encode("utf-8"),
bytes.fromhex(salt),
_PBKDF2_ITERATIONS,
).hex()
return f"{_PBKDF2_FORMAT}{_PBKDF2_ITERATIONS}${salt}${digest}"
def validate_dashboard_password(raw_password: str) -> None:
"""Validate whether dashboard password meets the minimal complexity policy."""
if not isinstance(raw_password, str) or raw_password == "":
raise ValueError("Password cannot be empty")
if len(raw_password) < _DASHBOARD_PASSWORD_MIN_LENGTH:
raise ValueError(
f"Password must be at least {_DASHBOARD_PASSWORD_MIN_LENGTH} characters long"
)
if not re.search(r"[A-Z]", raw_password):
raise ValueError("Password must include at least one uppercase letter")
if not re.search(r"[a-z]", raw_password):
raise ValueError("Password must include at least one lowercase letter")
if not re.search(r"\d", raw_password):
raise ValueError("Password must include at least one digit")
def normalize_dashboard_password_hash(stored_password: str) -> str:
"""Ensure dashboard password has a value, fallback to default dashboard password hash."""
if not stored_password:
return hash_dashboard_password(DEFAULT_DASHBOARD_PASSWORD)
return stored_password
def _is_legacy_md5_hash(stored: str) -> bool:
return (
isinstance(stored, str)
and len(stored) == _LEGACY_MD5_LENGTH
and all(c in "0123456789abcdefABCDEF" for c in stored)
)
def _is_pbkdf2_hash(stored: str) -> bool:
return isinstance(stored, str) and stored.startswith(_PBKDF2_FORMAT)
def verify_dashboard_password(stored_hash: str, candidate_password: str) -> bool:
"""Verify password against legacy md5 or new PBKDF2-SHA256 format."""
if not isinstance(stored_hash, str) or not isinstance(candidate_password, str):
return False
if _is_legacy_md5_hash(stored_hash):
# Keep compatibility with existing md5-based deployments:
# new clients send plain password, old clients may send md5 of it.
candidate_md5 = hashlib.md5(candidate_password.encode("utf-8")).hexdigest()
return hmac.compare_digest(
stored_hash.lower(), candidate_md5.lower()
) or hmac.compare_digest(
stored_hash.lower(),
candidate_password.lower(),
)
if _is_pbkdf2_hash(stored_hash):
parts: list[str] = stored_hash.split("$")
if len(parts) != 4:
return False
_, iterations_s, salt, digest = parts
try:
iterations = int(iterations_s)
stored_key = bytes.fromhex(digest)
salt_bytes = bytes.fromhex(salt)
except (TypeError, ValueError):
return False
candidate_key = hashlib.pbkdf2_hmac(
"sha256",
candidate_password.encode("utf-8"),
salt_bytes,
iterations,
)
return hmac.compare_digest(stored_key, candidate_key)
return False
def is_default_dashboard_password(stored_hash: str) -> bool:
"""Check whether the password still equals the built-in default value."""
return verify_dashboard_password(stored_hash, DEFAULT_DASHBOARD_PASSWORD)
def is_legacy_dashboard_password(stored_hash: str) -> bool:
"""Check whether the password is still stored with legacy MD5."""
return _is_legacy_md5_hash(stored_hash)