"""Passkey (WebAuthn) credential storage + config. One "household" identity, many enrolled devices (each device = one credential). Credentials live in passkeys.json on the volume (public keys only — there is no shared password to leak). RP config + the AUTH_ENABLED gate come from env so the hostname can change (mkcert host, Tailscale ts.net name, etc.) without code edits. """ import os import secrets from storage import DATA_DIR, load_json, save_json PASSKEYS_FILE = os.path.join(DATA_DIR, "passkeys.json") # ---- config (env-driven) ---- def rp_id(): return os.environ.get("WEBAUTHN_RP_ID", "budget.nastynas.xyz") def rp_name(): return os.environ.get("WEBAUTHN_RP_NAME", "Moon Household Budget") def origin(): return os.environ.get("WEBAUTHN_ORIGIN", f"https://{rp_id()}") def auth_enabled(): return os.environ.get("AUTH_ENABLED", "0").lower() in ("1", "true", "yes", "on") def enrollment_code(): return os.environ.get("PASSKEY_ENROLLMENT_CODE", "") # ---- credential store ---- def _data(): d = load_json(PASSKEYS_FILE, {}) or {} changed = False if "credentials" not in d: d["credentials"] = [] changed = True if not d.get("user_handle"): d["user_handle"] = secrets.token_urlsafe(16) changed = True if changed: save_json(PASSKEYS_FILE, d) return d def user_handle_bytes(): return _data()["user_handle"].encode() def list_credentials(): return _data()["credentials"] def has_credentials(): return len(_data()["credentials"]) > 0 def get_credential(cred_id_b64): for c in _data()["credentials"]: if c["id"] == cred_id_b64: return c return None def add_credential(cred_id_b64, public_key_b64, sign_count, label, transports=None): d = _data() d["credentials"].append({ "id": cred_id_b64, "public_key": public_key_b64, "sign_count": sign_count, "label": label or "Device", "transports": transports or [], }) save_json(PASSKEYS_FILE, d) def update_sign_count(cred_id_b64, new_count): d = _data() for c in d["credentials"]: if c["id"] == cred_id_b64: c["sign_count"] = new_count save_json(PASSKEYS_FILE, d) def remove_credential(cred_id_b64): d = _data() d["credentials"] = [c for c in d["credentials"] if c["id"] != cred_id_b64] save_json(PASSKEYS_FILE, d)