feat: Implement hashed admin password support and centralize password logic

Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
This commit is contained in:
2025-11-22 20:50:19 -07:00
parent 7b1e3da8b0
commit ebd120b2cd
2 changed files with 62 additions and 38 deletions

View File

@@ -16,6 +16,7 @@ import json
import hashlib
import os
import sqlite3
import binascii
from datetime import datetime
from typing import Dict, List, Optional
@@ -223,6 +224,34 @@ def read_exif_datetimes(file_bytes: bytes):
pass
return created, modified
def _hash_password(pw: str) -> str:
"""Return PBKDF2-SHA256 hash of a password."""
try:
if not pw:
return ""
salt = os.urandom(16)
iterations = 200_000
dk = hashlib.pbkdf2_hmac('sha256', pw.encode('utf-8'), salt, iterations)
return f"pbkdf2_sha256${iterations}${binascii.hexlify(salt).decode()}${binascii.hexlify(dk).decode()}"
except Exception:
return ""
def _verify_password(stored: str, pw: Optional[str]) -> bool:
"""Verify a password against a PBKDF2-SHA256 hash."""
if not pw or not stored:
return False
try:
algo, iter_s, salt_hex, hash_hex = stored.split("$")
if algo != 'pbkdf2_sha256':
return False
iterations = int(iter_s)
salt = binascii.unhexlify(salt_hex)
dk = hashlib.pbkdf2_hmac('sha256', pw.encode('utf-8'), salt, iterations)
return binascii.hexlify(dk).decode() == hash_hex
except Exception:
return False
def get_or_create_album_dir(album_name: str) -> str:
"""Get or create a directory for an album. Returns the path."""
if not album_name or not isinstance(album_name, str):
@@ -858,7 +887,14 @@ async def api_login(request: Request) -> JSONResponse:
if not email or not password:
return JSONResponse({"error": "missing_credentials"}, status_code=400)
if email == "admin" and password == SETTINGS.admin_password:
stored_password = SETTINGS.admin_password
password_ok = False
if stored_password.startswith("pbkdf2_sha256$"):
password_ok = _verify_password(stored_password, password)
else:
password_ok = (password == stored_password)
if email == "admin" and password_ok:
user_info = {
"accessToken": "local_admin_session", # dummy value
"userEmail": "admin",
@@ -1028,19 +1064,7 @@ async def api_invites_create(request: Request) -> JSONResponse:
import uuid
token = uuid.uuid4().hex
# Prepare password hash, if provided
def hash_password(pw: str) -> str:
try:
if not pw:
return ""
import os as _os
import binascii as _binascii
salt = _os.urandom(16)
iterations = 200_000
dk = hashlib.pbkdf2_hmac('sha256', pw.encode('utf-8'), salt, iterations)
return f"pbkdf2_sha256${iterations}${_binascii.hexlify(salt).decode()}${_binascii.hexlify(dk).decode()}"
except Exception:
return ""
pw_hash = hash_password(invite_password or "") if (invite_password and str(invite_password).strip()) else None
pw_hash = _hash_password(invite_password or "") if (invite_password and str(invite_password).strip()) else None
# Owner info from session
owner_user_id = str(request.session.get("userId") or "")
owner_email = str(request.session.get("userEmail") or "")
@@ -1235,16 +1259,8 @@ async def api_invite_update(token: str, request: Request) -> JSONResponse:
if "password" in (body or {}):
pw = str((body or {}).get("password") or "").strip()
if pw:
# Reuse hasher from above
def _hash_pw(pw: str) -> str:
import os as _os
import binascii as _binascii
salt = _os.urandom(16)
iterations = 200_000
dk = hashlib.pbkdf2_hmac('sha256', pw.encode('utf-8'), salt, iterations)
return f"pbkdf2_sha256${iterations}${_binascii.hexlify(salt).decode()}${_binascii.hexlify(dk).decode()}"
fields.append("password_hash = ?")
params.append(_hash_pw(pw))
params.append(_hash_password(pw))
else:
fields.append("password_hash = NULL")
# Reset usage
@@ -1488,21 +1504,7 @@ async def api_invite_auth(token: str, request: Request) -> JSONResponse:
request.session["inviteAuth"] = ia
return JSONResponse({"ok": True, "authorized": True})
# verify
def verify_password(stored: str, pw: Optional[str]) -> bool:
if not pw:
return False
try:
algo, iter_s, salt_hex, hash_hex = stored.split("$")
if algo != 'pbkdf2_sha256':
return False
iterations = int(iter_s)
import binascii as _binascii
salt = _binascii.unhexlify(salt_hex)
dk = hashlib.pbkdf2_hmac('sha256', pw.encode('utf-8'), salt, iterations)
return _binascii.hexlify(dk).decode() == hash_hex
except Exception:
return False
if not verify_password(password_hash, provided):
if not _verify_password(password_hash, provided):
return JSONResponse({"error": "invalid_password"}, status_code=403)
ia = request.session.get("inviteAuth") or {}
ia[token] = True