68 lines
1.8 KiB
Python
68 lines
1.8 KiB
Python
import base64
|
|
import hashlib
|
|
import hmac
|
|
import json
|
|
import os
|
|
import secrets
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
USERS_FILE = Path(os.getenv("DOCUMENT_PROCESSOR_USERS_FILE", "data/users.json"))
|
|
|
|
|
|
def _b64e(b: bytes) -> str:
|
|
return base64.b64encode(b).decode("utf-8")
|
|
|
|
|
|
def _b64d(s: str) -> bytes:
|
|
return base64.b64decode(s.encode("utf-8"))
|
|
|
|
|
|
def hash_password(password: str) -> str:
|
|
salt = secrets.token_bytes(16)
|
|
digest = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, 200_000)
|
|
return f"pbkdf2_sha256$200000${_b64e(salt)}${_b64e(digest)}"
|
|
|
|
|
|
def verify_password(password: str, stored: str) -> bool:
|
|
try:
|
|
scheme, iterations, salt_b64, digest_b64 = stored.split("$", 3)
|
|
if scheme != "pbkdf2_sha256":
|
|
return False
|
|
digest = hashlib.pbkdf2_hmac(
|
|
"sha256",
|
|
password.encode("utf-8"),
|
|
_b64d(salt_b64),
|
|
int(iterations),
|
|
)
|
|
return hmac.compare_digest(digest, _b64d(digest_b64))
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def load_users() -> list[dict]:
|
|
if not USERS_FILE.exists():
|
|
return []
|
|
return json.loads(USERS_FILE.read_text())
|
|
|
|
|
|
def get_user_by_username(username: str) -> Optional[dict]:
|
|
username = username.strip().lower()
|
|
for user in load_users():
|
|
if user.get("username", "").lower() == username:
|
|
return user
|
|
return None
|
|
|
|
|
|
def authenticate_user(username: str, password: str) -> Optional[dict]:
|
|
user = get_user_by_username(username)
|
|
if not user:
|
|
return None
|
|
if not verify_password(password, user.get("password_hash", "")):
|
|
return None
|
|
return {
|
|
"username": user["username"],
|
|
"display_name": user.get("display_name") or user["username"],
|
|
"is_admin": bool(user.get("is_admin", False)),
|
|
}
|