Skip to content

Commit 9ab169d

Browse files
committed
feat: migrate argon2 secure password hashing from dev branch
1 parent b749f62 commit 9ab169d

File tree

3 files changed

+64
-15
lines changed

3 files changed

+64
-15
lines changed

astrbot/core/utils/auth_password.py

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@
55
import re
66
import secrets
77

8+
try:
9+
import argon2.exceptions as argon2_exceptions
10+
from argon2 import PasswordHasher
11+
12+
_PASSWORD_HASHER = PasswordHasher()
13+
except ImportError:
14+
_PASSWORD_HASHER = None
15+
816
_PBKDF2_ITERATIONS = 600_000
917
_PBKDF2_SALT_BYTES = 16
1018
_PBKDF2_ALGORITHM = "pbkdf2_sha256"
@@ -15,10 +23,16 @@
1523

1624

1725
def hash_dashboard_password(raw_password: str) -> str:
18-
"""Return a salted hash for dashboard password using PBKDF2-HMAC-SHA256."""
26+
"""Return a salted hash for dashboard password using Argon2 (if available) or PBKDF2-HMAC-SHA256 fallback."""
1927
if not isinstance(raw_password, str) or raw_password == "":
2028
raise ValueError("Password cannot be empty")
2129

30+
if _PASSWORD_HASHER is not None:
31+
try:
32+
return _PASSWORD_HASHER.hash(raw_password)
33+
except Exception as e:
34+
raise ValueError(f"Failed to hash password securely (argon2): {e!s}")
35+
2236
salt = secrets.token_hex(_PBKDF2_SALT_BYTES)
2337
digest = hashlib.pbkdf2_hmac(
2438
"sha256",
@@ -65,21 +79,24 @@ def _is_pbkdf2_hash(stored: str) -> bool:
6579
return isinstance(stored, str) and stored.startswith(_PBKDF2_FORMAT)
6680

6781

82+
def _is_argon2_hash(stored: str) -> bool:
83+
return isinstance(stored, str) and stored.startswith("$argon2")
84+
85+
6886
def verify_dashboard_password(stored_hash: str, candidate_password: str) -> bool:
69-
"""Verify password against legacy md5 or new PBKDF2-SHA256 format."""
87+
"""Verify password against legacy md5, new PBKDF2-SHA256 format, or Argon2."""
7088
if not isinstance(stored_hash, str) or not isinstance(candidate_password, str):
7189
return False
7290

73-
if _is_legacy_md5_hash(stored_hash):
74-
# Keep compatibility with existing md5-based deployments:
75-
# new clients send plain password, old clients may send md5 of it.
76-
candidate_md5 = hashlib.md5(candidate_password.encode("utf-8")).hexdigest()
77-
return hmac.compare_digest(
78-
stored_hash.lower(), candidate_md5.lower()
79-
) or hmac.compare_digest(
80-
stored_hash.lower(),
81-
candidate_password.lower(),
82-
)
91+
if _is_argon2_hash(stored_hash):
92+
if _PASSWORD_HASHER is None:
93+
return False
94+
try:
95+
return _PASSWORD_HASHER.verify(stored_hash, candidate_password)
96+
except argon2_exceptions.VerifyMismatchError:
97+
return False
98+
except Exception:
99+
return False
83100

84101
if _is_pbkdf2_hash(stored_hash):
85102
parts: list[str] = stored_hash.split("$")
@@ -100,6 +117,28 @@ def verify_dashboard_password(stored_hash: str, candidate_password: str) -> bool
100117
)
101118
return hmac.compare_digest(stored_key, candidate_key)
102119

120+
if _is_legacy_md5_hash(stored_hash):
121+
# Keep compatibility with existing md5-based deployments:
122+
# new clients send plain password, old clients may send md5 of it.
123+
candidate_md5 = hashlib.md5(candidate_password.encode("utf-8")).hexdigest()
124+
return hmac.compare_digest(
125+
stored_hash.lower(), candidate_md5.lower()
126+
) or hmac.compare_digest(
127+
stored_hash.lower(),
128+
candidate_password.lower(),
129+
)
130+
131+
# Legacy SHA-256 fallback compatibility (from dev branch)
132+
if len(stored_hash) == 64 and all(
133+
c in "0123456789abcdefABCDEF" for c in stored_hash
134+
):
135+
candidate_sha256 = hashlib.sha256(
136+
candidate_password.encode("utf-8")
137+
).hexdigest()
138+
return hmac.compare_digest(
139+
stored_hash.lower(), candidate_sha256.lower()
140+
) or hmac.compare_digest(stored_hash.lower(), candidate_password.lower())
141+
103142
return False
104143

105144

@@ -109,5 +148,13 @@ def is_default_dashboard_password(stored_hash: str) -> bool:
109148

110149

111150
def is_legacy_dashboard_password(stored_hash: str) -> bool:
112-
"""Check whether the password is still stored with legacy MD5."""
113-
return _is_legacy_md5_hash(stored_hash)
151+
"""Check whether the password is still stored with legacy MD5 or plain SHA256."""
152+
if not isinstance(stored_hash, str) or not stored_hash:
153+
return False
154+
if _is_legacy_md5_hash(stored_hash):
155+
return True
156+
if len(stored_hash) == 64 and all(
157+
c in "0123456789abcdefABCDEF" for c in stored_hash
158+
):
159+
return True
160+
return False

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ dependencies = [
5858
"click>=8.2.1",
5959
"pypdf>=6.1.1",
6060
"aiofiles>=25.1.0",
61+
"argon2-cffi>=23.1.0",
6162
"rank-bm25>=0.2.2",
6263
"jieba>=0.42.1",
6364
"markitdown-no-magika[docx,xls,xlsx]>=0.1.2",

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,5 @@ tenacity>=9.1.2
5656
shipyard-python-sdk>=0.2.4
5757
shipyard-neo-sdk>=0.2.0
5858
packaging>=24.2
59-
qrcode>=8.2
59+
qrcode>=8.2
60+
argon2-cffi>=23.1.0

0 commit comments

Comments
 (0)