From 0a31b9939077062657dfa41fc8edc37b68e165ba Mon Sep 17 00:00:00 2001 From: matcha Date: Sun, 4 Oct 2026 02:57:58 +0000 Subject: [PATCH] Consolidate Persimmons shared backend and tests --- backend/shared/human_vault.py | 147 ++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 backend/shared/human_vault.py diff --git a/backend/shared/human_vault.py b/backend/shared/human_vault.py new file mode 100644 index 0000000..ce4151b --- /dev/null +++ b/backend/shared/human_vault.py @@ -0,0 +1,147 @@ +"""Browser-only opaque storage. Deliberately has no crypto or MCP registration.""" +from __future__ import annotations + +import base64 +import binascii +import json +import os +import re +import sqlite3 +from urllib.parse import urlsplit +from contextlib import closing + +from starlette.responses import JSONResponse, Response +from starlette.routing import Route + +EXTENSION_ORIGIN = "chrome-extension://kiolgjoohhgdohcafgelhikpgkbmckbg" +PREFIX = "/chat/persimmons/human-vault/items" +MAX_BODY = 24 * 1024 +ITEM_ID = re.compile(r"[0-9a-f]{32}") + + +def validate_item(item): + if not isinstance(item, dict) or set(item) not in ({"v", "id", "origin", "iv", "ciphertext"}, {"v", "id", "origin", "username", "iv", "ciphertext"}): + raise ValueError() + if type(item["v"]) is not int or item["v"] != 1: + raise ValueError() + if not isinstance(item["id"], str) or not ITEM_ID.fullmatch(item["id"]): + raise ValueError() + if not isinstance(item["origin"], str): + raise ValueError() + try: + parsed = urlsplit(item["origin"]) + _ = parsed.port + except ValueError: + raise ValueError() + if (parsed.scheme != "https" or not parsed.hostname or parsed.username or parsed.password or + parsed.path or parsed.query or parsed.fragment or parsed.geturl() != item["origin"]): + raise ValueError() + username = item.get("username") + if username is not None and (not isinstance(username, str) or not username or len(username) > 512 or any(ord(c) < 32 for c in username)): + raise ValueError() + for name, low, high in (("iv", 12, 12), ("ciphertext", 17, 16384)): + value = item[name] + if not isinstance(value, str): + raise ValueError() + raw = base64.b64decode(value, validate=True) + if not low <= len(raw) <= high or base64.b64encode(raw).decode() != value: + raise ValueError() + return item + + +class Store: + def __init__(self, path): + self.path = path + + def operate(self, owner, method, item_id=None, item=None): + with closing(sqlite3.connect(self.path, timeout=5)) as db, db: + db.execute("CREATE TABLE IF NOT EXISTS human_items (owner TEXT NOT NULL, id TEXT NOT NULL, origin TEXT NOT NULL, iv TEXT NOT NULL, ciphertext TEXT NOT NULL, username TEXT, PRIMARY KEY(owner,id))") + columns = {row[1] for row in db.execute("PRAGMA table_info(human_items)")} + if "username" not in columns: + db.execute("ALTER TABLE human_items ADD COLUMN username TEXT") + if method == "POST": + if db.execute("SELECT count(*) FROM human_items WHERE owner=?", (owner,)).fetchone()[0] >= 200: + return {"error": "item limit reached"}, 409 + try: + db.execute("INSERT INTO human_items (owner,id,origin,iv,ciphertext,username) VALUES (?,?,?,?,?,?)", (owner, item["id"], item["origin"], item["iv"], item["ciphertext"], item.get("username"))) + except sqlite3.IntegrityError: + return {"error": "item already exists"}, 409 + elif method == "PUT": + changed = db.execute("UPDATE human_items SET origin=?,iv=?,ciphertext=?,username=COALESCE(?,username) WHERE owner=? AND id=?", (item["origin"], item["iv"], item["ciphertext"], item.get("username"), owner, item_id)) + if not changed.rowcount: + return {"error": "not found"}, 404 + elif method == "DELETE": + changed = db.execute("DELETE FROM human_items WHERE owner=? AND id=?", (owner, item_id)) + if not changed.rowcount: + return {"error": "not found"}, 404 + return {"owner": owner, "ok": True}, 200 + elif item_id is None: + rows = db.execute("SELECT id,origin,username FROM human_items WHERE owner=? ORDER BY origin,username,id LIMIT 200", (owner,)).fetchall() + return {"owner": owner, "items": [{"v": 1, "id": row[0], "origin": row[1], **({"username": row[2]} if row[2] else {})} for row in rows]}, 200 + else: + row = db.execute("SELECT origin,iv,ciphertext,username FROM human_items WHERE owner=? AND id=?", (owner, item_id)).fetchone() + if row is None: + return {"error": "not found"}, 404 + item = dict(v=1, id=item_id, origin=row[0], iv=row[1], ciphertext=row[2], **({"username": row[3]} if row[3] else {})) + return {"owner": owner, "item": item}, 201 if method == "POST" else 200 + + +def routes(resolve_actor): + async def endpoint(request): + headers = {"Cache-Control": "no-store", "Vary": "Origin"} + + def reply(payload, code): + return JSONResponse(payload, status_code=code, headers=headers) + + # Do not trust forwarded headers here; the ASGI server's trusted proxy + # configuration owns scheme handling. No bearer/PAT fallback. + if request.url.scheme != "https" or request.headers.getlist("origin") != [EXTENSION_ORIGIN]: + return reply({"error": "forbidden"}, 403) + headers.update({"Access-Control-Allow-Origin": EXTENSION_ORIGIN, "Access-Control-Allow-Credentials": "true"}) + if request.url.query: + return reply({"error": "query forbidden"}, 400) + if request.method == "OPTIONS": + requested = {h.strip().lower() for h in request.headers.get("access-control-request-headers", "").split(",") if h.strip()} + if request.headers.get("access-control-request-method") not in {"GET", "POST", "PUT", "DELETE"} or not requested <= {"content-type", "x-vault-owner"}: + return reply({"error": "forbidden"}, 403) + return Response(status_code=204, headers={**headers, "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE", "Access-Control-Allow-Headers": "Content-Type, X-Vault-Owner"}) + if not request.headers.get("cookie") or request.headers.get("authorization"): + return reply({"error": "unauthorized"}, 401) + try: + actor = await resolve_actor(request) + except Exception: + return reply({"error": "session unavailable"}, 503) + if not actor: + return reply({"error": "unauthorized"}, 401) + owner = actor.lower() + item_id = request.path_params.get("item_id") + if item_id is not None and not ITEM_ID.fullmatch(item_id): + return reply({"error": "not found"}, 404) + if (item_id is not None or request.method != "GET") and request.headers.get("x-vault-owner") != owner: + return reply({"error": "session changed"}, 409) + item = None + if request.method in {"POST", "PUT"}: + if request.headers.get("content-type", "").split(";")[0] != "application/json": + return reply({"error": "JSON required"}, 415) + try: + raw = bytearray() + async for chunk in request.stream(): + raw.extend(chunk) + if len(raw) > MAX_BODY: + raise ValueError() + item = validate_item(json.loads(raw)) + if item_id is not None and item["id"] != item_id: + raise ValueError() + except (ValueError, TypeError, binascii.Error): + return reply({"error": "invalid encrypted item"}, 400) + path = os.environ.get("PERSIMMONS_HUMAN_VAULT_DB") + if not path: + return reply({"error": "vault not configured"}, 503) + try: + payload, code = Store(path).operate(owner, request.method, item_id, item) + except (sqlite3.Error, OSError): + return reply({"error": "vault unavailable"}, 503) + return reply(payload, code) + + return [Route(PREFIX, endpoint, methods=["GET", "POST", "OPTIONS"]), + Route(PREFIX + "/{item_id}", endpoint, methods=["GET", "PUT", "DELETE", "OPTIONS"])]