From 83d4f88eb5cf951c697f97b866836369e039fc6a Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Fri, 20 Feb 2026 09:15:14 +0300 Subject: [PATCH] Enha: input check; hash passwords --- handlers/auth.go | 13 ++-- handlers/sqlite:gralias.db | 0 scripts/migrate_passwords.py | 113 +++++++++++++++++++++++++++++++++++ utils/main.go | 14 +++++ 4 files changed, 136 insertions(+), 4 deletions(-) delete mode 100644 handlers/sqlite:gralias.db create mode 100755 scripts/migrate_passwords.py diff --git a/handlers/auth.go b/handlers/auth.go index 2f82578..db494de 100644 --- a/handlers/auth.go +++ b/handlers/auth.go @@ -2,8 +2,10 @@ package handlers import ( "crypto/hmac" + "crypto/md5" "crypto/sha256" "encoding/base64" + "encoding/hex" "fmt" "gralias/models" "gralias/utils" @@ -65,7 +67,7 @@ func HandleFrontLogin(w http.ResponseWriter, r *http.Request) { return } username := r.PostFormValue("username") - if username == "" { + if username == "" || !utils.IsInputSane(username) { msg := "username not provided" log.Error(msg) abortWithError(w, msg) @@ -77,6 +79,9 @@ func HandleFrontLogin(w http.ResponseWriter, r *http.Request) { // make sure username does not exists cleanName := utils.RemoveSpacesFromStr(username) clearPass := utils.RemoveSpacesFromStr(password) + // hash the password with md5 + hash := md5.Sum([]byte(clearPass)) + hashedPass := hex.EncodeToString(hash[:]) // check if that user was already in db userstate, err := repo.PlayerGetByName(r.Context(), cleanName) if err != nil || userstate == nil { @@ -84,8 +89,8 @@ func HandleFrontLogin(w http.ResponseWriter, r *http.Request) { userstate = models.InitPlayer(cleanName) makeplayer = true } else { - if userstate.Password != clearPass { - log.Error("wrong password", "username", cleanName, "password", clearPass) + if userstate.Password != hashedPass { + log.Error("wrong password", "username", cleanName) abortWithError(w, "wrong password") return } @@ -126,7 +131,7 @@ func HandleFrontLogin(w http.ResponseWriter, r *http.Request) { } // save state to cache if makeplayer { - userstate.Password = clearPass + userstate.Password = hashedPass if err := repo.PlayerAdd(r.Context(), userstate); err != nil { log.Error("failed to save state", "error", err) abortWithError(w, err.Error()) diff --git a/handlers/sqlite:gralias.db b/handlers/sqlite:gralias.db deleted file mode 100644 index e69de29..0000000 diff --git a/scripts/migrate_passwords.py b/scripts/migrate_passwords.py new file mode 100755 index 0000000..7a0d34c --- /dev/null +++ b/scripts/migrate_passwords.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +""" +One-time script to migrate player passwords from plaintext to MD5 hashes. + +This script: +1. Connects to the SQLite database +2. Retrieves all players with their passwords +3. Skips passwords that already look like MD5 hashes (32 hex chars) +4. Hashes plaintext passwords with MD5 +5. Updates the database with hashed passwords + +Usage: + python scripts/migrate_passwords.py [path_to_db] + +If no path is provided, defaults to 'gralias.db' in the current directory. +""" + +import hashlib +import re +import sqlite3 +import sys +from pathlib import Path + + +def is_md5_hash(password: str) -> bool: + """Check if password looks like an MD5 hash (32 hex characters).""" + return bool(re.match(r"^[a-fA-F0-9]{32}$", password)) + + +def hash_password(password: str) -> str: + """Hash password with MD5.""" + return hashlib.md5(password.encode("utf-8")).hexdigest() + + +def migrate_passwords(db_path: str) -> None: + """Migrate all player passwords to MD5 hashes.""" + print(f"Connecting to database: {db_path}") + + if not Path(db_path).exists(): + print(f"Error: Database file not found: {db_path}") + sys.exit(1) + + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + try: + # Get all players + cursor.execute("SELECT username, password FROM players;") + players = cursor.fetchall() + + if not players: + print("No players found in database.") + return + + print(f"Found {len(players)} player(s)") + print("-" * 60) + + migrated = 0 + skipped = 0 + errors = 0 + + for username, password in players: + if not password: + print(f"[SKIP] {username}: empty password") + skipped += 1 + continue + + if is_md5_hash(password): + print(f"[SKIP] {username}: already MD5 hashed") + skipped += 1 + continue + + # Hash the password + hashed = hash_password(password) + + try: + cursor.execute( + "UPDATE players SET password = ? WHERE username = ?;", + (hashed, username), + ) + print(f"[MIGRATED] {username}: '{password}' -> '{hashed}'") + migrated += 1 + except sqlite3.Error as e: + print(f"[ERROR] {username}: {e}") + errors += 1 + + print("-" * 60) + print(f"Summary: {migrated} migrated, {skipped} skipped, {errors} errors") + + # Commit changes + conn.commit() + print("Changes committed to database.") + + except sqlite3.Error as e: + print(f"Database error: {e}") + conn.rollback() + sys.exit(1) + finally: + conn.close() + + +def main(): + # Get database path from command line or use default + if len(sys.argv) > 1: + db_path = sys.argv[1] + else: + db_path = "gralias.db" + + migrate_passwords(db_path) + + +if __name__ == "__main__": + main() diff --git a/utils/main.go b/utils/main.go index 57b6253..c3d58b8 100644 --- a/utils/main.go +++ b/utils/main.go @@ -5,6 +5,10 @@ import ( "unicode" ) +const ( + SaneLengthMax = 20 +) + func RemoveSpacesFromStr(origin string) string { return strings.Map(func(r rune) rune { if unicode.IsSpace(r) { @@ -35,3 +39,13 @@ func RemoveFromSlice(key string, sl []string) []string { } return resp } + +func IsInputSane(s string) bool { + if len(s) > SaneLengthMax { + return false + } + if strings.ContainsAny(s, "{}?!$&:/[]~") { + return false + } + return true +}