fix(security & perf): implement bearer session tokens, salted pbkdf2, object pooling and background caching

This commit is contained in:
Antigravity committed 2026-09-17 13:14:31 +00:00
1 parent 258b1a9cb1
commit 7cd0792799
15 files changed
+1264 -105

No files matched your search

+156 -11
View File
@@ -1,13 +1,16 @@
#!/usr/bin/env python3
"""
SQLite Database Manager for Plants vs. Zombies Game (ai.qpid.pro)
Zero external dependencies. Manages users, game progress profiles, leaderboards, and bug reports.
Zero external dependencies. Manages users, game progress profiles, leaderboards, sessions, and bug reports.
"""
import sqlite3
import json
import os
from datetime import datetime
import secrets
import hashlib
import hmac
from datetime import datetime, timedelta
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DATA_DIR = os.path.join(BASE_DIR, "data")
@@ -20,6 +23,33 @@ def get_connection():
conn.row_factory = sqlite3.Row
return conn
def hash_password(password, salt=None):
"""Secure server-side salted PBKDF2 password hashing."""
if not password:
return ""
salt = salt or secrets.token_hex(16)
iterations = 100_000
derived = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt.encode("utf-8"), iterations).hex()
return f"pbkdf2:sha256:{iterations}:{salt}:{derived}"
def verify_password(password, stored_hash):
"""Verifies a password against stored hash with backward compatibility for legacy hashes."""
if not password or not stored_hash:
return False
if stored_hash.startswith("pbkdf2:sha256:"):
try:
parts = stored_hash.split(":")
if len(parts) == 5:
iterations = int(parts[2])
salt = parts[3]
expected = parts[4]
derived = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt.encode("utf-8"), iterations).hex()
return hmac.compare_digest(derived, expected)
except Exception:
return False
# Backward compatibility for plain/legacy hashes
return hmac.compare_digest(str(password), str(stored_hash))
def init_db():
conn = get_connection()
cursor = conn.cursor()
@@ -34,6 +64,17 @@ def init_db():
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS sessions (
token TEXT PRIMARY KEY,
user_id INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
""")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id)")
cursor.execute("""
CREATE TABLE IF NOT EXISTS profiles (
user_id INTEGER PRIMARY KEY,
@@ -81,6 +122,79 @@ def init_db():
conn.commit()
conn.close()
def create_session(user_id, duration_days=30):
"""Generates a secure 256-bit token and creates a session record."""
token = secrets.token_hex(32)
now = datetime.now()
created_at = now.strftime("%Y-%m-%d %H:%M:%S")
expires_at = (now + timedelta(days=duration_days)).strftime("%Y-%m-%d %H:%M:%S")
conn = get_connection()
cursor = conn.cursor()
cursor.execute(
"INSERT INTO sessions (token, user_id, created_at, expires_at) VALUES (?, ?, ?, ?)",
(token, user_id, created_at, expires_at)
)
conn.commit()
conn.close()
return token
def validate_session(token):
"""Validates session token. Returns user dict if valid, else None."""
if not token or not isinstance(token, str):
return None
token = token.strip()
if not token:
return None
conn = get_connection()
cursor = conn.cursor()
cursor.execute("""
SELECT u.id, u.username, u.created_at, u.last_login, s.expires_at, s.token
FROM sessions s
JOIN users u ON s.user_id = u.id
WHERE s.token = ?
""", (token,))
row = cursor.fetchone()
if not row:
conn.close()
return None
# Check expiration
try:
expires_at = datetime.strptime(row["expires_at"], "%Y-%m-%d %H:%M:%S")
if datetime.now() > expires_at:
cursor.execute("DELETE FROM sessions WHERE token = ?", (token,))
conn.commit()
conn.close()
return None
except Exception:
conn.close()
return None
user_info = {
"id": row["id"],
"user_id": row["id"],
"username": row["username"],
"createdAt": row["created_at"],
"lastLogin": row["last_login"],
"token": row["token"]
}
conn.close()
return user_info
def delete_session(token):
"""Deletes/invalidates a session by token."""
if not token:
return True
conn = get_connection()
cursor = conn.cursor()
cursor.execute("DELETE FROM sessions WHERE token = ?", (token.strip(),))
conn.commit()
conn.close()
return True
def get_default_profile_dict():
return {
"coins": 150,
@@ -133,10 +247,12 @@ def register_user(username, password_hash, initial_data=None):
cursor = conn.cursor()
try:
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
now = datetime.now()
now_str = now.strftime("%Y-%m-%d %H:%M:%S")
secure_hash = hash_password(password_hash)
cursor.execute(
"INSERT INTO users (username, password_hash, created_at, last_login) VALUES (?, ?, ?, ?)",
(username, password_hash, now_str, now_str)
(username, secure_hash, now_str, now_str)
)
user_id = cursor.lastrowid
@@ -168,9 +284,17 @@ def register_user(username, password_hash, initial_data=None):
now_str
))
# Create session token
token = secrets.token_hex(32)
expires_at = (now + timedelta(days=30)).strftime("%Y-%m-%d %H:%M:%S")
cursor.execute(
"INSERT INTO sessions (token, user_id, created_at, expires_at) VALUES (?, ?, ?, ?)",
(token, user_id, now_str, expires_at)
)
conn.commit()
conn.close()
return {"success": True, "user_id": user_id, "username": username, "data": profile_data}
return {"success": True, "token": token, "user_id": user_id, "username": username, "data": profile_data}
except sqlite3.IntegrityError:
conn.close()
return {"success": False, "message": "Пользователь с таким именем уже существует"}
@@ -189,13 +313,27 @@ def authenticate_user(username, password_hash):
conn.close()
return None
if user["password_hash"] != password_hash:
if not verify_password(password_hash, user["password_hash"]):
conn.close()
return {"success": False, "message": "Неверный пароль"}
# Update last login
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
cursor.execute("UPDATE users SET last_login = ? WHERE id = ?", (now_str, user["id"]))
now = datetime.now()
now_str = now.strftime("%Y-%m-%d %H:%M:%S")
# Seamless upgrade from legacy plaintext/djb2 hash to salted PBKDF2
if not user["password_hash"].startswith("pbkdf2:"):
new_secure_hash = hash_password(password_hash)
cursor.execute("UPDATE users SET password_hash = ?, last_login = ? WHERE id = ?", (new_secure_hash, now_str, user["id"]))
else:
cursor.execute("UPDATE users SET last_login = ? WHERE id = ?", (now_str, user["id"]))
# Create session token
token = secrets.token_hex(32)
expires_at = (now + timedelta(days=30)).strftime("%Y-%m-%d %H:%M:%S")
cursor.execute(
"INSERT INTO sessions (token, user_id, created_at, expires_at) VALUES (?, ?, ?, ?)",
(token, user["id"], now_str, expires_at)
)
conn.commit()
# Fetch profile
@@ -227,10 +365,17 @@ def authenticate_user(username, password_hash):
"achievements": json.loads(p["achievements"] or "{}")
}
return {"success": True, "user_id": user["id"], "username": user["username"], "data": profile_dict}
return {"success": True, "token": token, "user_id": user["id"], "username": user["username"], "data": profile_dict}
def save_user_profile(username, data):
def save_user_profile(username, data, token=None):
username = username.strip()
# If token is provided, verify it belongs to username
if token is not None:
session_user = validate_session(token)
if not session_user or session_user["username"].lower() != username.lower():
return {"success": False, "message": "Недействительный токен авторизации или доступ запрещен"}
conn = get_connection()
cursor = conn.cursor()