From 51c2e64319f76e2d6011fe6de6c86d93af0c3568 Mon Sep 17 00:00:00 2001 From: Johannes Date: Fri, 13 Mar 2026 00:31:19 +0100 Subject: [PATCH] Add password protection with double hashing (SHA-256 client, bcrypt server) --- app.py | 35 ++++++++++++++++++++++++++++++----- requirements.txt | 1 + 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/app.py b/app.py index 3984ec5..f6b0ae2 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,6 @@ from flask import Flask, request, jsonify, send_from_directory, abort from flask_cors import CORS +import bcrypt import os import re @@ -9,6 +10,7 @@ CORS(app, origins="*", allow_headers="*", methods=["GET", "POST", "DELETE", "OPT SAVES_DIR = os.environ.get("SAVES_DIR", "/saves") os.makedirs(SAVES_DIR, exist_ok=True) + def safe_filename(name): name = re.sub(r"[^\w\-. ]", "", name).strip() if not name: @@ -16,17 +18,32 @@ def safe_filename(name): return name + ".puC" +def get_hash_path(name): + return os.path.join(SAVES_DIR, safe_filename(name).replace(".puC", ".hash")) + + +def check_password(name, req): + hash_path = get_hash_path(name) + if not os.path.exists(hash_path): + return # unprotected + client_hash = req.headers.get("X-Password-Hash", "") + if not client_hash: + abort(401, "Password required") + stored = open(hash_path, "rb").read() + if not bcrypt.checkpw(client_hash.encode(), stored): + abort(401, "Wrong password") + + @app.route("/saves", methods=["GET"]) def list_saves(): - files = [ - f[:-4] for f in os.listdir(SAVES_DIR) - if f.endswith(".puC") - ] - return jsonify(sorted(files)) + saves = sorted(f[:-4] for f in os.listdir(SAVES_DIR) if f.endswith(".puC")) + protected = sorted(f[:-5] for f in os.listdir(SAVES_DIR) if f.endswith(".hash")) + return jsonify({"saves": saves, "protected": protected}) @app.route("/saves/", methods=["GET"]) def get_save(name): + check_password(name, request) filename = safe_filename(name) path = os.path.join(SAVES_DIR, filename) if not os.path.exists(path): @@ -42,16 +59,24 @@ def store_save(name): abort(400, "No data") with open(os.path.join(SAVES_DIR, filename), "wb") as f: f.write(data) + ph = request.headers.get("X-Password-Hash", "") + if ph: + hashed = bcrypt.hashpw(ph.encode(), bcrypt.gensalt()) + open(get_hash_path(name), "wb").write(hashed) return jsonify({"ok": True, "name": name}) @app.route("/saves/", methods=["DELETE"]) def delete_save(name): + check_password(name, request) filename = safe_filename(name) path = os.path.join(SAVES_DIR, filename) if not os.path.exists(path): abort(404) os.remove(path) + hash_path = get_hash_path(name) + if os.path.exists(hash_path): + os.remove(hash_path) return jsonify({"ok": True}) diff --git a/requirements.txt b/requirements.txt index 7fbb9cf..ff06431 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ flask==3.1.0 flask-cors==5.0.1 gunicorn==23.0.0 +bcrypt==4.2.1