From 43f57c55f55c8c97865bf8c16f2d8b6158ef7658 Mon Sep 17 00:00:00 2001 From: Hera Zhao Date: Tue, 7 Apr 2026 21:11:44 +0000 Subject: [PATCH] Hash passwords with PBKDF2-SHA256 instead of storing plaintext Existing plaintext passwords are auto-upgraded to hashed on next login. Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/main.py | 51 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/backend/main.py b/backend/main.py index 8818527..b213cee 100644 --- a/backend/main.py +++ b/backend/main.py @@ -6,9 +6,35 @@ import json import os from backend.database import get_db, init_db, seed_defaults, log_audit +import hashlib +import secrets as _secrets app = FastAPI(title="Essential Oil Formula Calculator API") + +# ── Password hashing (PBKDF2-SHA256, stdlib) ───────── +def hash_password(password: str) -> str: + salt = _secrets.token_hex(16) + h = hashlib.pbkdf2_hmac("sha256", password.encode(), salt.encode(), 200_000) + return f"{salt}${h.hex()}" + + +def verify_password(password: str, stored: str) -> bool: + if not stored: + return False + if "$" not in stored: + # Legacy plaintext — compare directly + return password == stored + salt, h = stored.split("$", 1) + return hashlib.pbkdf2_hmac("sha256", password.encode(), salt.encode(), 200_000).hex() == h + + +def _upgrade_password_if_needed(conn, user_id: int, password: str, stored: str): + """If stored password is legacy plaintext, upgrade to hashed.""" + if stored and "$" not in stored: + conn.execute("UPDATE users SET password = ? WHERE id = ?", (hash_password(password), user_id)) + conn.commit() + # Periodic WAL checkpoint to ensure data is flushed to main DB file import threading, time as _time def _wal_checkpoint_loop(): @@ -312,7 +338,6 @@ def symptom_search(body: dict, user=Depends(get_current_user)): # ── Register ──────────────────────────────────────────── @app.post("/api/register", status_code=201) def register(body: dict): - import secrets username = body.get("username", "").strip() password = body.get("password", "").strip() display_name = body.get("display_name", "").strip() @@ -320,12 +345,12 @@ def register(body: dict): raise HTTPException(400, "用户名至少2个字符") if not password or len(password) < 4: raise HTTPException(400, "密码至少4位") - token = secrets.token_hex(24) + token = _secrets.token_hex(24) conn = get_db() try: conn.execute( "INSERT INTO users (username, token, role, display_name, password) VALUES (?, ?, ?, ?, ?)", - (username, token, "viewer", display_name or username, password) + (username, token, "viewer", display_name or username, hash_password(password)) ) conn.commit() except Exception: @@ -343,14 +368,19 @@ def login(body: dict): if not username or not password: raise HTTPException(400, "请输入用户名和密码") conn = get_db() - user = conn.execute("SELECT token, password, display_name, role FROM users WHERE username = ?", (username,)).fetchone() - conn.close() + user = conn.execute("SELECT id, token, password, display_name, role FROM users WHERE username = ?", (username,)).fetchone() if not user: + conn.close() raise HTTPException(401, "用户名不存在") if not user["password"]: + conn.close() raise HTTPException(401, "该账号未设置密码,请使用链接登录后设置密码") - if user["password"] != password: + if not verify_password(password, user["password"]): + conn.close() raise HTTPException(401, "密码错误") + # Auto-upgrade legacy plaintext password to hashed + _upgrade_password_if_needed(conn, user["id"], password, user["password"]) + conn.close() return {"token": user["token"], "display_name": user["display_name"], "role": user["role"]} @@ -385,11 +415,11 @@ def update_me(body: dict, user=Depends(get_current_user)): raise HTTPException(400, "新密码至少4位") old_pw = body.get("old_password", "").strip() current_pw = user.get("password") or "" - if current_pw and old_pw != current_pw: + if current_pw and not verify_password(old_pw, current_pw): conn.close() raise HTTPException(400, "当前密码不正确") if pw: - conn.execute("UPDATE users SET password = ? WHERE id = ?", (pw, user["id"])) + conn.execute("UPDATE users SET password = ? WHERE id = ?", (hash_password(pw), user["id"])) conn.commit() conn.close() return {"ok": True} @@ -404,7 +434,7 @@ def set_password(body: dict, user=Depends(get_current_user)): if not pw or len(pw) < 4: raise HTTPException(400, "密码至少4位") conn = get_db() - conn.execute("UPDATE users SET password = ? WHERE id = ?", (pw, user["id"])) + conn.execute("UPDATE users SET password = ? WHERE id = ?", (hash_password(pw), user["id"])) conn.commit() conn.close() return {"ok": True} @@ -896,8 +926,7 @@ def list_users(user=Depends(require_role("admin"))): @app.post("/api/users", status_code=201) def create_user(body: UserIn, user=Depends(require_role("admin"))): - import secrets - token = secrets.token_hex(24) + token = _secrets.token_hex(24) conn = get_db() try: conn.execute(