mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-18 10:00:40 +08:00
fix(cli): robust password hashing when argon2 missing (fallback to PBKDF2)
This commit is contained in:
@@ -14,6 +14,7 @@ Notes:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import binascii
|
||||
import hashlib
|
||||
import json
|
||||
import zoneinfo
|
||||
@@ -21,16 +22,38 @@ from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
import click
|
||||
from argon2 import PasswordHasher
|
||||
from argon2 import exceptions as argon2_exceptions
|
||||
|
||||
try:
|
||||
# Argon2 provides secure password hashing. Import if available.
|
||||
from argon2 import PasswordHasher
|
||||
from argon2 import exceptions as argon2_exceptions
|
||||
|
||||
_HAS_ARGON2 = True
|
||||
except Exception:
|
||||
# Argon2 may be broken on some Python/platform combinations (e.g. missing legacy `imp`).
|
||||
# Fall back to a PBKDF2-HMAC-SHA256 based approach implemented below.
|
||||
PasswordHasher = None
|
||||
argon2_exceptions = None
|
||||
_HAS_ARGON2 = False
|
||||
|
||||
from astrbot.cli.utils import check_astrbot_root
|
||||
from astrbot.core.config.default import DEFAULT_CONFIG
|
||||
from astrbot.core.utils.astrbot_path import astrbot_paths
|
||||
|
||||
# Instantiate a module-level argon2 hasher.
|
||||
# Parameters use argon2 defaults provided by the library which are secure for typical use.
|
||||
_PASSWORD_HASHER = PasswordHasher()
|
||||
# Instantiate a module-level hasher (argon2 when available, else None).
|
||||
# When argon2 is unavailable we will use PBKDF2-HMAC-SHA256 as a deterministic secure fallback.
|
||||
if _HAS_ARGON2:
|
||||
try:
|
||||
_PASSWORD_HASHER = PasswordHasher()
|
||||
except Exception:
|
||||
# If construction fails for any reason, fall back to PBKDF2 mode.
|
||||
_PASSWORD_HASHER = None
|
||||
_HAS_ARGON2 = False
|
||||
if not _HAS_ARGON2:
|
||||
_PASSWORD_HASHER = None
|
||||
# PBKDF2 fallback parameters (kept stable for deterministic stored hash format)
|
||||
PBKDF2_SALT = b"astrbot-dashboard"
|
||||
PBKDF2_ITER = 200_000
|
||||
|
||||
# Plaintext default dashboard password used on first-deploy / demo environments.
|
||||
# This mirrors the default username "astrbot" from DEFAULT_CONFIG.
|
||||
@@ -45,17 +68,31 @@ DEFAULT_DASHBOARD_PASSWORD_SHA256 = hashlib.sha256(
|
||||
DEFAULT_DASHBOARD_PASSWORD.encode("utf-8")
|
||||
).hexdigest()
|
||||
|
||||
# A secure argon2 hash of the default password (useful when initializing new configs).
|
||||
# We compute it once at import-time. Argon2 produces a different encoded string per call,
|
||||
# but that's acceptable for generating a starting hash for fresh configs.
|
||||
try:
|
||||
DEFAULT_DASHBOARD_PASSWORD_HASH = _PASSWORD_HASHER.hash(DEFAULT_DASHBOARD_PASSWORD)
|
||||
except Exception:
|
||||
# If argon2 is unavailable for some reason, fall back to sha256 hex as a last resort.
|
||||
# This branch intentionally crashes loudly later if argon2 is truly required.
|
||||
DEFAULT_DASHBOARD_PASSWORD_HASH = hashlib.sha256(
|
||||
DEFAULT_DASHBOARD_PASSWORD.encode("utf-8")
|
||||
).hexdigest()
|
||||
# A secure default hash for the default password.
|
||||
# If argon2 is available we generate an argon2 encoded hash. Otherwise we use a
|
||||
# stable PBKDF2-HMAC-SHA256 encoded string format:
|
||||
# pbkdf2_sha256$<iterations>$<salt_hex>$<digest_hex>
|
||||
|
||||
|
||||
if _HAS_ARGON2 and _PASSWORD_HASHER is not None:
|
||||
try:
|
||||
DEFAULT_DASHBOARD_PASSWORD_HASH = _PASSWORD_HASHER.hash(
|
||||
DEFAULT_DASHBOARD_PASSWORD
|
||||
)
|
||||
except Exception:
|
||||
# Fall back to PBKDF2 if argon2 unexpectedly fails at runtime.
|
||||
dk = hashlib.pbkdf2_hmac(
|
||||
"sha256",
|
||||
DEFAULT_DASHBOARD_PASSWORD.encode("utf-8"),
|
||||
PBKDF2_SALT,
|
||||
PBKDF2_ITER,
|
||||
)
|
||||
DEFAULT_DASHBOARD_PASSWORD_HASH = f"pbkdf2_sha256${PBKDF2_ITER}${binascii.hexlify(PBKDF2_SALT).decode()}${dk.hex()}"
|
||||
else:
|
||||
dk = hashlib.pbkdf2_hmac(
|
||||
"sha256", DEFAULT_DASHBOARD_PASSWORD.encode("utf-8"), PBKDF2_SALT, PBKDF2_ITER
|
||||
)
|
||||
DEFAULT_DASHBOARD_PASSWORD_HASH = f"pbkdf2_sha256${PBKDF2_ITER}${binascii.hexlify(PBKDF2_SALT).decode()}${dk.hex()}"
|
||||
|
||||
|
||||
# --- Password hashing & validation utilities ---
|
||||
@@ -63,15 +100,27 @@ except Exception:
|
||||
|
||||
def hash_dashboard_password_secure(value: str) -> str:
|
||||
"""
|
||||
Hash the dashboard password using Argon2 (secure).
|
||||
Hash the dashboard password for storage.
|
||||
|
||||
Returns the encoded Argon2 hash string.
|
||||
Preferred: Argon2 encoded string (if argon2 available).
|
||||
Fallback: PBKDF2-HMAC-SHA256 encoded string in the format:
|
||||
pbkdf2_sha256$<iterations>$<salt_hex>$<digest_hex>
|
||||
"""
|
||||
try:
|
||||
return _PASSWORD_HASHER.hash(value)
|
||||
except argon2_exceptions.HashingError as e:
|
||||
# Convert argon2-specific error into a ClickException to surface to CLI users.
|
||||
raise click.ClickException(f"Failed to hash password securely: {e!s}")
|
||||
import binascii
|
||||
|
||||
if _HAS_ARGON2 and _PASSWORD_HASHER is not None:
|
||||
try:
|
||||
return _PASSWORD_HASHER.hash(value)
|
||||
except Exception as e:
|
||||
# Surface a ClickException for CLI users while allowing fallback below.
|
||||
# Do not silently swallow unexpected errors.
|
||||
raise click.ClickException(
|
||||
f"Failed to hash password securely (argon2): {e!s}"
|
||||
)
|
||||
|
||||
# PBKDF2 fallback (deterministic encoded string)
|
||||
dk = hashlib.pbkdf2_hmac("sha256", value.encode("utf-8"), PBKDF2_SALT, PBKDF2_ITER)
|
||||
return f"pbkdf2_sha256${PBKDF2_ITER}${binascii.hexlify(PBKDF2_SALT).decode()}${dk.hex()}"
|
||||
|
||||
|
||||
def verify_dashboard_password(value: str, stored_hash: str) -> bool:
|
||||
@@ -79,31 +128,50 @@ def verify_dashboard_password(value: str, stored_hash: str) -> bool:
|
||||
Verify a plaintext password `value` against a stored hash.
|
||||
|
||||
Supports:
|
||||
- Argon2 encoded hashes (preferred)
|
||||
- Argon2 encoded hashes (preferred when available)
|
||||
- PBKDF2-HMAC-SHA256 encoded strings created by the fallback
|
||||
format: pbkdf2_sha256$<iterations>$<salt_hex>$<digest_hex>
|
||||
- Legacy SHA-256 and MD5 hexadecimal digests for backward compatibility.
|
||||
"""
|
||||
import binascii
|
||||
|
||||
if not stored_hash:
|
||||
return False
|
||||
|
||||
# Argon2 encoded hashes start with $argon2
|
||||
if stored_hash.startswith("$argon2"):
|
||||
# Argon2 encoded hashes start with $argon2 (only valid if argon2 available)
|
||||
if (
|
||||
stored_hash.startswith("$argon2")
|
||||
and _HAS_ARGON2
|
||||
and _PASSWORD_HASHER is not None
|
||||
):
|
||||
try:
|
||||
return _PASSWORD_HASHER.verify(stored_hash, value)
|
||||
except argon2_exceptions.VerifyMismatchError:
|
||||
return False
|
||||
except Exception as e:
|
||||
# Fail loudly on unexpected errors
|
||||
raise click.ClickException(f"Password verification failure: {e!s}")
|
||||
# argon2 verification mismatch or errors: return False for mismatch,
|
||||
# raise for unexpected exceptions to avoid silent failures.
|
||||
if getattr(e, "__class__", None).__name__ == "VerifyMismatchError":
|
||||
return False
|
||||
raise click.ClickException(f"Password verification failure (argon2): {e!s}")
|
||||
|
||||
# PBKDF2 fallback format: pbkdf2_sha256$iters$salt_hex$digest_hex
|
||||
if stored_hash.startswith("pbkdf2_sha256$"):
|
||||
try:
|
||||
_, iters_s, salt_hex, digest_hex = stored_hash.split("$", 3)
|
||||
iters = int(iters_s)
|
||||
salt = binascii.unhexlify(salt_hex)
|
||||
expected = digest_hex.lower()
|
||||
dk = hashlib.pbkdf2_hmac("sha256", value.encode("utf-8"), salt, iters)
|
||||
return dk.hex() == expected
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
# Legacy hex digests: support both sha256 (64 hex chars) and md5 (32 hex chars)
|
||||
if len(stored_hash) == 64 and all(
|
||||
ch in "0123456789abcdef" for ch in stored_hash.lower()
|
||||
):
|
||||
return hashlib.sha256(value.encode("utf-8")).hexdigest() == stored_hash.lower()
|
||||
if len(stored_hash) == 32 and all(
|
||||
ch in "0123456789abcdef" for ch in stored_hash.lower()
|
||||
):
|
||||
return hashlib.md5(value.encode("utf-8")).hexdigest() == stored_hash.lower()
|
||||
value_l = value.encode("utf-8")
|
||||
s = stored_hash.lower()
|
||||
if len(s) == 64 and all(ch in "0123456789abcdef" for ch in s):
|
||||
return hashlib.sha256(value_l).hexdigest() == s
|
||||
if len(s) == 32 and all(ch in "0123456789abcdef" for ch in s):
|
||||
return hashlib.md5(value_l).hexdigest() == s
|
||||
|
||||
# Unknown format
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user