"""credentials.py — single-user password + session secret for ani-web. Run once: .venv/bin/python -m server.credentials Stores a PBKDF2 password hash and a random session-signing secret in server/data/credentials.json (mode 0600). No env vars, no DB. """ import getpass import hashlib import json import os import secrets from pathlib import Path FILE = Path(__file__).parent / "data" / "credentials.json" ITER = 210_000 # OWASP 2023 minimum for PBKDF2-SHA256 def set_password(): pw = getpass.getpass("Choose a password (min 8 chars): ") if len(pw) < 8: raise SystemExit("password too short") if pw != getpass.getpass("Again: "): raise SystemExit("passwords did not match") salt = secrets.token_bytes(16) data = { "v": 1, "salt": salt.hex(), "hash": hashlib.pbkdf2_hmac("sha256", pw.encode(), salt, ITER).hex(), "secret": secrets.token_hex(32), } FILE.parent.mkdir(exist_ok=True) # ponytail: mode 0600 on create; won't tighten an existing file fd = os.open(FILE, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) with os.fdopen(fd, "w") as f: json.dump(data, f) print(f"credentials saved to {FILE}") def load(): if not FILE.exists(): raise SystemExit( "no credentials yet — set a password first:\n" " .venv/bin/python -m server.credentials") return json.loads(FILE.read_text()) def check(password: str) -> bool: d = load() h = hashlib.pbkdf2_hmac("sha256", password.encode(), bytes.fromhex(d["salt"]), d.get("iter", ITER)) return secrets.compare_digest(h.hex(), d["hash"]) if __name__ == "__main__": set_password()