Files
vault-tools/custom-vault.py
T

172 lines
5.7 KiB
Python
Raw Normal View History

2026-04-30 20:16:25 -05:00
import sqlite3
import threading
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Any, Optional
import uvicorn
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
TOKEN = "changeme" # API Token
DB_PATH = Path("vault.db") # SQLite DB
HOST = "0.0.0.0" # Address
PORT = 0000 # Port
_local = threading.local()
def get_conn() -> sqlite3.Connection:
if not hasattr(_local, "conn"):
_local.conn = sqlite3.connect(DB_PATH)
_local.conn.row_factory = sqlite3.Row
return _local.conn
def has_table(service: str) -> bool:
cur = get_conn().execute(
"SELECT count(name) FROM sqlite_master WHERE type='table' AND name=?", (service,)
)
return cur.fetchone()[0] == 1
def create_table(service: str) -> None:
if has_table(service):
return
conn = get_conn()
conn.execute(f"""
CREATE TABLE IF NOT EXISTS `{service}` (
id INTEGER NOT NULL UNIQUE,
kid TEXT NOT NULL COLLATE NOCASE,
key_ TEXT NOT NULL COLLATE NOCASE,
PRIMARY KEY(id AUTOINCREMENT),
UNIQUE(kid, key_)
)
""")
conn.commit()
def db_get_key(kid: str, service: str) -> Optional[str]:
for svc in {service, service.lower(), service.upper()}:
if not has_table(svc):
continue
row = get_conn().execute(
f"SELECT key_ FROM `{svc}` WHERE kid=? AND key_!=?", (kid, "0" * 32)
).fetchone()
if row:
return row[0]
return None
def db_get_keys(service: str) -> list[tuple[str, str]]:
if not has_table(service):
return []
rows = get_conn().execute(
f"SELECT kid, key_ FROM `{service}` WHERE key_!=?", ("0" * 32,)
).fetchall()
return [(r[0], r[1]) for r in rows]
def db_insert_key(service: str, kid: str, key: str) -> bool:
create_table(service)
conn = get_conn()
existing = conn.execute(
f"SELECT id FROM `{service}` WHERE kid=? AND key_=?", (kid, key)
).fetchone()
if existing:
return False
conn.execute(f"INSERT INTO `{service}` (kid, key_) VALUES (?, ?)", (kid, key))
conn.commit()
return True
def db_get_services() -> list[str]:
rows = get_conn().execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name != 'sqlite_sequence'"
).fetchall()
return [r[0] for r in rows]
def db_key_count(service: str) -> int:
if not has_table(service):
return 0
row = get_conn().execute(f"SELECT count(*) FROM `{service}`").fetchone()
return row[0]
@asynccontextmanager
async def lifespan(app: FastAPI):
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
print(f" Vault DB : {DB_PATH.resolve()}")
print(f" Listening: http://{HOST}:{PORT}/vault/")
print(f" Token : {TOKEN}")
yield
app = FastAPI(lifespan=lifespan)
def ok(data: Any) -> JSONResponse:
return JSONResponse({"status_code": 200, "message": data})
def err(msg: str, code: int = 400) -> JSONResponse:
return JSONResponse({"status_code": code, "error": msg}, status_code=code)
@app.post("/vault/")
async def vault(request: Request) -> JSONResponse:
try:
body = await request.json()
except Exception:
return err("Invalid JSON body")
# auth
if body.get("token") != TOKEN:
return err("Unauthorized", 401)
method = body.get("method", "")
params = body.get("params", {})
if method == "GetKey":
kid = params.get("kid", "").strip().lower()
service = params.get("service", "").strip()
if not kid:
return JSONResponse({"status": "error", "message": "KID is required"}, status_code=400)
if not service:
return JSONResponse({"status": "error", "message": "service is required"}, status_code=400)
key = db_get_key(kid, service)
if key:
return ok({"keys": [f"{kid}:{key}"], "session_id": None})
return ok({"keys": [], "session_id": None})
elif method == "GetKeys":
service = params.get("service", "").strip()
if not service:
return JSONResponse({"status": "error", "message": "service is required"}, status_code=400)
pairs = db_get_keys(service)
keys = [f"{kid}:{key}" for kid, key in pairs]
return ok({"keys": keys, "count": len(keys), "session_id": None})
elif method == "InsertKey":
kid = params.get("kid", "").strip().lower()
key = params.get("key", "").strip().lower()
service = params.get("service", "").strip()
if not kid or not key or not service:
return JSONResponse({"status": "error", "message": "kid:key and service are required"}, status_code=400)
if len(kid) != 32 or len(key) != 32:
return JSONResponse({"status": "error", "message": "kid:key must be 32 hex characters"}, status_code=400)
if key == "0" * 32:
return JSONResponse({"status": "error", "message": "NULL keys are not allowed"}, status_code=400)
inserted = db_insert_key(service, kid, key)
return ok({"inserted": inserted, "status": "inserted" if inserted else "cached", "session_id": None})
elif method == "GetServices":
services = db_get_services()
return ok({"services": services, "session_id": None})
elif method == "Stats":
services = db_get_services()
stats = {svc: db_key_count(svc) for svc in services}
total = sum(stats.values())
return ok({"services": stats, "total_keys": total, "session_id": None})
else:
return JSONResponse(
{"status_code": 400, "error": f"Unknown method: {method}", "type": "invalid_method"},
status_code=400,
)
if __name__ == "__main__":
uvicorn.run(app, host=HOST, port=PORT)